Merge "Adapt GraphState to CameraState" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index d51c200..a0a7d61 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index b28eac7..435ee81 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,10 +33,10 @@
     api("androidx.core:core-ktx:1.1.0") {
         because "Mirror activity dependency graph for -ktx artifacts"
     }
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") {
+    api(project(":lifecycle:lifecycle-runtime-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.savedstate:savedstate-ktx:1.2.0") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
diff --git a/activity/activity/api/current.txt b/activity/activity/api/current.txt
index a6fafc74..9a04dca 100644
--- a/activity/activity/api/current.txt
+++ b/activity/activity/api/current.txt
@@ -73,7 +73,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/api/public_plus_experimental_current.txt b/activity/activity/api/public_plus_experimental_current.txt
index a6fafc74..9a04dca 100644
--- a/activity/activity/api/public_plus_experimental_current.txt
+++ b/activity/activity/api/public_plus_experimental_current.txt
@@ -73,7 +73,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/api/restricted_current.txt b/activity/activity/api/restricted_current.txt
index cf252d3..45422eb 100644
--- a/activity/activity/api/restricted_current.txt
+++ b/activity/activity/api/restricted_current.txt
@@ -72,7 +72,7 @@
   }
 
   public abstract class OnBackPressedCallback {
-    ctor public OnBackPressedCallback(boolean isEnabled);
+    ctor public OnBackPressedCallback(boolean enabled);
     method @MainThread public abstract void handleOnBackPressed();
     method @MainThread public final boolean isEnabled();
     method @MainThread public final void remove();
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index 27fd54e..bbadf0f 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -22,10 +22,10 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.core:core:1.8.0")
-    api("androidx.lifecycle:lifecycle-runtime:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+    api(project(":lifecycle:lifecycle-runtime"))
+    api(project(":lifecycle:lifecycle-viewmodel"))
     api("androidx.savedstate:savedstate:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation("androidx.tracing:tracing:1.0.0")
     api(libs.kotlinStdlib)
 
diff --git a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
index 67efa4b..7174a2a 100644
--- a/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
+++ b/activity/activity/src/main/java/androidx/activity/OnBackPressedCallback.kt
@@ -37,11 +37,11 @@
  * [OnBackPressedDispatcher] it has been added to. It is strongly recommended
  * to instead disable this callback to handle temporary changes in state.
  *
- * @param isEnabled The default enabled state for this callback.
+ * @param enabled The default enabled state for this callback.
  *
  * @see ComponentActivity.getOnBackPressedDispatcher
  */
-abstract class OnBackPressedCallback(isEnabled: Boolean) {
+abstract class OnBackPressedCallback(enabled: Boolean) {
     /**
      * The enabled state of the callback. Only when this callback
      * is enabled will it receive callbacks to [handleOnBackPressed].
@@ -54,7 +54,7 @@
     @get:MainThread
     @set:MainThread
     @set:OptIn(markerClass = [BuildCompat.PrereleaseSdkCheck::class])
-    var isEnabled: Boolean = isEnabled
+    var isEnabled: Boolean = enabled
         set(value) {
             field = value
             if (enabledConsumer != null) {
diff --git a/appactions/interaction/OWNERS b/appactions/interaction/OWNERS
new file mode 100644
index 0000000..18b6f56
--- /dev/null
+++ b/appactions/interaction/OWNERS
@@ -0,0 +1,3 @@
+aliibrahim@google.com
+jaazm@google.com
+mkucharski@google.com
\ No newline at end of file
diff --git a/appactions/interaction/interaction-proto/api/current.txt b/appactions/interaction/interaction-proto/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-proto/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-proto/api/public_plus_experimental_current.txt b/appactions/interaction/interaction-proto/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-proto/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-proto/api/res-current.txt b/appactions/interaction/interaction-proto/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/appactions/interaction/interaction-proto/api/res-current.txt
diff --git a/appactions/interaction/interaction-proto/api/restricted_current.txt b/appactions/interaction/interaction-proto/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/appactions/interaction/interaction-proto/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/appactions/interaction/interaction-proto/build.gradle b/appactions/interaction/interaction-proto/build.gradle
new file mode 100644
index 0000000..b7979b6
--- /dev/null
+++ b/appactions/interaction/interaction-proto/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
+import androidx.build.RunApiTasks
+
+plugins {
+    id("AndroidXPlugin")
+    id("android-library")
+    id("com.google.protobuf")
+}
+
+dependencies {
+    implementation(libs.protobufLite)
+}
+
+android {
+    namespace "androidx.appactions.interaction.proto"
+}
+
+androidx {
+    name = "androidx.appactions.interaction:interaction-proto"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.APPACTIONS_INTERACTION
+    inceptionYear = "2022"
+    description = "Protos for use with App Action interaction libraries."
+}
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
index b2c2950..88b1592 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/widget/AppCompatEditTextTest.java
@@ -53,6 +53,7 @@
 import androidx.test.rule.ActivityTestRule;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -303,6 +304,7 @@
                 TextViewCompat.getCompoundDrawableTintList(textView));
     }
 
+    @Ignore // b/259134876
     @Test
     public void testCompoundDrawablesTintList() {
         // Given an ACTV with a white drawableLeftCompat and a ColorStateList drawableTint set
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
index c7bc830..375f3519 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AlwaysSupportedFeatures.java
@@ -46,6 +46,9 @@
         if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
             return true;
         }
+        if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
+            return true;
+        }
         return false;
     }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
index 7ffb7e9..bc46326 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/stats/SearchStats.java
@@ -72,7 +72,7 @@
     private final int mRewriteSearchSpecLatencyMillis;
     /** Time used to rewrite the search results. */
     private final int mRewriteSearchResultLatencyMillis;
-    /** Time passed while waiting to acquire the lock during Java function calls. **/
+    /** Time passed while waiting to acquire the lock during Java function calls. */
     private final int mJavaLockAcquisitionLatencyMillis;
     /**
      * Time spent on ACL checking. This is the time spent filtering namespaces based on package
@@ -200,7 +200,7 @@
         return mRewriteSearchResultLatencyMillis;
     }
 
-    /** Returns time passed while waiting to acquire the lock during Java function calls **/
+    /** Returns time passed while waiting to acquire the lock during Java function calls */
     public int getJavaLockAcquisitionLatencyMillis() {
         return mJavaLockAcquisitionLatencyMillis;
     }
diff --git a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
index 336b213..47fad2c 100644
--- a/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
+++ b/appsearch/appsearch-platform-storage/src/main/java/androidx/appsearch/platformstorage/FeaturesImpl.java
@@ -44,6 +44,11 @@
         if (Features.ADD_PERMISSIONS_AND_GET_VISIBILITY.equals(feature)) {
             return BuildCompat.isAtLeastT();
         }
+        // TODO: Update to reflect support in Android U+ once this feature is synced over into
+        //  service-appsearch
+        if (Features.TOKENIZER_TYPE_RFC822.equals(feature)) {
+            return false;
+        }
         return false;
     }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -165,6 +165,7 @@
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
     field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
   }
 
   public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
   public class GenericDocument {
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -165,6 +165,7 @@
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
     field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
   }
 
   public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
   public class GenericDocument {
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 8580d6f..935fa28 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -165,6 +165,7 @@
     field public static final int INDEXING_TYPE_PREFIXES = 2; // 0x2
     field public static final int TOKENIZER_TYPE_NONE = 0; // 0x0
     field public static final int TOKENIZER_TYPE_PLAIN = 1; // 0x1
+    field @RequiresFeature(enforcement="androidx.appsearch.app.Features#isFeatureSupported", name=androidx.appsearch.app.Features.TOKENIZER_TYPE_RFC822) public static final int TOKENIZER_TYPE_RFC822 = 3; // 0x3
   }
 
   public static final class AppSearchSchema.StringPropertyConfig.Builder {
@@ -205,6 +206,7 @@
     field public static final String GLOBAL_SEARCH_SESSION_GET_SCHEMA = "GLOBAL_SEARCH_SESSION_GET_SCHEMA";
     field public static final String GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK = "GLOBAL_SEARCH_SESSION_REGISTER_OBSERVER_CALLBACK";
     field public static final String SEARCH_RESULT_MATCH_INFO_SUBMATCH = "SEARCH_RESULT_MATCH_INFO_SUBMATCH";
+    field public static final String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
   }
 
   public class GenericDocument {
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index c9e4a8b..c82c623 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -22,6 +22,9 @@
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled true
+    }
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
@@ -48,6 +51,7 @@
     // This dependency is unused by the test implementation, but it's here to validate that
     // icing's jarjar'ing of the Protobuf_lite doesn't conflict with external dependencies.
     androidTestImplementation(libs.protobufLite)
+    androidTestImplementation(libs.multidex)
 }
 
 androidx {
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index ddb7093..00ad136 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+// @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
 import android.content.Context;
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
index 1417929..d08a89d 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSchemaCtsTest.java
@@ -368,4 +368,14 @@
 
         assertThat(schemaString).isEqualTo(expectedString);
     }
+
+    @Test
+    public void testStringPropertyConfig_setTokenizerType() {
+        assertThrows(IllegalArgumentException.class, () ->
+                new StringPropertyConfig.Builder("subject").setTokenizerType(5).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new StringPropertyConfig.Builder("subject").setTokenizerType(2).build());
+        assertThrows(IllegalArgumentException.class, () ->
+                new StringPropertyConfig.Builder("subject").setTokenizerType(-1).build());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
index bc1e02c..751ee86 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/AppSearchSessionCtsTestBase.java
@@ -3406,4 +3406,54 @@
             assertThat(matches.get(0).getSubmatch()).isEqualTo("🐟");
         }
     }
+
+    @Test
+    public void testRfc822() throws Exception {
+        assumeTrue(mDb1.getFeatures().isFeatureSupported(Features.TOKENIZER_TYPE_RFC822));
+        AppSearchSchema emailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("address")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+                        .build()
+                ).build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder()
+                .setForceOverride(true).addSchemas(emailSchema).build()).get();
+
+        GenericDocument email = new GenericDocument.Builder<>("NS", "alex1", "Email")
+                .setPropertyString("address", "Alex Saveliev <alex.sav@google.com>")
+                .build();
+        mDb1.putAsync(new PutDocumentsRequest.Builder().addGenericDocuments(email).build()).get();
+
+        SearchResults sr = mDb1.search("com", new SearchSpec.Builder().build());
+        List<SearchResult> page = sr.getNextPageAsync().get();
+
+        // RFC tokenization will produce the following tokens for
+        // "Alex Saveliev <alex.sav@google.com>" : ["Alex Saveliev <alex.sav@google.com>", "Alex",
+        // "Saveliev", "alex.sav", "alex.sav@google.com", "alex.sav", "google", "com"]. Therefore,
+        // a query for "com" should match the document.
+        assertThat(page).hasSize(1);
+        assertThat(page.get(0).getGenericDocument().getId()).isEqualTo("alex1");
+
+        // Plain tokenizer will not match this
+        AppSearchSchema plainEmailSchema = new AppSearchSchema.Builder("Email")
+                .addProperty(new StringPropertyConfig.Builder("address")
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        // Flipping the tokenizer type is a backwards compatible change. The index will be
+        // rebuilt with the email doc being tokenized in the new way.
+        mDb1.setSchemaAsync(
+                new SetSchemaRequest.Builder().addSchemas(plainEmailSchema).build()).get();
+
+        sr = mDb1.search("com", new SearchSpec.Builder().build());
+
+        // Plain tokenization will produce the following tokens for
+        // "Alex Saveliev <alex.sav@google.com>" : ["Alex", "Saveliev", "<", "alex.sav",
+        // "google.com", ">"]. So "com" will not match any of the tokens produced.
+        assertThat(sr.getNextPageAsync().get()).hasSize(0);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
index dd47914..2086796 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SetSchemaRequestCtsTest.java
@@ -41,6 +41,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -780,4 +781,30 @@
                         ImmutableSet.of(SetSchemaRequest.READ_SMS, SetSchemaRequest.READ_CALENDAR),
                         ImmutableSet.of(SetSchemaRequest.READ_HOME_APP_SEARCH_DATA)));
     }
+
+    @Test
+    public void testRfc822TokenizerType() {
+        AppSearchSchema.StringPropertyConfig prop1 =
+                new AppSearchSchema.StringPropertyConfig.Builder("prop1")
+                        .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(
+                                AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(
+                                AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+                        .build();
+        AppSearchSchema schema1 =
+                new AppSearchSchema.Builder("type1").addProperty(prop1).build();
+
+        SetSchemaRequest request = new SetSchemaRequest.Builder()
+                .addSchemas(schema1)
+                .setForceOverride(true)
+                .setVersion(142857)
+                .build();
+        AppSearchSchema[] schemas = request.getSchemas().toArray(new AppSearchSchema[0]);
+        assertThat(schemas).hasLength(1);
+        List<AppSearchSchema.PropertyConfig> properties = schemas[0].getProperties();
+        assertThat(properties).hasSize(1);
+        assertThat(((AppSearchSchema.StringPropertyConfig) properties.get(0)).getTokenizerType())
+                .isEqualTo(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822);
+    }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 1a19c65..cce0237 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -21,6 +21,7 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RequiresFeature;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.IllegalSchemaException;
 import androidx.appsearch.util.BundleUtil;
@@ -482,6 +483,7 @@
         @IntDef(value = {
                 TOKENIZER_TYPE_NONE,
                 TOKENIZER_TYPE_PLAIN,
+                TOKENIZER_TYPE_RFC822
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface TokenizerType {}
@@ -507,6 +509,25 @@
          */
         public static final int TOKENIZER_TYPE_PLAIN = 1;
 
+        // TODO(b/204333391): In icing, the "2" tokenizer is the verbatim tokenizer.
+
+        /**
+         * Tokenization for emails. This value indicates that tokens should be extracted from
+         * this property based on email structure.
+         *
+         * <p>Ex. A property with "alex.sav@google.com" will produce tokens for "alex", "sav",
+         * "alex.sav", "google", "com", and "alexsav@google.com"
+         *
+         * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is
+         * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
+         */
+// @exportToFramework:startStrip()
+        @RequiresFeature(
+                enforcement = "androidx.appsearch.app.Features#isFeatureSupported",
+                name = Features.TOKENIZER_TYPE_RFC822)
+// @exportToFramework:endStrip()
+        public static final int TOKENIZER_TYPE_RFC822 = 3;
+
         StringPropertyConfig(@NonNull Bundle bundle) {
             super(bundle);
         }
@@ -576,8 +597,13 @@
              */
             @NonNull
             public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
-                Preconditions.checkArgumentInRange(
-                        tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
+                // TODO(b/204333391): Change to checkArgumentInRange once verbatim is supported
+                if (tokenizerType != TOKENIZER_TYPE_NONE && tokenizerType != TOKENIZER_TYPE_PLAIN
+                        && tokenizerType != TOKENIZER_TYPE_RFC822) {
+                    throw new IllegalArgumentException("Tokenizer value " + tokenizerType + " is "
+                            + "out of range. Valid values are TOKENIZER_TYPE_NONE, "
+                            + "TOKENIZER_TYPE_PLAIN, and TOKENIZER_TYPE_RFC822");
+                }
                 mTokenizerType = tokenizerType;
                 return this;
             }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
index dc530ee..5edc780 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/Features.java
@@ -69,6 +69,12 @@
     String ADD_PERMISSIONS_AND_GET_VISIBILITY = "ADD_PERMISSIONS_AND_GET_VISIBILITY";
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}. This feature covers
+     * {@link AppSearchSchema.StringPropertyConfig#TOKENIZER_TYPE_RFC822}.
+     */
+    String TOKENIZER_TYPE_RFC822 = "TOKENIZER_TYPE_RFC822";
+
+    /**
      * Returns whether a feature is supported at run-time. Feature support depends on the
      * feature in question, the AppSearch backend being used and the Android version of the
      * device.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
index 07e709a..7dae734 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -293,6 +293,8 @@
      *
      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
      * by this function, rather than calling it multiple times.
+     *
+     * @return A mapping of schema types to lists of projection strings.
      */
     @NonNull
     public Map<String, List<String>> getProjections() {
@@ -314,6 +316,8 @@
      *
      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
      * by this function, rather than calling it multiple times.
+     *
+     * @return A mapping of schema types to lists of projection {@link PropertyPath} objects.
      */
     @NonNull
     public Map<String, List<PropertyPath>> getProjectionPaths() {
@@ -624,6 +628,9 @@
          * it will be ignored for that result. Property paths cannot be null.
          *
          * @see #addProjectionPaths
+         *
+         * @param schema a string corresponding to the schema to add projections to.
+         * @param propertyPaths the projections to add.
          */
         @NonNull
         public SearchSpec.Builder addProjection(
@@ -699,6 +706,9 @@
          *   subject: "IMPORTANT"
          * }
          * }</pre>
+         *
+         * @param schema a string corresponding to the schema to add projections to.
+         * @param propertyPaths the projections to add.
          */
         @NonNull
         public SearchSpec.Builder addProjectionPaths(
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
index 96b0f88..c23a469 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/SchemaCodeGenerator.java
@@ -197,6 +197,9 @@
             } else if (tokenizerType == 1) {  // TOKENIZER_TYPE_PLAIN
                 tokenizerEnum = mHelper.getAppSearchClass(
                         "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_PLAIN");
+            } else if (tokenizerType == 3) { // TOKENIZER_TYPE_RFC822
+                tokenizerEnum = mHelper.getAppSearchClass(
+                        "AppSearchSchema", "StringPropertyConfig", "TOKENIZER_TYPE_RFC822");
             } else {
                 throw new ProcessingException("Unknown tokenizer type " + tokenizerType, property);
             }
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index 1339501..c461fc3 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -937,8 +937,30 @@
                         + "public class Gift {\n"
                         + "  @Document.Namespace String namespace;\n"
                         + "  @Document.Id String id;\n"
-                        + "  @Document.StringProperty(tokenizerType=0) String tokNone;\n"
-                        + "  @Document.StringProperty(tokenizerType=1) String tokPlain;\n"
+                        + "\n"
+                        // NONE index type will generate a NONE tokenizerType type.
+                        + "  @Document.StringProperty(tokenizerType=0, indexingType=0) "
+                        + "  String tokNoneInvalid;\n"
+                        + "  @Document.StringProperty(tokenizerType=1, indexingType=0) "
+                        + "  String tokPlainInvalid;\n"
+                        + "  @Document.StringProperty(tokenizerType=3, indexingType=0) "
+                        + "  String tokRfc822Invalid;\n"
+                        + "\n"
+                        // Indexing type exact.
+                        + "  @Document.StringProperty(tokenizerType=0, indexingType=1) "
+                        + "  String tokNone;\n"
+                        + "  @Document.StringProperty(tokenizerType=1, indexingType=1) "
+                        + "  String tokPlain;\n"
+                        + "  @Document.StringProperty(tokenizerType=3, indexingType=1) "
+                        + "  String tokRfc822;\n"
+                        + "\n"
+                        // Indexing type prefix.
+                        + "  @Document.StringProperty(tokenizerType=0, indexingType=2) "
+                        + "  String tokNonePrefix;\n"
+                        + "  @Document.StringProperty(tokenizerType=1, indexingType=2) "
+                        + "  String tokPlainPrefix;\n"
+                        + "  @Document.StringProperty(tokenizerType=3, indexingType=2) "
+                        + "  String tokRfc822Prefix;\n"
                         + "}\n");
 
         assertThat(compilation).succeededWithoutWarnings();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
index b6ccd81..0fa23d4 100644
--- a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testTokenizerType.JAVA
@@ -20,16 +20,51 @@
   @Override
   public AppSearchSchema getSchema() throws AppSearchException {
     return new AppSearchSchema.Builder(SCHEMA_NAME)
-          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNoneInvalid")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
             .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
-          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlainInvalid")
             .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
             .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
             .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
             .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822Invalid")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNone")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlain")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokNonePrefix")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokPlainPrefix")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+            .build())
+          .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("tokRfc822Prefix")
+            .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+            .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822)
+            .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+            .build())
           .build();
   }
 
@@ -37,6 +72,18 @@
   public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
     GenericDocument.Builder<?> builder =
         new GenericDocument.Builder<>(document.namespace, document.id, SCHEMA_NAME);
+    String tokNoneInvalidCopy = document.tokNoneInvalid;
+    if (tokNoneInvalidCopy != null) {
+      builder.setPropertyString("tokNoneInvalid", tokNoneInvalidCopy);
+    }
+    String tokPlainInvalidCopy = document.tokPlainInvalid;
+    if (tokPlainInvalidCopy != null) {
+      builder.setPropertyString("tokPlainInvalid", tokPlainInvalidCopy);
+    }
+    String tokRfc822InvalidCopy = document.tokRfc822Invalid;
+    if (tokRfc822InvalidCopy != null) {
+      builder.setPropertyString("tokRfc822Invalid", tokRfc822InvalidCopy);
+    }
     String tokNoneCopy = document.tokNone;
     if (tokNoneCopy != null) {
       builder.setPropertyString("tokNone", tokNoneCopy);
@@ -45,6 +92,22 @@
     if (tokPlainCopy != null) {
       builder.setPropertyString("tokPlain", tokPlainCopy);
     }
+    String tokRfc822Copy = document.tokRfc822;
+    if (tokRfc822Copy != null) {
+      builder.setPropertyString("tokRfc822", tokRfc822Copy);
+    }
+    String tokNonePrefixCopy = document.tokNonePrefix;
+    if (tokNonePrefixCopy != null) {
+      builder.setPropertyString("tokNonePrefix", tokNonePrefixCopy);
+    }
+    String tokPlainPrefixCopy = document.tokPlainPrefix;
+    if (tokPlainPrefixCopy != null) {
+      builder.setPropertyString("tokPlainPrefix", tokPlainPrefixCopy);
+    }
+    String tokRfc822PrefixCopy = document.tokRfc822Prefix;
+    if (tokRfc822PrefixCopy != null) {
+      builder.setPropertyString("tokRfc822Prefix", tokRfc822PrefixCopy);
+    }
     return builder.build();
   }
 
@@ -52,6 +115,21 @@
   public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
     String idConv = genericDoc.getId();
     String namespaceConv = genericDoc.getNamespace();
+    String[] tokNoneInvalidCopy = genericDoc.getPropertyStringArray("tokNoneInvalid");
+    String tokNoneInvalidConv = null;
+    if (tokNoneInvalidCopy != null && tokNoneInvalidCopy.length != 0) {
+      tokNoneInvalidConv = tokNoneInvalidCopy[0];
+    }
+    String[] tokPlainInvalidCopy = genericDoc.getPropertyStringArray("tokPlainInvalid");
+    String tokPlainInvalidConv = null;
+    if (tokPlainInvalidCopy != null && tokPlainInvalidCopy.length != 0) {
+      tokPlainInvalidConv = tokPlainInvalidCopy[0];
+    }
+    String[] tokRfc822InvalidCopy = genericDoc.getPropertyStringArray("tokRfc822Invalid");
+    String tokRfc822InvalidConv = null;
+    if (tokRfc822InvalidCopy != null && tokRfc822InvalidCopy.length != 0) {
+      tokRfc822InvalidConv = tokRfc822InvalidCopy[0];
+    }
     String[] tokNoneCopy = genericDoc.getPropertyStringArray("tokNone");
     String tokNoneConv = null;
     if (tokNoneCopy != null && tokNoneCopy.length != 0) {
@@ -62,11 +140,38 @@
     if (tokPlainCopy != null && tokPlainCopy.length != 0) {
       tokPlainConv = tokPlainCopy[0];
     }
+    String[] tokRfc822Copy = genericDoc.getPropertyStringArray("tokRfc822");
+    String tokRfc822Conv = null;
+    if (tokRfc822Copy != null && tokRfc822Copy.length != 0) {
+      tokRfc822Conv = tokRfc822Copy[0];
+    }
+    String[] tokNonePrefixCopy = genericDoc.getPropertyStringArray("tokNonePrefix");
+    String tokNonePrefixConv = null;
+    if (tokNonePrefixCopy != null && tokNonePrefixCopy.length != 0) {
+      tokNonePrefixConv = tokNonePrefixCopy[0];
+    }
+    String[] tokPlainPrefixCopy = genericDoc.getPropertyStringArray("tokPlainPrefix");
+    String tokPlainPrefixConv = null;
+    if (tokPlainPrefixCopy != null && tokPlainPrefixCopy.length != 0) {
+      tokPlainPrefixConv = tokPlainPrefixCopy[0];
+    }
+    String[] tokRfc822PrefixCopy = genericDoc.getPropertyStringArray("tokRfc822Prefix");
+    String tokRfc822PrefixConv = null;
+    if (tokRfc822PrefixCopy != null && tokRfc822PrefixCopy.length != 0) {
+      tokRfc822PrefixConv = tokRfc822PrefixCopy[0];
+    }
     Gift document = new Gift();
     document.namespace = namespaceConv;
     document.id = idConv;
+    document.tokNoneInvalid = tokNoneInvalidConv;
+    document.tokPlainInvalid = tokPlainInvalidConv;
+    document.tokRfc822Invalid = tokRfc822InvalidConv;
     document.tokNone = tokNoneConv;
     document.tokPlain = tokPlainConv;
+    document.tokRfc822 = tokRfc822Conv;
+    document.tokNonePrefix = tokNonePrefixConv;
+    document.tokPlainPrefix = tokPlainPrefixConv;
+    document.tokRfc822Prefix = tokRfc822PrefixConv;
     return document;
   }
 }
diff --git a/benchmark/OWNERS b/benchmark/OWNERS
index 171c5cc..eeaf357 100644
--- a/benchmark/OWNERS
+++ b/benchmark/OWNERS
@@ -1,3 +1,4 @@
 ccraik@google.com
 dustinlam@google.com
+jgielzak@google.com
 rahulrav@google.com
diff --git a/benchmark/benchmark-common/api/public_plus_experimental_current.txt b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
index 54c1187..07ca9f6 100644
--- a/benchmark/benchmark-common/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
@@ -36,7 +36,7 @@
   public final class ConfigurationErrorKt {
   }
 
-  @kotlin.RequiresOptIn public @interface ExperimentalBenchmarkStateApi {
+  @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalBenchmarkStateApi {
   }
 
   public final class MetricNameUtilsKt {
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
index bd01556..493611c 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
@@ -62,12 +62,16 @@
     @Test
     fun pidof() {
         // Should only be one process - this one!
-        val pidofString = Shell.executeScriptCaptureStdout("pidof ${Packages.TEST}").trim()
+        val output = Shell.executeScriptCaptureStdoutStderr("pidof ${Packages.TEST}")
+        val pidofString = output.stdout.trim()
 
         when {
             Build.VERSION.SDK_INT < 23 -> {
-                // command doesn't exist (and we don't try and read stderr here)
-                assertEquals("", pidofString)
+                // command doesn't exist
+                assertTrue(
+                    output.stdout.isBlank() && output.stderr.isNotBlank(),
+                    "saw output $output"
+                )
             }
             Build.VERSION.SDK_INT == 23 -> {
                 // on API 23 specifically, pidof prints... all processes, ignoring the arg...
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
index c23a119..576a03b 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
@@ -68,10 +68,7 @@
         val output = Shell.optionalCommand("echo foo")
 
         val expected = when {
-            Build.VERSION.SDK_INT >= 23 -> "foo\n"
-            // known bug in the shell on L (21,22). `echo` doesn't work with shell
-            // programmatically, only works in interactive shell :|
-            Build.VERSION.SDK_INT in 21..22 -> ""
+            Build.VERSION.SDK_INT >= 21 -> "foo\n"
             else -> null
         }
 
@@ -153,7 +150,7 @@
         )
     }
 
-    @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+    @SdkSuppress(minSdkVersion = 23) // xargs added api 23
     @Test
     fun executeScriptCaptureStdout_pipe_xargs() {
         // validate piping works with xargs
@@ -170,7 +167,7 @@
         )
     }
 
-    @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+    @SdkSuppress(minSdkVersion = 23) // xargs added api 23
     @Test
     fun executeScriptCaptureStdout_stdinArg_xargs() {
         // validate stdin to first command in script
@@ -204,7 +201,7 @@
         )
     }
 
-    @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
+    @SdkSuppress(minSdkVersion = 23) // xargs added api 23
     @Test
     fun executeScriptCaptureStdout_multilineRedirectStdin_xargs() {
         Assert.assertEquals(
@@ -383,7 +380,7 @@
         }
     }
 
-    @SdkSuppress(minSdkVersion = 21)
+    @SdkSuppress(minSdkVersion = 23) // xargs added api 23
     @Test
     fun shellReuse() {
         val script = Shell.createShellScript("xargs echo $1", stdin = "foo")
@@ -397,6 +394,32 @@
         script.cleanUp()
     }
 
+    @SdkSuppress(minSdkVersion = 21)
+    @Test
+    fun getChecksum() {
+        val emptyPaths = listOf("/data/local/tmp/emptyfile1", "/data/local/tmp/emptyfile2")
+        try {
+            val checksums = emptyPaths.map {
+                Shell.executeScriptSilent("rm -f $it")
+                Shell.executeScriptSilent("touch $it")
+                Shell.getChecksum(it)
+            }
+
+            assertEquals(checksums.first(), checksums.last())
+            if (Build.VERSION.SDK_INT < 23) {
+                checksums.forEach { checksum ->
+                    // getChecksum uses ls -l to check size pre API 23,
+                    // this validates that behavior + result parsing
+                    assertEquals("0", checksum)
+                }
+            }
+        } finally {
+            emptyPaths.forEach {
+                Shell.executeScriptSilent("rm -f $it")
+            }
+        }
+    }
+
     @RequiresApi(21)
     private fun pidof(packageName: String): Int? {
         return Shell.getPidsForProcess(packageName).firstOrNull()
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
index 283a472..33eca57 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/ExperimentalBenchmarkStateApi.kt
@@ -21,4 +21,5 @@
  * of the BenchmarkRule JUnit4 API.
  */
 @RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
 public annotation class ExperimentalBenchmarkStateApi
\ No newline at end of file
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
index 86d304e..680dd62 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -107,27 +107,63 @@
             }
     }
 
+    /**
+     * Get a checksum for a given path
+     *
+     * Note: Does not check for stderr, as this method is used during ShellImpl init, so stderr not
+     * yet available
+     */
     @RequiresApi(21)
-    fun chmodExecutable(absoluteFilePath: String) {
-        // use unsafe commands, as this is used in Shell.executeScript
+    internal fun getChecksum(path: String): String {
+        val sum = if (Build.VERSION.SDK_INT >= 23) {
+            ShellImpl.executeCommandUnsafe("md5sum $path").substringBefore(" ")
+        } else {
+            // this isn't good, but it's good enough for API 22
+            val out = ShellImpl.executeCommandUnsafe("ls -l $path").split(Regex("\\s+"))[3]
+            println("value is $out")
+            out
+        }
+        check(sum.isNotBlank()) {
+            "Checksum for $path was blank"
+        }
+        return sum
+    }
+
+    /**
+     * Copy file and make executable
+     *
+     * Note: this operation does checksum validation of dst, since it's used during setup of the
+     * shell script used to capture stderr, so stderr isn't available.
+     */
+    @RequiresApi(21)
+    private fun moveToTmpAndMakeExecutable(src: String, dst: String) {
+        ShellImpl.executeCommandUnsafe("cp $src $dst")
         if (Build.VERSION.SDK_INT >= 23) {
-            ShellImpl.executeCommandUnsafe("chmod +x $absoluteFilePath")
+            ShellImpl.executeCommandUnsafe("chmod +x $dst")
         } else {
             // chmod with support for +x only added in API 23
             // While 777 is technically more permissive, this is only used for scripts and temporary
             // files in tests, so we don't worry about permissions / access here
-            ShellImpl.executeCommandUnsafe("chmod 777 $absoluteFilePath")
+            ShellImpl.executeCommandUnsafe("chmod 777 $dst")
         }
-    }
 
-    @RequiresApi(21)
-    fun moveToTmpAndMakeExecutable(src: String, dst: String) {
-        ShellImpl.executeCommandUnsafe("cp $src $dst")
-        chmodExecutable(dst)
+        // validate checksums instead of checking stderr, since it's not yet safe to
+        // read from stderr. This detects the problem where root left a stale executable
+        // that can't be modified by shell at the dst path
+        val srcSum = getChecksum(src)
+        val dstSum = getChecksum(dst)
+        if (srcSum != dstSum) {
+            throw IllegalStateException("Failed to verify copied executable $dst, " +
+                "md5 sums $srcSum, $dstSum don't match. Check if root owns" +
+                " $dst and if so, delete it with `adb root`-ed shell session.")
+        }
     }
 
     /**
      * Writes the inputStream to an executable file with the given name in `/data/local/tmp`
+     *
+     * Note: this operation does not validate command success, since it's used during setup of shell
+     * scripting code used to parse stderr. This means callers should validate.
      */
     @RequiresApi(21)
     fun createRunnableExecutable(name: String, inputStream: InputStream): String {
@@ -467,7 +503,8 @@
         // These variables are used in executeCommand and executeScript, so we keep them as var
         // instead of val and use a separate initializer
         isSessionRooted = executeCommandUnsafe("id").contains("uid=0(root)")
-        // use a script below, since `su` command failure is unrecoverable on some API levels
+        // use a script below, since direct `su` command failure brings down this process
+        // on some API levels (and can fail even on userdebug builds)
         isSuAvailable = createShellScript(
             script = "su root id",
             stdin = null
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
index 3e74e58..01381a9 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoHelper.kt
@@ -26,9 +26,9 @@
 import androidx.benchmark.userspaceTrace
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tracing.trace
-import org.jetbrains.annotations.TestOnly
 import java.io.File
 import java.io.IOException
+import org.jetbrains.annotations.TestOnly
 
 /**
  * PerfettoHelper is used to start and stop the perfetto tracing and move the
@@ -97,7 +97,19 @@
                 val path = "$UNBUNDLED_PERFETTO_ROOT_DIR/config.pb"
                 // Move the config to a directory that unbundled perfetto has permissions for.
                 Shell.executeScriptSilent("rm -f $path")
-                Shell.executeScriptSilent("mv $configFilePath $path")
+                if (Build.VERSION.SDK_INT >= 24) {
+                    Shell.executeScriptSilent("mv $configFilePath $path")
+                } else {
+                    // Observed stderr output (though command still completes successfully) on:
+                    // google/shamu/shamu:6.0.1/MOB31T/3671974:userdebug/dev-keys
+                    // Doesn't repro on all API 23 devices :|
+                    Shell.executeScriptCaptureStdoutStderr("mv $configFilePath $path").also {
+                        check(
+                            it.stdout.isBlank() &&
+                                (it.stderr.isBlank() || it.stderr.startsWith("mv: chown"))
+                        )
+                    }
+                }
                 path
             } else {
                 configFilePath
@@ -110,8 +122,10 @@
             // Perfetto
             val perfettoCmd = perfettoCommand(actualConfigPath, isTextProtoConfig)
             Log.i(LOG_TAG, "Starting perfetto tracing with cmd: $perfettoCmd")
-            val perfettoCmdOutput =
-                Shell.executeScriptCaptureStdout("$perfettoCmd; echo EXITCODE=$?").trim()
+            // Note: we intentionally don't check stderr, as benign warnings are printed
+            val perfettoCmdOutput = Shell.executeScriptCaptureStdoutStderr(
+                "$perfettoCmd; echo EXITCODE=$?"
+            ).stdout.trim()
 
             val expectedSuffix = "\nEXITCODE=0"
             if (!perfettoCmdOutput.endsWith(expectedSuffix)) {
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
index bcd94d2..c9949cc 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/UiState.kt
@@ -16,16 +16,19 @@
 
 package androidx.benchmark.perfetto
 
+import android.os.Build
+import android.util.Log
 import androidx.annotation.RestrictTo
+import androidx.benchmark.BenchmarkState.Companion.TAG
+import java.io.File
+import java.io.FileNotFoundException
 import perfetto.protos.Trace
 import perfetto.protos.TracePacket
 import perfetto.protos.UiState
-import java.io.File
 
 /**
  * Convenience for UiState construction with specified package
  */
-@Suppress("FunctionName") // constructor convenience
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 fun UiState(
     timelineStart: Long?,
@@ -42,5 +45,19 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 fun File.appendUiState(state: UiState) {
     val traceToAppend = Trace(packet = listOf(TracePacket(ui_state = state)))
-    appendBytes(traceToAppend.encode())
+    appendBytesSafely(traceToAppend.encode())
+}
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun File.appendBytesSafely(bytes: ByteArray) {
+    try {
+        appendBytes(bytes)
+    } catch (e: FileNotFoundException) {
+        if (Build.VERSION.SDK_INT in 21..22) {
+            // Failure is common on API 21/22 due to b/227510293
+            Log.d(TAG, "Unable to append additional bytes to ${this.absolutePath}")
+        } else {
+            throw e
+        }
+    }
 }
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 a016541..936c42f 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
@@ -76,6 +76,9 @@
             project.layout.buildDirectory.dir("$name.xcresult")
         }
 
+        // Configure the XCode Build Service so we don't run too many benchmarks at the same time.
+        project.configureXCodeBuildService()
+
         val fetchXCodeGenTask = project.tasks.register(
             FETCH_XCODEGEN_TASK, FetchXCodeGenTask::class.java
         ) {
@@ -90,12 +93,18 @@
             it.yamlFile.set(extension.xcodeGenConfigFile)
             it.projectName.set(extension.xcodeProjectName)
             it.xcProjectPath.set(xcodeProjectPath)
-            it.infoPlistPath.set(project.layout.buildDirectory.file("Info.plist"))
         }
 
         val runDarwinBenchmarks = project.tasks.register(
             RUN_DARWIN_BENCHMARKS_TASK, RunDarwinBenchmarksTask::class.java
         ) {
+            val sharedService =
+                project
+                    .gradle
+                    .sharedServices
+                    .registrations.getByName(XCodeBuildService.XCODE_BUILD_SERVICE_NAME)
+                    .service
+            it.usesService(sharedService)
             it.xcodeProjectPath.set(generateXCodeProjectTask.flatMap { task ->
                 task.xcProjectPath
             })
@@ -152,6 +161,7 @@
         const val DIST_DIR = "DIST_DIR"
         const val LIBRARY_METRICS = "librarymetrics"
         const val DARWIN_BENCHMARKS_DIR = "darwinBenchmarks"
+
         // Gradle Properties
         const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri"
 
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
index 7d4136d..dfca8e9 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkResultsTask.kt
@@ -17,7 +17,7 @@
 package androidx.benchmark.darwin.gradle
 
 import androidx.benchmark.darwin.gradle.skia.Metrics
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import androidx.benchmark.darwin.gradle.xcode.XcResultParser
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -67,7 +67,7 @@
         }
         val (record, summaries) = parser.parseResults()
         val metrics = Metrics.buildMetrics(record, summaries)
-        val output = Models.gsonBuilder()
+        val output = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
             .toJson(metrics)
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
index 065e7ff..b07f5235 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
@@ -26,7 +26,6 @@
 import org.gradle.api.tasks.Input
 import org.gradle.api.tasks.InputFile
 import org.gradle.api.tasks.OutputDirectory
-import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.TaskAction
@@ -48,9 +47,6 @@
     @get:Input
     abstract val projectName: Property<String>
 
-    @get:OutputFile
-    abstract val infoPlistPath: RegularFileProperty
-
     @get:OutputDirectory
     abstract val xcProjectPath: DirectoryProperty
 
@@ -78,12 +74,14 @@
     }
 
     private fun copyProjectMetadata() {
-        val sourceFile = File(yamlFile.get().asFile.parent, "Info.plist")
-        require(sourceFile.exists())
-        val targetFile = infoPlistPath.get().asFile
-        val copied = sourceFile.copyRecursively(targetFile, overwrite = true)
-        require(copied) {
-            "Unable to copy $sourceFile to $targetFile"
+        // Copy generated `App.plist` and `Benchmark.plist` files.
+        // Context: b/258545725
+        val metadataFileNames = listOf("App.plist", "Benchmark.plist")
+        metadataFileNames.forEach { name ->
+            val sourceFile = File(yamlFile.get().asFile.parent, name)
+            require(sourceFile.exists())
+            val targetFile = File(xcProjectPath.get().asFile.parent, name)
+            sourceFile.copyRecursively(targetFile, overwrite = true)
         }
     }
 }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
index d33dd6a..b9ebca2 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/RunDarwinBenchmarksTask.kt
@@ -49,21 +49,26 @@
     @TaskAction
     fun runBenchmarks() {
         requireXcodeBuild()
+        // Consider moving this into the shared instance of XCodeBuildService
+        // given that is a much cleaner way of sharing a single instance of a running simulator.
+        val simCtrl = XCodeSimCtrl(execOperations, destination.get())
         val xcodeProject = xcodeProjectPath.get().asFile
         val xcResultFile = xcResultPath.get().asFile
         if (xcResultFile.exists()) {
             xcResultFile.deleteRecursively()
         }
-        val args = listOf(
-            "xcodebuild",
-            "test",
-            "-project", xcodeProject.absolutePath.toString(),
-            "-scheme", scheme.get(),
-            "-destination", destination.get(),
-            "-resultBundlePath", xcResultFile.absolutePath,
-        )
-        logger.info("Command : ${args.joinToString(" ")}")
-        execOperations.executeQuietly(args)
+        simCtrl.start { destinationDesc ->
+            val args = listOf(
+                "xcodebuild",
+                "test",
+                "-project", xcodeProject.absolutePath.toString(),
+                "-scheme", scheme.get(),
+                "-destination", destinationDesc,
+                "-resultBundlePath", xcResultFile.absolutePath,
+            )
+            logger.info("Command : ${args.joinToString(" ")}")
+            execOperations.executeQuietly(args)
+        }
     }
 
     private fun requireXcodeBuild() {
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt
new file mode 100644
index 0000000..6f83f22
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeBuildService.kt
@@ -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.benchmark.darwin.gradle
+
+import org.gradle.api.Project
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+
+/**
+ * A service that manages simulators / devices. Also manages booting up and tearing down instances
+ */
+interface XCodeBuildService : BuildService<BuildServiceParameters.None> {
+    companion object {
+        const val XCODE_BUILD_SERVICE_NAME = "DarwinXCodeBuildService"
+    }
+}
+
+/**
+ * Register the [XCodeBuildService] as a shared gradle service.
+ */
+fun Project.configureXCodeBuildService() {
+    gradle.sharedServices.registerIfAbsent(
+        XCodeBuildService.XCODE_BUILD_SERVICE_NAME,
+        XCodeBuildService::class.java
+    ) { spec ->
+        // Run one xcodebuild at a time.
+        spec.maxParallelUsages.set(1)
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt
new file mode 100644
index 0000000..bfa0fc6
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/XCodeSimCtrl.kt
@@ -0,0 +1,184 @@
+/*
+ * 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.benchmark.darwin.gradle
+
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
+import androidx.benchmark.darwin.gradle.xcode.SimulatorRuntimes
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import org.gradle.process.ExecOperations
+
+/**
+ * Controls XCode Simulator instances.
+ */
+class XCodeSimCtrl(
+    private val execOperations: ExecOperations,
+    private val destination: String
+) {
+
+    private var destinationDesc: String? = null
+    private var deviceId: String? = null
+
+    // A device type looks something like
+    // platform=iOS Simulator,name=iPhone 13,OS=15.2
+
+    fun start(block: (destinationDesc: String) -> Unit) {
+        try {
+            val instance = boot(destination, execOperations)
+            destinationDesc = instance.destinationDesc
+            deviceId = instance.deviceId
+            block(destinationDesc!!)
+        } finally {
+            val id = deviceId
+            if (id != null) {
+                shutDownAndDelete(execOperations, id)
+            }
+        }
+    }
+
+    companion object {
+        private const val PLATFORM_KEY = "platform"
+        private const val NAME_KEY = "name"
+        private const val RUNTIME_KEY = "OS"
+        private const val IOS_SIMULATOR = "iOS Simulator"
+        private const val IPHONE_PRODUCT_FAMILY = "iPhone"
+
+        /** Simulator metadata */
+        internal data class SimulatorInstance(
+            /* The full device descriptor. */
+            val destinationDesc: String,
+            /* The unique device UUID if we end up booting the device. */
+            val deviceId: String? = null
+        )
+
+        internal fun boot(
+            destination: String,
+            execOperations: ExecOperations
+        ): SimulatorInstance {
+            val parsed = parse(destination)
+            return when (platform(parsed)) {
+                // Simulator needs to be booted up.
+                IOS_SIMULATOR -> bootSimulator(destination, parsed, execOperations)
+                // For other destinations, we don't have much to do.
+                else -> SimulatorInstance(destinationDesc = destination)
+            }
+        }
+
+        private fun discoverSimulatorRuntimeVersion(
+            execOperations: ExecOperations
+        ): String? {
+            val json = executeCommand(
+                execOperations, listOf(
+                    "xcrun", "simctl", "list", "runtimes", "--json"
+                )
+            )
+            val simulatorRuntimes = GsonHelpers.gson().fromJson(json, SimulatorRuntimes::class.java)
+            // There is usually one version of the simulator runtime available per xcode version
+            val supported = simulatorRuntimes.runtimes.firstOrNull { runtime ->
+                runtime.isAvailable && runtime.supportedDeviceTypes.any { deviceType ->
+                    deviceType.productFamily == IPHONE_PRODUCT_FAMILY
+                }
+            }
+            return supported?.version
+        }
+
+        private fun bootSimulator(
+            destination: String,
+            parsed: Map<String, String>,
+            execOperations: ExecOperations
+        ): SimulatorInstance {
+            val deviceName = deviceName(parsed)
+            val supported = discoverSimulatorRuntimeVersion(execOperations)
+            // While this is not strictly correct, these versions should be pretty close.
+            val runtimeVersion = supported ?: runtimeVersion(parsed)
+            check(deviceName != null && runtimeVersion != null) {
+                "Invalid destination spec: $destination"
+            }
+            val deviceId = executeCommand(
+                execOperations, listOf(
+                    "xcrun",
+                    "simctl",
+                    "create",
+                    deviceName, // Use the deviceName as the name
+                    deviceName,
+                    "iOS$runtimeVersion"
+                )
+            )
+            check(deviceId.isNotBlank()) {
+                "Invalid device id for simulator: $deviceId (Destination: $destination)"
+            }
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "boot", deviceId)
+            )
+            // Return a simulator instance with the new descriptor + device id
+            return SimulatorInstance(destinationDesc = "id=$deviceId", deviceId = deviceId)
+        }
+
+        internal fun shutDownAndDelete(
+            execOperations: ExecOperations,
+            deviceId: String
+        ) {
+            // Cleans up the instance of the simulator that was booted up.
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "shutdown", deviceId)
+            )
+            executeCommand(
+                execOperations, listOf("xcrun", "simctl", "delete", deviceId)
+            )
+        }
+
+        private fun executeCommand(execOperations: ExecOperations, args: List<String>): String {
+            val output = ByteArrayOutputStream()
+            output.use {
+                execOperations.exec { spec ->
+                    spec.commandLine = args
+                    spec.standardOutput = output
+                }
+                val input = ByteArrayInputStream(output.toByteArray())
+                return input.use {
+                    // Trimming is important here, otherwise ExecOperations encodes the string
+                    // with shell specific escape sequences which mangle the device
+                    input.reader().readText().trim()
+                }
+            }
+        }
+
+        private fun platform(parsed: Map<String, String>): String? {
+            return parsed[PLATFORM_KEY]
+        }
+
+        private fun deviceName(parsed: Map<String, String>): String? {
+            return parsed[NAME_KEY]
+        }
+
+        private fun runtimeVersion(parsed: Map<String, String>): String? {
+            return parsed[RUNTIME_KEY]
+        }
+
+        private fun parse(destination: String): Map<String, String> {
+            return destination.splitToSequence(",")
+                .map { split ->
+                    check(split.contains("=")) {
+                        "Invalid destination spec: $destination"
+                    }
+                    val (key, value) = split.split("=", limit = 2)
+                    key.trim() to value.trim()
+                }
+                .toMap()
+        }
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt
new file mode 100644
index 0000000..3d9414c
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/GsonHelpers.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.benchmark.darwin.gradle.xcode
+
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+
+object GsonHelpers {
+    internal fun gsonBuilder(): GsonBuilder {
+        val builder = GsonBuilder()
+        builder.registerTypeAdapter(
+            ActionsTestSummaryGroupOrMeta::class.java,
+            ActionTestSummaryDeserializer()
+        )
+        return builder
+    }
+
+    fun gson(): Gson {
+        return gsonBuilder().create()
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
similarity index 93%
rename from benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
rename to benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
index 863da27..237a8e2 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/Models.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCResultModels.kt
@@ -16,8 +16,6 @@
 
 package androidx.benchmark.darwin.gradle.xcode
 
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
 import com.google.gson.JsonDeserializationContext
 import com.google.gson.JsonDeserializer
 import com.google.gson.JsonElement
@@ -203,10 +201,10 @@
         context: JsonDeserializationContext
     ): ActionsTestSummaryGroupOrMeta {
         return if (checkType(jsonElement, ACTION_TEST_SUMMARY_GROUP)) {
-            val adapter = Models.gson().getAdapter(ActionTestSummaryGroup::class.java)
+            val adapter = GsonHelpers.gson().getAdapter(ActionTestSummaryGroup::class.java)
             adapter.fromJson(jsonElement.toString())
         } else if (checkType(jsonElement, ACTION_TEST_SUMMARY_META)) {
-            val adapter = Models.gson().getAdapter(ActionTestSummaryMeta::class.java)
+            val adapter = GsonHelpers.gson().getAdapter(ActionTestSummaryMeta::class.java)
             adapter.fromJson(jsonElement.toString())
         } else {
             reportException(jsonElement)
@@ -227,7 +225,7 @@
             val json = jsonElement.asJsonObject
             val jsonType: JsonElement? = json.get(TYPE)
             if (jsonType != null && jsonType.isJsonObject) {
-                val adapter = Models.gson().getAdapter(TypeDefinition::class.java)
+                val adapter = GsonHelpers.gson().getAdapter(TypeDefinition::class.java)
                 val type = adapter.fromJson(jsonType.toString())
                 return type.name == name
             }
@@ -324,18 +322,3 @@
         return activitySummaries.title()
     }
 }
-
-object Models {
-    internal fun gsonBuilder(): GsonBuilder {
-        val builder = GsonBuilder()
-        builder.registerTypeAdapter(
-            ActionsTestSummaryGroupOrMeta::class.java,
-            ActionTestSummaryDeserializer()
-        )
-        return builder
-    }
-
-    fun gson(): Gson {
-        return gsonBuilder().create()
-    }
-}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt
new file mode 100644
index 0000000..5255a96
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XCodeSimulatorModels.kt
@@ -0,0 +1,69 @@
+/*
+ * 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.benchmark.darwin.gradle.xcode
+
+/**
+ * A representation of the output of `xcrun simctl runtimes list --json`.
+ *
+ * That produces an object that contains a [List] of [SimulatorRuntime].
+ */
+data class SimulatorRuntimes(
+    val runtimes: List<SimulatorRuntime>
+)
+
+/**
+ * An XCode simulator runtime. The serialized representation looks something like:
+ *
+ * ```json
+ * {
+ *   "bundlePath" : "...\/Profiles\/Runtimes\/watchOS.simruntime",
+ *   "buildversion" : "19S51",
+ *   "runtimeRoot" : "....\/Runtimes\/watchOS.simruntime\/Contents\/Resources\/RuntimeRoot",
+ *   "identifier" : "com.apple.CoreSimulator.SimRuntime.watchOS-8-3",
+ *   "version" : "8.3",
+ *    "isAvailable" : true,
+ *    "supportedDeviceTypes" : [
+ *      ...
+ *    ]
+ * }
+ * ```
+ */
+data class SimulatorRuntime(
+    val identifier: String,
+    val version: String,
+    val isAvailable: Boolean,
+    val supportedDeviceTypes: List<SupportedDeviceType>
+)
+
+/**
+ * A serialized supported device type has a representation that looks like:
+ *
+ * ```json
+ * {
+ *  "bundlePath" : "...\/CoreSimulator\/Profiles\/DeviceTypes\/iPhone 6s.simdevicetype",
+ *  "name" : "iPhone 6s",
+ *  "identifier" : "com.apple.CoreSimulator.SimDeviceType.iPhone-6s",
+ *  "productFamily" : "iPhone"
+ *  }
+ * ```
+ */
+data class SupportedDeviceType(
+    val bundlePath: String,
+    val name: String,
+    val identifier: String,
+    val productFamily: String
+)
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
index c0c3c15..953b771 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/xcode/XcResultParser.kt
@@ -29,7 +29,7 @@
 ) {
     fun parseResults(): Pair<ActionsInvocationRecord, List<ActionTestSummary>> {
         val json = commandExecutor(xcRunCommand())
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
         val summaries = record.actions.testReferences().flatMap { testRef ->
             val summary = commandExecutor(xcRunCommand(testRef))
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
index 663b7ea..20bc13a 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/ModelsTest.kt
@@ -17,7 +17,7 @@
 import androidx.benchmark.darwin.gradle.xcode.ActionTestPlanRunSummaries
 import androidx.benchmark.darwin.gradle.xcode.ActionTestSummary
 import androidx.benchmark.darwin.gradle.xcode.ActionsInvocationRecord
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -28,7 +28,7 @@
     @Test
     fun parseXcResultOutputs() {
         val json = testData(XCRESULT_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val record = gson.fromJson(json, ActionsInvocationRecord::class.java)
         assertThat(record.actions.testReferences().size).isEqualTo(1)
         assertThat(record.metrics.size()).isEqualTo(1)
@@ -38,7 +38,7 @@
     @Test
     fun parseTestsReferenceOutput() {
         val json = testData(XC_TESTS_REFERENCE_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val testPlanSummaries = gson.fromJson(json, ActionTestPlanRunSummaries::class.java)
         val testSummaryMetas = testPlanSummaries.testSummaries()
         assertThat(testSummaryMetas.size).isEqualTo(1)
@@ -49,7 +49,7 @@
     @Test
     fun parseTestOutput() {
         val json = testData(XC_TEST_OUTPUT_JSON).readText()
-        val gson = Models.gson()
+        val gson = GsonHelpers.gson()
         val testSummary = gson.fromJson(json, ActionTestSummary::class.java)
         assertThat(testSummary.title()).isNotEmpty()
         assertThat(testSummary.isSuccessful()).isTrue()
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
index 00468a9..2ce55a7 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/test/kotlin/XcResultParserTest.kt
@@ -15,7 +15,7 @@
  */
 
 import androidx.benchmark.darwin.gradle.skia.Metrics
-import androidx.benchmark.darwin.gradle.xcode.Models
+import androidx.benchmark.darwin.gradle.xcode.GsonHelpers
 import androidx.benchmark.darwin.gradle.xcode.XcResultParser
 import com.google.common.truth.Truth.assertThat
 import org.junit.Assume.assumeTrue
@@ -51,7 +51,7 @@
         assertThat(record.metrics.size()).isEqualTo(2)
         assertThat(summaries.isNotEmpty()).isTrue()
         val metrics = Metrics.buildMetrics(record, summaries)
-        val json = Models.gsonBuilder()
+        val json = GsonHelpers.gsonBuilder()
             .setPrettyPrinting()
             .create()
             .toJson(metrics)
diff --git a/benchmark/benchmark-darwin-xcode/.gitignore b/benchmark/benchmark-darwin-xcode/.gitignore
index 78f146f..332a4f0 100644
--- a/benchmark/benchmark-darwin-xcode/.gitignore
+++ b/benchmark/benchmark-darwin-xcode/.gitignore
@@ -1,7 +1,5 @@
 # XCode Projects
 *.xcodeproj
-Info.plist
 
 # XCode results
 *.xcresult
-
diff --git a/benchmark/benchmark-darwin-xcode/projects/App.plist b/benchmark/benchmark-darwin-xcode/projects/App.plist
new file mode 100644
index 0000000..47ef031
--- /dev/null
+++ b/benchmark/benchmark-darwin-xcode/projects/App.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist b/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist
new file mode 100644
index 0000000..6c40a6c
--- /dev/null
+++ b/benchmark/benchmark-darwin-xcode/projects/Benchmark.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>BNDL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>
diff --git a/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml b/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
index a7c06ed..1616cfa 100644
--- a/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
+++ b/benchmark/benchmark-darwin-xcode/projects/benchmark-darwin-samples-xcode.yml
@@ -7,7 +7,7 @@
     type: application
     platform: iOS
     info:
-      path: Info.plist
+      path: App.plist
     sources:
       - path: '../iosSources/main'
     scheme:
@@ -23,7 +23,7 @@
     type: bundle.unit-test
     platform: iOS
     info:
-      path: Info.plist
+      path: Benchmark.plist
     sources:
       - path: '../iosAppUnitTests/main'
     scheme:
@@ -38,4 +38,4 @@
   CODE_SIGNING_REQUIRED: 'NO'
   CODE_SIGN_ENTITLEMENTS: ''
   CODE_SIGNING_ALLOWED: 'NO'
-  IPHONEOS_DEPLOYMENT_TARGET: 15.2
+  IPHONEOS_DEPLOYMENT_TARGET: 15.0
diff --git a/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml b/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
index c40aa28..910be36 100644
--- a/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
+++ b/benchmark/benchmark-darwin-xcode/projects/collection-benchmark-ios.yml
@@ -7,7 +7,7 @@
     type: application
     platform: iOS
     info:
-      path: Info.plist
+      path: App.plist
     sources:
       - path: '../iosSources/main'
     scheme:
@@ -23,7 +23,7 @@
     type: bundle.unit-test
     platform: iOS
     info:
-      path: Info.plist
+      path: Benchmark.plist
     sources:
       - path: '../iosAppUnitTests/main'
     scheme:
@@ -38,4 +38,4 @@
   CODE_SIGNING_REQUIRED: 'NO'
   CODE_SIGN_ENTITLEMENTS: ''
   CODE_SIGNING_ALLOWED: 'NO'
-  IPHONEOS_DEPLOYMENT_TARGET: 15.2
+  IPHONEOS_DEPLOYMENT_TARGET: 15.0
diff --git a/benchmark/benchmark-junit4/build.gradle b/benchmark/benchmark-junit4/build.gradle
index 3ee2141..ee2aaf0 100644
--- a/benchmark/benchmark-junit4/build.gradle
+++ b/benchmark/benchmark-junit4/build.gradle
@@ -37,7 +37,7 @@
 
     implementation("androidx.test:rules:1.4.0")
     implementation("androidx.test:runner:1.4.0")
-    implementation("androidx.tracing:tracing-ktx:1.0.0")
+    implementation("androidx.tracing:tracing-ktx:1.1.0")
     api("androidx.annotation:annotation:1.1.0")
 
     androidTestImplementation(project(":internal-testutils-ktx"))
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
index 676ce86..1671a9b 100644
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/BenchmarkRule.kt
@@ -24,13 +24,13 @@
 import androidx.benchmark.UserspaceTracing
 import androidx.benchmark.perfetto.PerfettoCaptureWrapper
 import androidx.benchmark.perfetto.UiState
+import androidx.benchmark.perfetto.appendBytesSafely
 import androidx.benchmark.perfetto.appendUiState
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.GrantPermissionRule
 import androidx.tracing.Trace
 import androidx.tracing.trace
 import java.io.File
-import java.io.FileNotFoundException
 import org.junit.Assert.assertTrue
 import org.junit.Assume.assumeTrue
 import org.junit.rules.RuleChain
@@ -225,23 +225,16 @@
                 userspaceTrace = UserspaceTracing.commitToTrace()
             }?.apply {
                 // trace completed, and copied into app writeable dir
-
-                try {
-                    val file = File(this)
-
-                    file.appendBytes(userspaceTrace!!.encode())
-                    file.appendUiState(
-                        UiState(
-                            timelineStart = null,
-                            timelineEnd = null,
-                            highlightPackage = InstrumentationRegistry.getInstrumentation()
-                                .context.packageName
-                        )
+                val file = File(this)
+                file.appendBytesSafely(userspaceTrace!!.encode())
+                file.appendUiState(
+                    UiState(
+                        timelineStart = null,
+                        timelineEnd = null,
+                        highlightPackage = InstrumentationRegistry.getInstrumentation()
+                            .context.packageName
                     )
-                } catch (exception: FileNotFoundException) {
-                    // TODO(b/227510293): fix record to return a null in this case
-                    Log.d(TAG, "Unable to add additional detail to captured trace $this")
-                }
+                )
             }
 
             if (enableReport) {
diff --git a/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
index 63adf8e..9ddb585 100644
--- a/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro-junit4/api/public_plus_experimental_current.txt
@@ -7,6 +7,10 @@
     method public void collectBaselineProfile(String packageName, optional int iterations, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method public void collectBaselineProfile(String packageName, optional int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method public void collectBaselineProfile(String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, optional boolean strictStability, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, optional int stableIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi public void collectStableBaselineProfile(String packageName, int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
   }
 
   public final class MacrobenchmarkRule implements org.junit.rules.TestRule {
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
index 7b09cb7..6ccf203 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/BaselineProfileRule.kt
@@ -19,8 +19,10 @@
 import android.Manifest
 import androidx.annotation.RequiresApi
 import androidx.benchmark.Arguments
+import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
 import androidx.benchmark.macro.MacrobenchmarkScope
 import androidx.benchmark.macro.collectBaselineProfile
+import androidx.benchmark.macro.collectStableBaselineProfile
 import androidx.test.rule.GrantPermissionRule
 import org.junit.Assume.assumeTrue
 import org.junit.rules.RuleChain
@@ -102,5 +104,39 @@
         )
     }
 
+    /**
+     * Collects baseline profiles for a critical user journey, while ensuring that the generated
+     * profiles are stable for a minimum of [stableIterations].
+     *
+     * @param packageName Package name of the app for which profiles are to be generated.
+     * @param maxIterations Maximum number of iterations to run for when collecting profiles.
+     * @param stableIterations Minimum number of iterations for while baseline profiles have to be stable.
+     * @param strictStability Enforce if the generated profile was stable
+     * @param packageFilters List of package names to use as a filter for the generated profiles.
+     *  By default no filters are applied. Note that this works only when the code is not obfuscated.
+     *  Package filters are only applied after the profiles are deemed stable.
+     * @param [profileBlock] defines the critical user journey.
+     */
+    @JvmOverloads
+    @ExperimentalStableBaselineProfilesApi
+    public fun collectStableBaselineProfile(
+        packageName: String,
+        maxIterations: Int,
+        stableIterations: Int = 3,
+        strictStability: Boolean = false,
+        packageFilters: List<String> = emptyList(),
+        profileBlock: MacrobenchmarkScope.() -> Unit
+    ) {
+        collectStableBaselineProfile(
+            uniqueName = currentDescription.toUniqueName(),
+            packageName = packageName,
+            stableIterations = stableIterations,
+            maxIterations = maxIterations,
+            strictStability = strictStability,
+            packageFilters = packageFilters,
+            profileBlock = profileBlock
+        )
+    }
+
     private fun Description.toUniqueName() = testClass.simpleName + "_" + methodName
 }
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index 301070a..8f5b8c0 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -58,6 +58,9 @@
   @kotlin.RequiresOptIn(message="This Metric API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalMetricApi {
   }
 
+  @kotlin.RequiresOptIn(message="The stable Baseline profile generation API is experimental.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalStableBaselineProfilesApi {
+  }
+
   public final class FrameTimingMetric extends androidx.benchmark.macro.Metric {
     ctor public FrameTimingMetric();
   }
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index aa128ff..eaaf6ef 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -16,6 +16,9 @@
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, optional int iterations, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, optional int iterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
     method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, optional boolean strictStability, optional java.util.List<java.lang.String> packageFilters, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, optional boolean strictStability, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectStableBaselineProfile(String uniqueName, String packageName, int stableIterations, int maxIterations, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class BatteryCharge {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
new file mode 100644
index 0000000..0da5134
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+import android.os.Build
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.test.filters.SdkSuppress
+import kotlin.test.assertNull
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@MediumTest
+class ProfileInstallBroadcastTest {
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun installProfile() {
+        assertNull(ProfileInstallBroadcast.installProfile(Packages.TARGET))
+    }
+
+    @Test
+    fun skipFileOperation() {
+        assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "WRITE_SKIP_FILE"))
+        assertNull(ProfileInstallBroadcast.skipFileOperation(Packages.TARGET, "DELETE_SKIP_FILE"))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
+    @Test
+    fun saveProfile() {
+        assertNull(ProfileInstallBroadcast.saveProfile(Packages.TARGET))
+    }
+
+    @Test
+    fun dropShaderCache() {
+        assertNull(ProfileInstallBroadcast.dropShaderCache(Packages.TARGET))
+    }
+}
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
index 7023a69..7583d02 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/StartupTimingQueryTest.kt
@@ -21,11 +21,11 @@
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import java.util.Locale
+import kotlin.test.assertEquals
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.Locale
-import kotlin.test.assertEquals
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
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 89c624e..8b370af 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
@@ -44,26 +44,9 @@
     packageFilters: List<String> = emptyList(),
     profileBlock: MacrobenchmarkScope.() -> Unit,
 ) {
-    require(
-        Build.VERSION.SDK_INT >= 33 ||
-            (Build.VERSION.SDK_INT >= 28 && Shell.isSessionRooted())
-    ) {
-        "Baseline Profile collection requires API 33+, or a rooted" +
-            " device running API 28 or higher and rooted adb session (via `adb root`)."
-    }
-
-    getInstalledPackageInfo(packageName) // throws clearly if not installed
-
+    val scope = buildMacrobenchmarkScope(packageName)
     val startTime = System.nanoTime()
-    val scope = MacrobenchmarkScope(packageName, launchWithClearTask = true)
-
-    val killProcessBlock = {
-        // When generating baseline profiles we want to default to using
-        // killProcess if the session is rooted. This is so we can collect
-        // baseline profiles for System Apps.
-        scope.killProcess(useKillAll = Shell.isSessionRooted())
-        Thread.sleep(Arguments.killProcessDelayMillis)
-    }
+    val killProcessBlock = scope.killProcessBlock()
 
     // always kill the process at beginning of a collection.
     killProcessBlock.invoke()
@@ -90,69 +73,215 @@
         }
 
         check(unfilteredProfile.isNotBlank()) {
+            """
+                Generated Profile is empty, before filtering.
+                Ensure your profileBlock invokes the target app, and
+                runs a non-trivial amount of code.
+            """.trimIndent()
+        }
+        // Filter
+        val profile = filterProfileRulesToTargetP(unfilteredProfile)
+        // Report
+        reportResults(profile, packageFilters, uniqueName, startTime)
+    } finally {
+        killProcessBlock.invoke()
+    }
+}
+
+/**
+ * Collects baseline profiles using a given [profileBlock], while additionally
+ * waiting until they are stable.
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresApi(28)
+@JvmOverloads
+fun collectStableBaselineProfile(
+    uniqueName: String,
+    packageName: String,
+    stableIterations: Int,
+    maxIterations: Int,
+    strictStability: Boolean = false,
+    packageFilters: List<String> = emptyList(),
+    profileBlock: MacrobenchmarkScope.() -> Unit
+) {
+    val scope = buildMacrobenchmarkScope(packageName)
+    val startTime = System.nanoTime()
+    val killProcessBlock = scope.killProcessBlock()
+    // always kill the process at beginning of a collection.
+    killProcessBlock.invoke()
+
+    try {
+        var stableCount = 1
+        var lastProfile: String? = null
+        var iteration = 1
+
+        while (iteration <= maxIterations) {
+            userspaceTrace("generate profile for $packageName ($iteration)") {
+                val mode = CompilationMode.Partial(
+                    baselineProfileMode = BaselineProfileMode.Disable,
+                    warmupIterations = 1
+                )
+                if (iteration == 1) {
+                    Log.d(TAG, "Resetting compiled state for $packageName for stable profiles.")
+                    mode.resetAndCompile(
+                        packageName = packageName,
+                        killProcessBlock = killProcessBlock
+                    ) {
+                        scope.iteration = iteration
+                        profileBlock(scope)
+                    }
+                } else {
+                    // Don't reset for subsequent iterations
+                    Log.d(TAG, "Killing package $packageName")
+                    killProcessBlock()
+                    mode.compileImpl(packageName = packageName,
+                        killProcessBlock = killProcessBlock
+                    ) {
+                        scope.iteration = iteration
+                        Log.d(TAG, "Compile iteration (${scope.iteration}) for $packageName")
+                        profileBlock(scope)
+                    }
+                }
+            }
+            val unfilteredProfile = if (Build.VERSION.SDK_INT >= 33) {
+                extractProfile(packageName)
+            } else {
+                extractProfileRooted(packageName)
+            }
+
+            // Check stability
+            val lastRuleSet = lastProfile?.lines()?.toSet() ?: emptySet()
+            val existingRuleSet = unfilteredProfile.lines().toSet()
+            if (lastRuleSet != existingRuleSet) {
+                if (iteration != 1) {
+                    Log.d(TAG, "Unstable profiles during iteration $iteration")
+                }
+                lastProfile = unfilteredProfile
+                stableCount = 1
+            } else {
+                Log.d(TAG,
+                    "Profiles stable in iteration $iteration (for $stableCount iterations)"
+                )
+                stableCount += 1
+                if (stableCount == stableIterations) {
+                    Log.d(TAG, "Baseline profile for $packageName is stable.")
+                    break
+                }
+            }
+            iteration += 1
+        }
+
+        if (strictStability) {
+            check(stableCount == stableIterations) {
+                "Baseline profiles for $packageName are not stable after $maxIterations."
+            }
+        }
+
+        check(!lastProfile.isNullOrBlank()) {
             "Generated Profile is empty, before filtering. Ensure your profileBlock" +
                 " invokes the target app, and runs a non-trivial amount of code"
         }
 
-        val profile = filterProfileRulesToTargetP(unfilteredProfile)
-
-        // Build a startup profile
-        var startupProfile: String? = null
-        if (Arguments.enableStartupProfiles) {
-            startupProfile =
-                startupProfile(profile, includeStartupOnly = Arguments.strictStartupProfiles)
-        }
-
-        // Filter profile if necessary based on filters
-        val filteredProfile = applyPackageFilters(profile, packageFilters)
-
-        // Write a file with a timestamp to be able to disambiguate between runs with the same
-        // unique name.
-
-        val fileName = "$uniqueName-baseline-prof.txt"
-        val absolutePath = Outputs.writeFile(fileName, "baseline-profile") {
-            it.writeText(filteredProfile)
-        }
-        var startupProfilePath: String? = null
-        if (startupProfile != null) {
-            val startupProfileFileName = "$uniqueName-startup-prof.txt"
-            startupProfilePath = Outputs.writeFile(startupProfileFileName, "startup-profile") {
-                it.writeText(startupProfile)
-            }
-        }
-        val tsFileName = "$uniqueName-baseline-prof-${Outputs.dateToFileName()}.txt"
-        val tsAbsolutePath = Outputs.writeFile(tsFileName, "baseline-profile-ts") {
-            Log.d(TAG, "Pull Baseline Profile with: `adb pull \"${it.absolutePath}\" .`")
-            it.writeText(filteredProfile)
-        }
-        var tsStartupAbsolutePath: String? = null
-        if (startupProfile != null) {
-            val tsStartupFileName = "$uniqueName-startup-prof-${Outputs.dateToFileName()}.txt"
-            tsStartupAbsolutePath = Outputs.writeFile(tsStartupFileName, "startup-profile-ts") {
-                Log.d(TAG, "Pull Startup Profile with: `adb pull \"${it.absolutePath}\" .`")
-                it.writeText(startupProfile)
-            }
-        }
-
-        val totalRunTime = System.nanoTime() - startTime
-        val results = Summary(
-            totalRunTime = totalRunTime,
-            profilePath = absolutePath,
-            profileTsPath = tsAbsolutePath,
-            startupProfilePath = startupProfilePath,
-            startupTsProfilePath = tsStartupAbsolutePath
-        )
-        InstrumentationResults.instrumentationReport {
-            val summary = summaryRecord(results)
-            ideSummaryRecord(summaryV1 = summary, summaryV2 = summary)
-            Log.d(TAG, "Total Run Time Ns: $totalRunTime")
-        }
+        val profile = filterProfileRulesToTargetP(lastProfile)
+        reportResults(profile, packageFilters, uniqueName, startTime)
     } finally {
         killProcessBlock.invoke()
     }
 }
 
 /**
+ * Builds a [MacrobenchmarkScope] instance after checking for the necessary pre-requisites.
+ */
+private fun buildMacrobenchmarkScope(packageName: String): MacrobenchmarkScope {
+    require(
+        Build.VERSION.SDK_INT >= 33 ||
+            (Build.VERSION.SDK_INT >= 28 && Shell.isSessionRooted())
+    ) {
+        "Baseline Profile collection requires API 33+, or a rooted" +
+            " device running API 28 or higher and rooted adb session (via `adb root`)."
+    }
+    getInstalledPackageInfo(packageName) // throws clearly if not installed
+    return MacrobenchmarkScope(packageName, launchWithClearTask = true)
+}
+
+/**
+ * Builds a function that can kill the target process using the provided [MacrobenchmarkScope].
+ */
+private fun MacrobenchmarkScope.killProcessBlock(): () -> Unit {
+    val killProcessBlock = {
+        // When generating baseline profiles we want to default to using
+        // killProcess if the session is rooted. This is so we can collect
+        // baseline profiles for System Apps.
+        this.killProcess(useKillAll = Shell.isSessionRooted())
+        Thread.sleep(Arguments.killProcessDelayMillis)
+    }
+    return killProcessBlock
+}
+
+/**
+ * Reports the results after having collected baseline profiles.
+ */
+private fun reportResults(
+    profile: String,
+    packageFilters: List<String>,
+    uniqueFilePrefix: String,
+    startTime: Long
+) {
+    // Build a startup profile
+    var startupProfile: String? = null
+    if (Arguments.enableStartupProfiles) {
+        startupProfile =
+            startupProfile(profile, includeStartupOnly = Arguments.strictStartupProfiles)
+    }
+
+    // Filter profile if necessary based on filters
+    val filteredProfile = applyPackageFilters(profile, packageFilters)
+
+    // Write a file with a timestamp to be able to disambiguate between runs with the same
+    // unique name.
+
+    val fileName = "$uniqueFilePrefix-baseline-prof.txt"
+    val absolutePath = Outputs.writeFile(fileName, "baseline-profile") {
+        it.writeText(filteredProfile)
+    }
+    var startupProfilePath: String? = null
+    if (startupProfile != null) {
+        val startupProfileFileName = "$uniqueFilePrefix-startup-prof.txt"
+        startupProfilePath = Outputs.writeFile(startupProfileFileName, "startup-profile") {
+            it.writeText(startupProfile)
+        }
+    }
+    val tsFileName = "$uniqueFilePrefix-baseline-prof-${Outputs.dateToFileName()}.txt"
+    val tsAbsolutePath = Outputs.writeFile(tsFileName, "baseline-profile-ts") {
+        Log.d(TAG, "Pull Baseline Profile with: `adb pull \"${it.absolutePath}\" .`")
+        it.writeText(filteredProfile)
+    }
+    var tsStartupAbsolutePath: String? = null
+    if (startupProfile != null) {
+        val tsStartupFileName = "$uniqueFilePrefix-startup-prof-${Outputs.dateToFileName()}.txt"
+        tsStartupAbsolutePath = Outputs.writeFile(tsStartupFileName, "startup-profile-ts") {
+            Log.d(TAG, "Pull Startup Profile with: `adb pull \"${it.absolutePath}\" .`")
+            it.writeText(startupProfile)
+        }
+    }
+
+    val totalRunTime = System.nanoTime() - startTime
+    val results = Summary(
+        totalRunTime = totalRunTime,
+        profilePath = absolutePath,
+        profileTsPath = tsAbsolutePath,
+        startupProfilePath = startupProfilePath,
+        startupTsProfilePath = tsStartupAbsolutePath
+    )
+    InstrumentationResults.instrumentationReport {
+        val summary = summaryRecord(results)
+        ideSummaryRecord(summaryV1 = summary, summaryV2 = summary)
+        Log.d(TAG, "Total Run Time Ns: $totalRunTime")
+    }
+}
+
+/**
  * Use `pm dump-profiles` to get profile from the target app,
  * which puts results in `/data/misc/profman/`
  *
@@ -201,16 +330,29 @@
     // When compiling with CompilationMode.SpeedProfile, ART stores the profile in one of
     // 2 locations. The `ref` profile path, or the `current` path.
     // The `current` path is eventually merged  into the `ref` path after background dexopt.
-    for (currentPath in pathOptions) {
+    val profiles = pathOptions.mapNotNull { currentPath ->
         Log.d(TAG, "Using profile location: $currentPath")
         val profile = Shell.executeScriptCaptureStdout(
             "profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath"
         )
-        if (profile.isNotBlank()) {
-            return profile
+        profile.ifBlank { null }
+    }
+    if (profiles.isEmpty()) {
+        throw IllegalStateException("The profile is empty.")
+    }
+    // Merge rules
+    val rules = mutableSetOf<String>()
+    profiles.forEach { profile ->
+        profile.lines().forEach { rule ->
+            rules.add(rule)
         }
     }
-    throw IllegalStateException("The profile is empty.")
+    val builder = StringBuilder()
+    rules.forEach {
+        builder.append(it)
+        builder.append("\n")
+    }
+    return builder.toString()
 }
 
 @VisibleForTesting
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
similarity index 68%
copy from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
copy to benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
index dfb2685..de9746e 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ExperimentalStableBaselineProfilesApi.kt
@@ -14,13 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.benchmark.macro
 
-import androidx.compose.ui.graphics.Color
-
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+@RequiresOptIn(message = "The stable Baseline profile generation API is experimental.")
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+annotation class ExperimentalStableBaselineProfilesApi
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
index 9199b83..4c2451d 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialListScrollBaselineProfile.kt
@@ -18,6 +18,7 @@
 
 import android.content.Intent
 import android.graphics.Point
+import androidx.benchmark.macro.ExperimentalStableBaselineProfilesApi
 import androidx.benchmark.macro.junit4.BaselineProfileRule
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -72,6 +73,37 @@
         )
     }
 
+    @Test
+    @OptIn(ExperimentalStableBaselineProfilesApi::class)
+    fun stableBaselineProfiles() {
+        baselineRule.collectStableBaselineProfile(
+            packageName = "androidx.benchmark.integration.macrobenchmark.target",
+            stableIterations = 3,
+            maxIterations = 10,
+            profileBlock = {
+                val intent = Intent()
+                intent.action = ACTION
+                startActivityAndWait(intent)
+                val recycler = device.wait(
+                    Until.findObject(
+                        By.res(
+                            PACKAGE_NAME,
+                            RESOURCE_ID
+                        )
+                    ),
+                    TIMEOUT
+                )
+                // Setting a gesture margin is important otherwise gesture nav is triggered.
+                recycler.setGestureMargin(device.displayWidth / 5)
+                repeat(10) {
+                    // From center we scroll 2/3 of it which is 1/3 of the screen.
+                    recycler.drag(Point(0, recycler.visibleCenter.y / 3))
+                    device.waitForIdle()
+                }
+            }
+        )
+    }
+
     companion object {
         private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
         private const val ACTION =
diff --git a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
index 951e8ee..000db52 100644
--- a/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
+++ b/browser/browser/src/main/aidl/android/support/customtabs/ICustomTabsCallback.aidl
@@ -23,11 +23,15 @@
  * @hide
  */
 interface ICustomTabsCallback {
-    void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
-    void extraCallback(String callbackName, in Bundle args) = 2;
+    oneway void onNavigationEvent(int navigationEvent, in Bundle extras) = 1;
+    oneway void extraCallback(String callbackName, in Bundle args) = 2;
+
+    // Not defined with 'oneway' to preserve the calling order among |onPostMessage()| and related calls.
     void onMessageChannelReady(in Bundle extras) = 3;
     void onPostMessage(String message, in Bundle extras) = 4;
-    void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
+    oneway void onRelationshipValidationResult(int relation, in Uri origin, boolean result, in Bundle extras) = 5;
+
+    // API with return value cannot be 'oneway'.
     Bundle extraCallbackWithResult(String callbackName, in Bundle args) = 6;
-    void onActivityResized(int height, int width, in Bundle extras) = 7;
+    oneway void onActivityResized(int height, int width, in Bundle extras) = 7;
 }
diff --git a/buildSrc/OWNERS b/buildSrc/OWNERS
index d675db7..93edef1 100644
--- a/buildSrc/OWNERS
+++ b/buildSrc/OWNERS
@@ -1,7 +1,6 @@
 set noparent
 
 jeffrygaston@google.com
-sjgilbert@google.com
 aurimas@google.com
 alanv@google.com
 nickanthony@google.com
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index d82baf1..c0a5ae5 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -17,7 +17,6 @@
 package androidx.build
 
 import androidx.benchmark.gradle.BenchmarkPlugin
-import androidx.build.AndroidXImplPlugin.Companion.CHECK_RELEASE_READY_TASK
 import androidx.build.AndroidXImplPlugin.Companion.TASK_TIMEOUT_MINUTES
 import androidx.build.Release.DEFAULT_PUBLISH_CONFIG
 import androidx.build.SupportConfig.BUILD_TOOLS_VERSION
@@ -836,7 +835,6 @@
             if (extension.type != LibraryType.SAMPLES) {
                 val verifyDependencyVersionsTask = project.createVerifyDependencyVersionsTask()
                 if (verifyDependencyVersionsTask != null) {
-                    project.createCheckReleaseReadyTask(listOf(verifyDependencyVersionsTask))
                     taskConfigurator(verifyDependencyVersionsTask)
                 }
             }
@@ -845,7 +843,6 @@
 
     companion object {
         const val BUILD_TEST_APKS_TASK = "buildTestApks"
-        const val CHECK_RELEASE_READY_TASK = "checkReleaseReady"
         const val CREATE_LIBRARY_BUILD_INFO_FILES_TASK = "createLibraryBuildInfoFiles"
         const val GENERATE_TEST_CONFIGURATION_TASK = "GenerateTestConfiguration"
         const val ZIP_TEST_CONFIGS_WITH_APKS_TASK = "zipTestConfigsWithApks"
@@ -906,18 +903,6 @@
 val Project.multiplatformExtension
     get() = extensions.findByType(KotlinMultiplatformExtension::class.java)
 
-/**
- * Creates the [CHECK_RELEASE_READY_TASK], which aggregates tasks that must pass for a
- * project to be considered ready for public release.
- */
-private fun Project.createCheckReleaseReadyTask(taskProviderList: List<TaskProvider<out Task>>) {
-    tasks.register(CHECK_RELEASE_READY_TASK) {
-        for (taskProvider in taskProviderList) {
-            it.dependsOn(taskProvider)
-        }
-    }
-}
-
 @Suppress("UNCHECKED_CAST")
 fun Project.getProjectsMap(): ConcurrentHashMap<String, String> {
     project.rootProject.extra.set(ACCESSED_PROJECTS_MAP_KEY, true)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
index e6780db..f7110aa 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -99,18 +99,10 @@
     @Input
     lateinit var excludedPackagesForKotlin: Set<String>
 
-    /**
-     * These two variables control displaying of additional metadata in the refdocs.
-     *
-     * LIBRARY_METADATA_FILE: file containing artifactID and other metadata
-     * SHOW_LIBRARY_METADATA: set to "true" to display the data
-     */
+    // Maps to the system variable LIBRARY_METADATA_FILE containing artifactID and other metadata
     @get:[InputFile PathSensitive(PathSensitivity.NONE)]
     abstract val libraryMetadataFile: RegularFileProperty
 
-    @Input
-    var showLibraryMetadata: Boolean = false
-
     // The base URL to create source links for classes, as a format string with placeholders for the
     // file path and qualified class name.
     @Input
@@ -212,7 +204,6 @@
             excludedPackagesForJava = excludedPackagesForJava,
             excludedPackagesForKotlin = excludedPackagesForKotlin,
             libraryMetadataFile = libraryMetadataFile,
-            showLibraryMetadata = showLibraryMetadata,
         )
     }
 
@@ -236,7 +227,6 @@
     val excludedPackagesForJava: ListProperty<String>
     val excludedPackagesForKotlin: ListProperty<String>
     var libraryMetadataFile: Provider<RegularFile>
-    var showLibraryMetadata: Boolean
 }
 
 fun runDackkaWithArgs(
@@ -247,7 +237,6 @@
     excludedPackagesForJava: Set<String>,
     excludedPackagesForKotlin: Set<String>,
     libraryMetadataFile: Provider<RegularFile>,
-    showLibraryMetadata: Boolean,
 ) {
     val workQueue = workerExecutor.noIsolation()
     workQueue.submit(DackkaWorkAction::class.java) { parameters ->
@@ -257,7 +246,6 @@
         parameters.excludedPackagesForJava.set(excludedPackagesForJava)
         parameters.excludedPackagesForKotlin.set(excludedPackagesForKotlin)
         parameters.libraryMetadataFile = libraryMetadataFile
-        parameters.showLibraryMetadata = showLibraryMetadata
     }
 }
 
@@ -273,7 +261,6 @@
             // b/183989795 tracks moving these away from an environment variables
             it.environment("DEVSITE_TENANT", "androidx")
             it.environment("LIBRARY_METADATA_FILE", parameters.libraryMetadataFile.get().toString())
-            it.environment("SHOW_LIBRARY_METADATA", parameters.showLibraryMetadata)
 
             if (parameters.excludedPackages.get().isNotEmpty())
                 it.environment(
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS b/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
index 76c5a36..a823e8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/OWNERS
@@ -1,3 +1,5 @@
 asfalcone@google.com
+fsladkey@google.com
+juliamcclellan@google.com
 owengray@google.com
 tiem@google.com
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
index 3a6a8be..8784f69 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -458,7 +458,6 @@
                 excludedPackagesForJava = hiddenPackagesJava
                 excludedPackagesForKotlin = emptySet()
                 libraryMetadataFile.set(getMetadataRegularFile(project))
-                showLibraryMetadata = true
                 projectStructureMetadataFile = mergedProjectMetadata
                 // See go/dackka-source-link for details on this link.
                 baseSourceLink = "https://cs.android.com/search?" +
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS b/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
index b26dde4..a823e8b 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/OWNERS
@@ -1,3 +1,5 @@
 asfalcone@google.com
 fsladkey@google.com
-owengray@google.com
\ No newline at end of file
+juliamcclellan@google.com
+owengray@google.com
+tiem@google.com
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 dfb282f..89e34eb 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/paparazzi/AndroidXPaparazziImplPlugin.kt
@@ -31,9 +31,12 @@
 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.Copy
 import org.gradle.api.tasks.PathSensitivity
 import org.gradle.api.tasks.testing.Test
+import org.gradle.kotlin.dsl.get
 import org.gradle.kotlin.dsl.the
+import org.gradle.kotlin.dsl.register
 import org.gradle.kotlin.dsl.withType
 import org.gradle.process.JavaForkOptions
 
@@ -45,9 +48,10 @@
 ) : Plugin<Project> {
     override fun apply(project: Project) {
         val paparazziNative = project.createUnzippedPaparazziNativeDependency()
-        project.afterEvaluate {
-            project.tasks.withType<Test>().configureEach { it.configureTestTask(paparazziNative) }
-        }
+        project.afterEvaluate { it.addTestUtilsDependency() }
+        project.tasks.register("updateGolden")
+        project.tasks.withType<Test>().configureEach { it.configureTestTask(paparazziNative) }
+        project.tasks.withType<Test>().whenTaskAdded { project.registerUpdateGoldenTask(it) }
     }
 
     /**
@@ -56,9 +60,8 @@
      */
     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 cachedGoldenRootDirectory = project.goldenRootDirectory
+        val cachedReportDirectory = reportDirectory
         val android = project.the<BaseExtension>()
         val packageName = requireNotNull(android.namespace) {
             "android.namespace must be set for Paparazzi"
@@ -70,7 +73,7 @@
             .withPropertyName("paparazziNative")
 
         // Attach golden directory to task inputs to invalidate tests when updating goldens
-        inputs.dir(goldenRootDirectory.resolve(modulePath))
+        inputs.dir(project.goldenDirectory)
             .withPathSensitivity(PathSensitivity.RELATIVE)
             .withPropertyName("goldenDirectory")
 
@@ -79,14 +82,15 @@
             .withPropertyName("paparazziReportDir")
 
         // Clean the contents of the report directory before each test run
-        doFirst { fileSystemOperations.delete { it.delete(reportDirectory.listFiles()) } }
+        doFirst { fileSystemOperations.delete { it.delete(cachedReportDirectory.listFiles()) } }
 
         // Set non-path system properties at configuration time, so that changes invalidate caching
         prefixedSystemProperties(
             "gradlePluginApplied" to "true",
             "compileSdkVersion" to TARGET_SDK_VERSION,
             "resourcePackageNames" to packageName, // TODO: Transitive resource packages?
-            "modulePath" to modulePath
+            "modulePath" to project.modulePath,
+            "updateGoldenTask" to "${project.path}:${updateGoldenTaskName()}"
         )
 
         // Set the remaining system properties at execution time, after the snapshotting, so that
@@ -97,12 +101,32 @@
                 "platformDir" to platformDirectory.canonicalPath,
                 "assetsDir" to ".", // TODO: Merged assets dirs? (needed for compose?)
                 "resDir" to ".", // TODO: Merged resource dirs? (needed for compose?)
-                "reportDir" to reportDirectory.canonicalPath,
-                "goldenRootDir" to goldenRootDirectory.canonicalPath,
+                "reportDir" to cachedReportDirectory.canonicalPath,
+                "goldenRootDir" to cachedGoldenRootDirectory.canonicalPath,
             )
         }
     }
 
+    /** Register a copy task for moving new images to the golden directory. */
+    private fun Project.registerUpdateGoldenTask(testTask: Test) {
+        tasks.register<Copy>(testTask.updateGoldenTaskName()) {
+            dependsOn(testTask)
+
+            from(testTask.reportDirectory) {
+                include("**/*_actual.png")
+                into(goldenDirectory)
+                rename { it.removeSuffix("_actual.png") + "_paparazzi.png" }
+            }
+        }
+
+        tasks["updateGolden"].dependsOn(testTask.updateGoldenTaskName())
+    }
+
+    /** Derive updateGolden task name from a test task name. */
+    private fun Test.updateGoldenTaskName(): String {
+        return "updateGolden" + name.removePrefix("test").replaceFirstChar { it.titlecase() }
+    }
+
     /**
      * Configure [UnzipPaparazziNativeTransform] for the project, and add the platform-specific
      * Paparazzi native layoutlib dependency, using the version in `libs.versions.toml`.
@@ -132,10 +156,36 @@
         }.files
     }
 
+    /** The golden image directory for this project. */
+    private val Project.goldenDirectory
+        get() = goldenRootDirectory.resolve(modulePath)
+
+    /** The root of the golden image directory in a standard AndroidX checkout. */
+    private val Project.goldenRootDirectory
+        get() = getSupportRootFolder().resolve("../../golden")
+
+    /** Filesystem path for this module derived from Gradle project path. */
+    private val Project.modulePath
+        get() = path.replace(':', '/').trim('/')
+
+    /** Output directory for storing reports and images. */
+    private val Test.reportDirectory
+        get() = project.buildDir.resolve("paparazzi").resolve(name)
+
+    /** Add a testImplementation dependency on the wrapper test utils library. */
+    private fun Project.addTestUtilsDependency() {
+        configurations["testImplementation"].dependencies.add(
+            dependencies.create(project(TEST_UTILS_PROJECT))
+        )
+    }
+
     private companion object {
         /** Package name of the test library, used to namespace system properties */
         const val PACKAGE_NAME = "androidx.testutils.paparazzi"
 
+        /** Project path to the wrapper test utils project. */
+        const val TEST_UTILS_PROJECT = ":internal-testutils-paparazzi"
+
         /** Artifact type attribute for unzipped Paparazzi layoutlib unzipped artifacts */
         const val UNZIPPED_PAPARAZZI_NATIVE = "unzipped-paparazzi-native"
 
diff --git a/busytown/androidx-native-mac-host-tests.sh b/busytown/androidx-native-mac-host-tests.sh
index b39a384..9c38c66 100755
--- a/busytown/androidx-native-mac-host-tests.sh
+++ b/busytown/androidx-native-mac-host-tests.sh
@@ -10,7 +10,10 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh allTests \
+# Setup simulators
+impl/androidx-native-mac-simulator-setup.sh
+
+impl/build.sh darwinBenchmarkResults allTests \
     --no-configuration-cache \
     -Pandroidx.ignoreTestFailures \
     -Pandroidx.displayTestOutput=false \
diff --git a/busytown/androidx-native-mac.sh b/busytown/androidx-native-mac.sh
index df2abf1..84d9387 100755
--- a/busytown/androidx-native-mac.sh
+++ b/busytown/androidx-native-mac.sh
@@ -7,6 +7,9 @@
 # disable GCP cache, these machines don't have credentials.
 export USE_ANDROIDX_REMOTE_BUILD_CACHE=false
 
+# Setup simulators
+impl/androidx-native-mac-simulator-setup.sh
+
 impl/build.sh buildOnServer allTests :docs-kmp:zipCombinedKmpDocs --no-configuration-cache -Pandroidx.displayTestOutput=false
 
 # run a separate createArchive task to prepare a repository
diff --git a/busytown/androidx_multiplatform.sh b/busytown/androidx_multiplatform.sh
index cf2f41c..5f39449 100755
--- a/busytown/androidx_multiplatform.sh
+++ b/busytown/androidx_multiplatform.sh
@@ -7,4 +7,12 @@
 export USE_ANDROIDX_REMOTE_BUILD_CACHE=gcp
 export ANDROIDX_PROJECTS=COMPOSE
 
-./androidx.sh -Pandroidx.compose.multiplatformEnabled=true compileDebugAndroidTestSources compileDebugSources desktopTestClasses -Pandroidx.enableAffectedModuleDetection=false "$@"
+
+# b/235340662 don't verify dependency versions because we cannot pin to multiplatform deps
+./androidx.sh \
+  -Pandroidx.compose.multiplatformEnabled=true \
+  compileDebugAndroidTestSources \
+  compileDebugSources \
+  desktopTestClasses \
+  -x verifyDependencyVersions  \
+  -Pandroidx.enableAffectedModuleDetection=false "$@"
diff --git a/busytown/impl/androidx-native-mac-simulator-setup.sh b/busytown/impl/androidx-native-mac-simulator-setup.sh
new file mode 100755
index 0000000..c10941e
--- /dev/null
+++ b/busytown/impl/androidx-native-mac-simulator-setup.sh
@@ -0,0 +1,8 @@
+XCODE_SIMULATORS=$(xcrun simctl list devices | grep "iPhone" | wc -l)
+if [ $XCODE_SIMULATORS == '0' ]; then
+  SIMULATOR_DEVICE=$(xcrun simctl create 'iPhone 12' 'iPhone 12' 'iOS15.0')
+  echo "Booting device $SIMULATOR_DEVICE"
+  xcrun simctl boot $SIMULATOR_DEVICE
+else
+  echo "Already have $XCODE_SIMULATORS simulators set up."
+fi
diff --git a/busytown/impl/build-metalava-and-androidx.sh b/busytown/impl/build-metalava-and-androidx.sh
index d24f86c..b609c1a 100755
--- a/busytown/impl/build-metalava-and-androidx.sh
+++ b/busytown/impl/build-metalava-and-androidx.sh
@@ -46,7 +46,7 @@
 
 # Mac grep doesn't support -P, so use perl version of `grep -oP "(?<=metalavaVersion=).*"`
 export METALAVA_VERSION=`perl -nle'print $& while m{(?<=metalavaVersion=).*}g' $METALAVA_DIR/src/main/resources/version.properties`
-export METALAVA_REPO="$CHECKOUT_ROOT/out/dist/repo/m2repository"
+export METALAVA_REPO="$DIST_DIR/repo/m2repository"
 
 function buildAndroidx() {
   ./frameworks/support/busytown/impl/build.sh $androidxArguments \
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
new file mode 100644
index 0000000..7bbc024
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -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 androidx.camera.camera2.pipe.integration.impl
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.display.DisplayManager
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Assume
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@Suppress("DEPRECATION") // getRealSize
+@RunWith(AndroidJUnit4::class)
+@SdkSuppress(minSdkVersion = 21)
+class DisplayInfoManagerTest {
+    private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
+
+    @Test
+    fun defaultDisplayIsDeviceDisplay_whenOneDisplay() {
+        // Arrange
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+
+        Assume.assumeTrue(displayManager.displays.size == 1)
+
+        val currentDisplaySize = Point()
+        displayManager.displays[0].getRealSize(currentDisplaySize)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(currentDisplaySize, size)
+    }
+
+    @Test
+    fun previewSizeAreaIsWithinMaxPreviewArea() {
+        // Act & Assert
+        val previewSize = displayInfoManager.previewSize
+        assertTrue("$previewSize has larger area than 1920 * 1080",
+            previewSize.width * previewSize.height <= 1920 * 1080)
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
index fc355fe..9b9c238 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombination.kt
@@ -42,7 +42,8 @@
 import androidx.camera.core.impl.SurfaceSizeDefinition
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.utils.AspectRatioUtil
-import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.impl.utils.CameraOrientationUtil
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.internal.utils.SizeUtil
@@ -85,6 +86,8 @@
     internal lateinit var surfaceSizeDefinition: SurfaceSizeDefinition
     private val displayManager: DisplayManager =
         (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
+    private val activeArraySize =
+        cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]
 
     init {
         checkCapabilities()
@@ -583,10 +586,90 @@
     }
 
     /**
-     * Obtains the target aspect ratio from ImageOutputConfig
+     * Returns the aspect ratio group key of the target size when grouping the input resolution
+     * candidate list.
+     *
+     * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
+     * also need to consider the mod 16 factor to find which aspect ratio of group the target size
+     * might be put in. So that sizes of the group will be selected to use in the highest priority.
      */
-    private fun getTargetAspectRatio(imageOutputConfig: ImageOutputConfig): Rational? {
-        val targetSize = getTargetSize(imageOutputConfig)
+    private fun getAspectRatioGroupKeyOfTargetSize(
+        targetSize: Size?,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
+        if (targetSize == null) {
+            return null
+        }
+
+        val aspectRatios = getResolutionListGroupingAspectRatioKeys(
+            resolutionCandidateList
+        )
+        aspectRatios.forEach {
+            if (hasMatchingAspectRatio(targetSize, it)) {
+                return it
+            }
+        }
+        return Rational(targetSize.width, targetSize.height)
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    private fun getResolutionListGroupingAspectRatioKeys(
+        resolutionCandidateList: List<Size>
+    ): List<Rational> {
+        val aspectRatios: MutableList<Rational> = mutableListOf()
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_4_3)
+        aspectRatios.add(AspectRatioUtil.ASPECT_RATIO_16_9)
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        resolutionCandidateList.forEach { size ->
+            val newRatio = Rational(size.width, size.height)
+            var aspectRatioFound = aspectRatios.contains(newRatio)
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                var hasMatchingAspectRatio = false
+                aspectRatios.forEach loop@{ aspectRatio ->
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true
+                        return@loop
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio)
+                }
+            }
+        }
+        return aspectRatios
+    }
+
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by TargetAspectRatio quirk (not implemented yet).
+     * 2. The use case's original aspect ratio if TargetAspectRatio quirk returns RATIO_ORIGINAL
+     * and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if TargetAspectRatio quirk returns
+     * RATIO_ORIGINAL and the use case has no target aspect ratio but has target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private fun getTargetAspectRatio(
+        imageOutputConfig: ImageOutputConfig,
+        resolutionCandidateList: List<Size>
+    ): Rational? {
         var outputRatio: Rational? = null
         // TODO(b/245622117) Get the corrected aspect ratio from quirks instead of always using
         //  TargetAspectRatio.RATIO_ORIGINAL
@@ -603,11 +686,17 @@
                     "Undefined target aspect ratio: $aspectRatio"
                 )
             }
-        } else if (targetSize != null) {
-            // Target size is calculated from the target resolution. If target size is not
-            // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-            // will be selected in priority.
-            outputRatio = Rational(targetSize.width, targetSize.height)
+        } else {
+            // The legacy resolution API will use the aspect ratio of the target size to
+            // be the fallback target aspect ratio value when the use case has no target
+            // aspect ratio setting.
+            val targetSize = getTargetSize(imageOutputConfig)
+            if (targetSize != null) {
+                outputRatio = getAspectRatioGroupKeyOfTargetSize(
+                    targetSize,
+                    resolutionCandidateList
+                )
+            }
         }
         return outputRatio
     }
@@ -656,33 +745,22 @@
      * Groups sizes together according to their aspect ratios.
      */
     private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
-        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = java.util.HashMap()
+        val aspectRatioSizeListMap: MutableMap<Rational, MutableList<Size>> = mutableMapOf()
 
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_4_3] = ArrayList()
-        aspectRatioSizeListMap[AspectRatioUtil.ASPECT_RATIO_16_9] = ArrayList()
-        for (outputSize in sizes) {
-            var matchedKey: Rational? = null
-            for (key in aspectRatioSizeListMap.keys) {
+        val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+        aspectRatioKeys.forEach {
+            aspectRatioSizeListMap[it] = mutableListOf()
+        }
+
+        sizes.forEach { size ->
+            aspectRatioSizeListMap.keys.forEach { aspectRatio ->
                 // Put the size into all groups that is matched in mod16 condition since a size
                 // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key
-                    val sizeList = aspectRatioSizeListMap[matchedKey]!!
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize)
-                    }
+                if (hasMatchingAspectRatio(size, aspectRatio)) {
+                    aspectRatioSizeListMap[aspectRatio]?.add(size)
                 }
             }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap[Rational(outputSize.width, outputSize.height)] =
-                    ArrayList(setOf(outputSize))
-            }
         }
         return aspectRatioSizeListMap
     }
@@ -714,8 +792,8 @@
         // result.
         Arrays.sort(outputSizes, CompareSizesByArea(true))
         var targetSize: Size? = getTargetSize(imageOutputConfig)
-        var minSize = SizeUtil.RESOLUTION_VGA
-        val defaultSizeArea = SizeUtil.getArea(SizeUtil.RESOLUTION_VGA)
+        var minSize = RESOLUTION_VGA
+        val defaultSizeArea = SizeUtil.getArea(RESOLUTION_VGA)
         val maxSizeArea = SizeUtil.getArea(maxSize)
         // When maxSize is smaller than 640x480, set minSize as 0x0. It means the min size bound
         // will be ignored. Otherwise, set the minimal size according to min(DEFAULT_SIZE,
@@ -742,7 +820,8 @@
                     imageFormat
             )
         }
-        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig)
+
+        val aspectRatio: Rational? = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates)
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize ?: imageOutputConfig.getDefaultResolution(null)
@@ -773,9 +852,17 @@
 
             // Sort the aspect ratio key set by the target aspect ratio.
             val aspectRatios: List<Rational?> = ArrayList(aspectRatioSizeListMap.keys)
+            val fullFovRatio = if (activeArraySize != null) {
+                Rational(activeArraySize.width(), activeArraySize.height())
+            } else {
+                null
+            }
             Collections.sort(
                 aspectRatios,
-                CompareAspectRatiosByDistanceToTargetRatio(aspectRatio)
+                CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                    aspectRatio,
+                    fullFovRatio
+                )
             )
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
index 1babffd..c7745f6 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManager.kt
@@ -19,6 +19,9 @@
 import android.content.Context
 import android.graphics.Point
 import android.hardware.display.DisplayManager
+import android.hardware.display.DisplayManager.DisplayListener
+import android.os.Handler
+import android.os.Looper
 import android.util.Size
 import android.view.Display
 import androidx.annotation.RequiresApi
@@ -31,45 +34,86 @@
 class DisplayInfoManager @Inject constructor(context: Context) {
     private val MAX_PREVIEW_SIZE = Size(1920, 1080)
 
+    companion object {
+        private var lazyMaxDisplay: Display? = null
+        private var lazyPreviewSize: Size? = null
+
+        internal fun invalidateLazyFields() {
+            lazyMaxDisplay = null
+            lazyPreviewSize = null
+        }
+
+        internal val displayListener by lazy {
+            object : DisplayListener {
+                override fun onDisplayAdded(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayRemoved(displayId: Int) {
+                    invalidateLazyFields()
+                }
+
+                override fun onDisplayChanged(displayId: Int) {
+                    invalidateLazyFields()
+                }
+            }
+        }
+    }
+
     private val displayManager: DisplayManager by lazy {
-        context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+        (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager).also {
+            it.registerDisplayListener(displayListener, Handler(Looper.getMainLooper()))
+        }
     }
 
-    // TODO(b/198257203): Fetch latest display information for devices where display size is not
-    //  guaranteed to be fixed. (e.g. foldable devices or devices with multiple displays)
+    val defaultDisplay: Display
+        get() = getMaxSizeDisplay()
 
-    val defaultDisplay: Display by lazy {
-        getMaxSizeDisplay()
-    }
-
-    val previewSize: Size by lazy {
-        calculatePreviewSize()
-    }
+    val previewSize: Size
+        get() = calculatePreviewSize()
 
     private fun getMaxSizeDisplay(): Display {
+        lazyMaxDisplay?.let { return it }
+
         val displays = displayManager.displays
+
+        var maxDisplayWhenStateNotOff: Display? = null
+        var maxDisplaySizeWhenStateNotOff = -1
+
         var maxDisplay: Display? = null
         var maxDisplaySize = -1
 
-        // TODO(b/211945950, b/255170076): Handle STATE_OFF displays.
-
         for (display: Display in displays) {
             val displaySize = Point()
             // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
             //  is deprecated since API level 31.
             display.getRealSize(displaySize)
+
             if (displaySize.x * displaySize.y > maxDisplaySize) {
                 maxDisplaySize = displaySize.x * displaySize.y
                 maxDisplay = display
             }
+            if (display.state != Display.STATE_OFF) {
+                if (displaySize.x * displaySize.y > maxDisplaySizeWhenStateNotOff) {
+                    maxDisplaySizeWhenStateNotOff = displaySize.x * displaySize.y
+                    maxDisplayWhenStateNotOff = display
+                }
+            }
         }
-        return checkNotNull(maxDisplay) { "No displays found from ${displayManager.displays}!" }
+
+        lazyMaxDisplay = maxDisplayWhenStateNotOff ?: maxDisplay
+
+        return checkNotNull(lazyMaxDisplay) {
+            "No displays found from ${displayManager.displays}!"
+        }
     }
 
     /**
      * Calculates the device's screen resolution, or MAX_PREVIEW_SIZE, whichever is smaller.
      */
     private fun calculatePreviewSize(): Size {
+        lazyPreviewSize?.let { return it }
+
         val displaySize = Point()
         val display: Display = defaultDisplay
         // TODO(b/230400472): Use WindowManager#getCurrentWindowMetrics(). Display#getRealSize()
@@ -87,6 +131,7 @@
             displayViewSize = MAX_PREVIEW_SIZE
         }
         // TODO(b/230402463): Migrate extra cropping quirk from CameraX.
-        return displayViewSize
+
+        return displayViewSize.also { lazyPreviewSize = displayViewSize }
     }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 684eb88..47dc650 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -219,6 +219,8 @@
 
         override fun setZslDisabled(disabled: Boolean) = this
 
+        override fun setHighResolutionDisabled(disabled: Boolean) = this
+
         override fun build(): MeteringRepeating {
             return MeteringRepeating(cameraProperties, useCaseConfig, displayInfoManager)
         }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
index 84788a9..2249cca 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/SupportedSurfaceCombinationTest.kt
@@ -697,10 +697,10 @@
             mockCamcorderProfileAdapter
         )
 
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val targetResolution = Size(1200, 720)
+        val targetResolution = Size(1280, 640)
         val imageCapture = ImageCapture.Builder().setTargetResolution(
             targetResolution
         ).setTargetRotation(Surface.ROTATION_90).build()
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
index f550d39..1d12d9b 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/DisplayInfoManagerTest.kt
@@ -20,22 +20,57 @@
 import android.graphics.Point
 import android.hardware.display.DisplayManager
 import android.util.Size
+import android.view.Display
+import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowDisplay
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 
 @Suppress("DEPRECATION") // getRealSize
-@RunWith(RobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 class DisplayInfoManagerTest {
     private val displayInfoManager = DisplayInfoManager(ApplicationProvider.getApplicationContext())
 
-    private fun addDisplay(width: Int, height: Int) {
-        ShadowDisplayManager.addDisplay(String.format("w%ddp-h%ddp", width, height))
+    private fun addDisplay(width: Int, height: Int, state: Int = Display.STATE_ON): Int {
+        val displayStr = String.format("w%ddp-h%ddp", width, height)
+        val displayId = ShadowDisplayManager.addDisplay(displayStr)
+
+        if (state != Display.STATE_ON) {
+            val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+                .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
+            (Shadow.extract(displayManager.getDisplay(displayId)) as ShadowDisplay).setState(state)
+        }
+
+        return displayId
+    }
+
+    companion object {
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
+    }
+
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
     }
 
     @Test
@@ -68,10 +103,89 @@
         assertEquals(Point(2000, 3000), size)
     }
 
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenPreviousMaxDisplayRemoved() {
+        // Arrange
+        val id = addDisplay(2000, 3000)
+        addDisplay(480, 640)
+        removeDisplay(id)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeDisplay_whenNewMaxDisplayAddedAfterGettingPrevious() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.defaultDisplay
+        addDisplay(2000, 3000)
+
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithSomeOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640)
+        addDisplay(240, 320)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInNotOffState_whenMultipleDisplayWithNoOnState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_UNKNOWN)
+        addDisplay(240, 320, Display.STATE_UNKNOWN)
+        addDisplay(200, 300, Display.STATE_OFF)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(480, 640), size)
+    }
+
+    @Test
+    fun defaultDisplayIsMaxSizeInOffState_whenMultipleDisplayWithAllOffState() {
+        // Arrange
+        addDisplay(2000, 3000, Display.STATE_OFF)
+        addDisplay(480, 640, Display.STATE_OFF)
+        addDisplay(200, 300, Display.STATE_OFF)
+        removeDisplay(0)
+
+        // Act
+        val size = Point()
+        displayInfoManager.defaultDisplay.getRealSize(size)
+
+        // Assert
+        assertEquals(Point(2000, 3000), size)
+    }
+
     @Test(expected = IllegalStateException::class)
     fun throwsCorrectExceptionForDefaultDisplay_whenNoDisplay() {
         // Arrange
-        ShadowDisplayManager.removeDisplay(0)
+        removeDisplay(0)
 
         // Act
         val size = Point()
@@ -95,4 +209,17 @@
         // Act & Assert
         assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
     }
+
+    @Test
+    fun previewSizeIsUpdated_whenNewDisplayAddedAfterPreviousUse() {
+        // Arrange
+        addDisplay(480, 640)
+
+        // Act
+        displayInfoManager.previewSize
+        addDisplay(2000, 3000)
+
+        // Assert
+        assertEquals(Size(1920, 1080), displayInfoManager.previewSize)
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
index f40a4cf..cc035a9 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeatingTest.kt
@@ -16,19 +16,24 @@
 
 package androidx.camera.camera2.pipe.integration.impl
 
+import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
+import android.hardware.display.DisplayManager
 import android.os.Build
 import android.util.Size
 import androidx.camera.camera2.pipe.integration.adapter.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.integration.testing.FakeCameraProperties
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import androidx.test.core.app.ApplicationProvider
+import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.BeforeClass
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 import org.robolectric.shadows.ShadowDisplayManager
+import org.robolectric.shadows.ShadowDisplayManager.removeDisplay
 import org.robolectric.shadows.StreamConfigurationMapBuilder
 
 @RunWith(RobolectricCameraPipeTestRunner::class)
@@ -80,6 +85,12 @@
                 )
             )
         }
+
+        @JvmStatic
+        @BeforeClass
+        fun classSetUp() {
+            DisplayInfoManager.invalidateLazyFields()
+        }
     }
 
     private lateinit var meteringRepeating: MeteringRepeating
@@ -103,6 +114,18 @@
         ).build()
     }
 
+    @After
+    fun tearDown() {
+        val displayManager = (ApplicationProvider.getApplicationContext() as Context)
+            .getSystemService(Context.DISPLAY_SERVICE) as DisplayManager?
+
+        displayManager?.let {
+            for (display in it.displays) {
+                removeDisplay(display.displayId)
+            }
+        }
+    }
+
     @Test
     fun attachedSurfaceResolutionIsLargestLessThan640x480_when640x480NotPresentInOutputSizes() {
         meteringRepeating = getMeteringRepeatingAndInitDisplay(dummySizeListWithout640x480)
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 6ebf92f..f9affe9 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
@@ -52,6 +52,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.InitializationException;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
@@ -651,13 +652,17 @@
     private UseCase createUseCase(int template, boolean isZslDisabled) {
         FakeUseCaseConfig.Builder configBuilder =
                 new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
-                        new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
+                                new Camera2SessionOptionUnpacker()).setTargetName("UseCase")
                         .setZslDisabled(isZslDisabled);
         new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
+        return createUseCase(configBuilder.getUseCaseConfig(), template);
+    }
+
+    private UseCase createUseCase(@NonNull FakeUseCaseConfig config, int template) {
         CameraSelector selector =
                 new CameraSelector.Builder().requireLensFacing(
                         CameraSelector.LENS_FACING_BACK).build();
-        TestUseCase testUseCase = new TestUseCase(template, configBuilder.getUseCaseConfig(),
+        TestUseCase testUseCase = new TestUseCase(template, config,
                 selector, mMockOnImageAvailableListener, mMockRepeatingCaptureCallback);
         testUseCase.updateSuggestedResolution(new Size(640, 480));
         mFakeUseCases.add(testUseCase);
@@ -934,6 +939,38 @@
                 .isFalse();
     }
 
+    @SdkSuppress(minSdkVersion = 23)
+    @Test
+    public void zslDisabled_whenHighResolutionIsEnabled() throws InterruptedException {
+        UseCase zsl = createUseCase(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG,
+                /* isZslDisabled = */false);
+
+        // Creates a test use case with high resolution enabled.
+        ResolutionSelector highResolutionSelector =
+                new ResolutionSelector.Builder().setHighResolutionEnabled(true).build();
+        FakeUseCaseConfig.Builder configBuilder =
+                new FakeUseCaseConfig.Builder().setSessionOptionUnpacker(
+                        new Camera2SessionOptionUnpacker()).setTargetName(
+                        "UseCase").setResolutionSelector(highResolutionSelector);
+        new Camera2Interop.Extender<>(configBuilder).setSessionStateCallback(mSessionStateCallback);
+        UseCase highResolutionUseCase = createUseCase(configBuilder.getUseCaseConfig(),
+                CameraDevice.TEMPLATE_PREVIEW);
+
+        // Checks zsl is disabled after UseCase#onAttach() is called to merge/update config.
+        assertThat(highResolutionUseCase.getCurrentConfig().isZslDisabled(false)).isTrue();
+
+        if (!mCamera2CameraImpl.getCameraInfo().isZslSupported()) {
+            return;
+        }
+
+        mCamera2CameraImpl.attachUseCases(Arrays.asList(zsl, highResolutionUseCase));
+        mCamera2CameraImpl.onUseCaseActive(zsl);
+        mCamera2CameraImpl.onUseCaseActive(highResolutionUseCase);
+        HandlerUtil.waitForLooperToIdle(sCameraHandler);
+        assertThat(mCamera2CameraImpl.getCameraControlInternal().isZslDisabledByByUserCaseConfig())
+                .isTrue();
+    }
+
     private DeferrableSurface getUseCaseSurface(UseCase useCase) {
         return useCase.getSessionConfig().getSurfaces().get(0);
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
new file mode 100644
index 0000000..02e0614
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedOutputSizesCollector.java
@@ -0,0 +1,737 @@
+/*
+ * 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.camera2.internal;
+
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
+
+import android.graphics.ImageFormat;
+import android.graphics.Rect;
+import android.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.params.StreamConfigurationMap;
+import android.os.Build;
+import android.util.Rational;
+import android.util.Size;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.workaround.ExcludedSupportedSizesContainer;
+import androidx.camera.camera2.internal.compat.workaround.ResolutionCorrector;
+import androidx.camera.camera2.internal.compat.workaround.TargetAspectRatio;
+import androidx.camera.core.AspectRatio;
+import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
+import androidx.camera.core.impl.ImageFormatConstants;
+import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.SizeCoordinate;
+import androidx.camera.core.impl.SurfaceConfig;
+import androidx.camera.core.impl.utils.AspectRatioUtil;
+import androidx.camera.core.impl.utils.CameraOrientationUtil;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+import androidx.camera.core.internal.utils.SizeUtil;
+import androidx.core.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The supported output sizes collector to help collect the available resolution candidate list
+ * according to the use case config and the following settings in {@link ResolutionSelector}:
+ *
+ * 1. Preferred aspect ratio
+ * 2. Preferred resolution
+ * 3. Max resolution
+ * 4. Is high resolution enabled
+ *
+ * The problematic resolutions retrieved from {@link ExcludedSupportedSizesContainer} will also
+ * be execulded.
+ */
+@RequiresApi(21)
+final class SupportedOutputSizesCollector {
+    private static final String TAG = "SupportedOutputSizesCollector";
+    private final String mCameraId;
+    @NonNull
+    private final CameraCharacteristicsCompat mCharacteristics;
+    @NonNull
+    private final DisplayInfoManager mDisplayInfoManager;
+    private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
+    private final Map<Integer, Size[]> mOutputSizesCache = new HashMap<>();
+    private final Map<Integer, Size[]> mHighResolutionOutputSizesCache = new HashMap<>();
+    private final Map<Integer, Size> mMaxSizeCache = new HashMap<>();
+    private final ExcludedSupportedSizesContainer mExcludedSupportedSizesContainer;
+    private final Map<Integer, List<Size>> mExcludedSizeListCache = new HashMap<>();
+    private final boolean mIsSensorLandscapeResolution;
+    private final boolean mIsBurstCaptureSupported;
+    private final Size mActiveArraySize;
+    private final int mSensorOrientation;
+    private final int mLensFacing;
+
+    SupportedOutputSizesCollector(@NonNull String cameraId,
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics,
+            @NonNull DisplayInfoManager displayInfoManager) {
+        mCameraId = cameraId;
+        mCharacteristics = cameraCharacteristics;
+        mDisplayInfoManager = displayInfoManager;
+
+        mExcludedSupportedSizesContainer = new ExcludedSupportedSizesContainer(cameraId);
+
+        mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
+        mIsBurstCaptureSupported = isBurstCaptureSupported();
+
+        Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
+
+        mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+        mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
+    }
+
+    /**
+     * Collects and sorts the resolution candidate list by the following steps:
+     *
+     * 1. Collects the candidate list by the high resolution enable setting.
+     * 2. Filters out the candidate list according to the min size bound, max resolution or
+     * excluded resolution quirk.
+     * 3. Sorts the candidate list according to the rules of legacy resolution API or new
+     * Resolution API.
+     * 4. Forces select specific resolutions according to ResolutionCorrector workaround.
+     */
+    @NonNull
+    List<Size> getSupportedOutputSizes(@NonNull ResolutionSelector resolutionSelector,
+            int imageFormat, @Nullable Size miniBoundingSize, boolean isHighResolutionDisabled,
+            @Nullable Size[] customizedSupportSizes) {
+        // 1. Collects the candidate list by the high resolution enable setting.
+        List<Size> resolutionCandidateList = collectResolutionCandidateList(resolutionSelector,
+                imageFormat, isHighResolutionDisabled, customizedSupportSizes);
+
+        // 2. Filters out the candidate list according to the min size bound, max resolution or
+        // excluded resolution quirk.
+        resolutionCandidateList = filterOutResolutionCandidateListBySettings(
+                resolutionCandidateList, resolutionSelector, imageFormat);
+
+        // 3. Sorts the candidate list according to the rules of new Resolution API.
+        resolutionCandidateList = sortResolutionCandidateListByResolutionSelector(
+                resolutionCandidateList, resolutionSelector,
+                mDisplayInfoManager.getMaxSizeDisplay().getRotation(), miniBoundingSize);
+
+        // 4. Forces select specific resolutions according to ResolutionCorrector workaround.
+        resolutionCandidateList = mResolutionCorrector.insertOrPrioritize(
+                SurfaceConfig.getConfigType(imageFormat), resolutionCandidateList);
+
+        return resolutionCandidateList;
+    }
+
+    /**
+     * Collects the resolution candidate list.
+     *
+     * 1. Customized supported resolutions list will be returned when it exists
+     * 2. Otherwise, the sizes retrieved from {@link StreamConfigurationMap#getOutputSizes(int)}
+     * will be the base of the resolution candidate list.
+     * 3. High resolution sizes retrieved from
+     * {@link StreamConfigurationMap#getHighResolutionOutputSizes(int)} will be included when
+     * {@link ResolutionSelector#isHighResolutionEnabled()} returns true.
+     *
+     * The returned list will be sorted in descending order and duplicate items will be removed.
+     */
+    @NonNull
+    private List<Size> collectResolutionCandidateList(
+            @NonNull ResolutionSelector resolutionSelector, int imageFormat,
+            boolean isHighResolutionDisabled, @Nullable Size[] customizedSupportedSizes) {
+        Size[] outputSizes = customizedSupportedSizes;
+
+        if (outputSizes == null) {
+            boolean highResolutionEnabled =
+                    !isHighResolutionDisabled && resolutionSelector.isHighResolutionEnabled();
+            outputSizes = getAllOutputSizesByFormat(imageFormat, highResolutionEnabled);
+        }
+
+        // Sort the output sizes. The Comparator result must be reversed to have a descending order
+        // result.
+        Arrays.sort(outputSizes, new CompareSizesByArea(true));
+
+        // Removes the duplicate items
+        List<Size> resultList = new ArrayList<>();
+        for (Size size: outputSizes) {
+            if (!resultList.contains(size)) {
+                resultList.add(size);
+            }
+        }
+
+        if (resultList.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Resolution candidate list is empty when collecting by the settings!");
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Filters out the resolution candidate list by the max resolution setting.
+     *
+     * The input size list should have been sorted in descending order.
+     */
+    private List<Size> filterOutResolutionCandidateListBySettings(
+            @NonNull List<Size> resolutionCandidateList,
+            @NonNull ResolutionSelector resolutionSelector, int imageFormat) {
+        // Retrieves the max resolution setting. When ResolutionSelector is used, all resolution
+        // selection logic should depend on ResolutionSelector's settings.
+        Size maxResolution = resolutionSelector.getMaxResolution();
+
+        // Filter out the resolution candidate list by the max resolution. Sizes that any edge
+        // exceeds the max resolution will be filtered out.
+        List<Size> resultList;
+
+        if (maxResolution == null) {
+            resultList = new ArrayList<>(resolutionCandidateList);
+        } else {
+            resultList = new ArrayList<>();
+            for (Size outputSize : resolutionCandidateList) {
+                if (!SizeUtil.isLongerInAnyEdge(outputSize, maxResolution)) {
+                    resultList.add(outputSize);
+                }
+            }
+        }
+
+        resultList = excludeProblematicSizes(resultList, imageFormat);
+
+        if (resultList.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Resolution candidate list is empty after filtering out by the settings!");
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Sorts the resolution candidate list according to the new ResolutionSelector API logic.
+     *
+     * The list will be sorted by the following order:
+     * 1. size of preferred resolution
+     * 2. a resolution with preferred aspect ratio, is not smaller than, and is closest to the
+     * preferred resolution.
+     * 3. resolutions with preferred aspect ratio and is smaller than the preferred resolution
+     * size in descending order of resolution area size.
+     * 4. Other sizes sorted by CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace and
+     * area size.
+     */
+    @NonNull
+    private List<Size> sortResolutionCandidateListByResolutionSelector(
+            @NonNull List<Size> resolutionCandidateList,
+            @NonNull ResolutionSelector resolutionSelector,
+            @ImageOutputConfig.RotationValue int targetRotation,
+            @Nullable Size miniBoundingSize) {
+        Rational aspectRatio = getTargetAspectRatioByResolutionSelector(resolutionSelector);
+        Preconditions.checkNotNull(aspectRatio, "ResolutionSelector should also have aspect ratio"
+                + " value.");
+
+        Size targetSize = getTargetSizeByResolutionSelector(resolutionSelector, targetRotation,
+                mSensorOrientation, mLensFacing);
+        List<Size> resultList = sortResolutionCandidateListByTargetAspectRatioAndSize(
+                resolutionCandidateList, aspectRatio, miniBoundingSize);
+
+        // Moves the target size to the first position if it exists in the resolution candidate
+        // list and there is no quirk that needs to select specific aspect ratio sizes in priority.
+        if (resultList.contains(targetSize) && canResolutionBeMovedToHead(targetSize)) {
+            resultList.remove(targetSize);
+            resultList.add(0, targetSize);
+        }
+
+        return resultList;
+    }
+
+    @NonNull
+    private Size[] getAllOutputSizesByFormat(int imageFormat, boolean highResolutionEnabled) {
+        Size[] outputs = mOutputSizesCache.get(imageFormat);
+        if (outputs == null) {
+            outputs = doGetOutputSizesByFormat(imageFormat);
+            mOutputSizesCache.put(imageFormat, outputs);
+        }
+
+        Size[] highResolutionOutputs = null;
+
+        // A device that does not support the BURST_CAPTURE capability,
+        // StreamConfigurationMap#getHighResolutionOutputSizes() will return null.
+        if (highResolutionEnabled && mIsBurstCaptureSupported) {
+            highResolutionOutputs = mHighResolutionOutputSizesCache.get(imageFormat);
+
+            // High resolution output sizes list may be empty. If it is empty and cached in the
+            // map, don't need to query it again.
+            if (highResolutionOutputs == null && !mHighResolutionOutputSizesCache.containsKey(
+                    imageFormat)) {
+                highResolutionOutputs = doGetHighResolutionOutputSizesByFormat(imageFormat);
+                mHighResolutionOutputSizesCache.put(imageFormat, highResolutionOutputs);
+            }
+        }
+
+        // Combines output sizes if high resolution sizes list is not empty.
+        if (highResolutionOutputs != null) {
+            Size[] allOutputs = Arrays.copyOf(highResolutionOutputs,
+                    highResolutionOutputs.length + outputs.length);
+            System.arraycopy(outputs, 0, allOutputs, highResolutionOutputs.length, outputs.length);
+            outputs = allOutputs;
+        }
+
+        return outputs;
+    }
+
+    @NonNull
+    private Size[] doGetOutputSizesByFormat(int imageFormat) {
+        Size[] outputSizes;
+
+        StreamConfigurationMap map =
+                mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+        if (map == null) {
+            throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
+        }
+
+        if (Build.VERSION.SDK_INT < 23
+                && imageFormat == ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE) {
+            // This is a little tricky that 0x22 that is internal defined in
+            // StreamConfigurationMap.java to be equal to ImageFormat.PRIVATE that is public
+            // after Android level 23 but not public in Android L. Use {@link SurfaceTexture}
+            // or {@link MediaCodec} will finally mapped to 0x22 in StreamConfigurationMap to
+            // retrieve the output sizes information.
+            outputSizes = map.getOutputSizes(SurfaceTexture.class);
+        } else {
+            outputSizes = map.getOutputSizes(imageFormat);
+        }
+
+        if (outputSizes == null) {
+            throw new IllegalArgumentException(
+                    "Can not get supported output size for the format: " + imageFormat);
+        }
+
+        return outputSizes;
+    }
+
+    @Nullable
+    private Size[] doGetHighResolutionOutputSizesByFormat(int imageFormat) {
+        if (Build.VERSION.SDK_INT < 23) {
+            return null;
+        }
+
+        Size[] outputSizes;
+
+        StreamConfigurationMap map =
+                mCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
+
+        if (map == null) {
+            throw new IllegalArgumentException("Can not retrieve SCALER_STREAM_CONFIGURATION_MAP");
+        }
+
+        outputSizes = Api23Impl.getHighResolutionOutputSizes(map, imageFormat);
+
+        return outputSizes;
+    }
+
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by {@link TargetAspectRatio} if it is
+     * {@link TargetAspectRatio#RATIO_4_3}, {@link TargetAspectRatio#RATIO_16_9} or
+     * {@link TargetAspectRatio#RATIO_MAX_JPEG}.
+     * 2. The use case's original aspect ratio if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has target aspect ratio setting.
+     *
+     * @param resolutionSelector the resolution selector of the use case.
+     */
+    @Nullable
+    private Rational getTargetAspectRatioByResolutionSelector(
+            @NonNull ResolutionSelector resolutionSelector) {
+        Rational outputRatio = getTargetAspectRatioFromQuirk();
+
+        if (outputRatio == null) {
+            @AspectRatio.Ratio int aspectRatio = resolutionSelector.getPreferredAspectRatio();
+            switch (aspectRatio) {
+                case AspectRatio.RATIO_4_3:
+                    outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3
+                            : ASPECT_RATIO_3_4;
+                    break;
+                case AspectRatio.RATIO_16_9:
+                    outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9
+                            : ASPECT_RATIO_9_16;
+                    break;
+                default:
+                    Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
+            }
+        }
+        return outputRatio;
+    }
+
+    /**
+     * Returns the restricted target aspect ratio value from quirk. The returned value can be
+     * null which means that no quirk to restrict the use case to use a specific target aspect
+     * ratio value.
+     */
+    @Nullable
+    private Rational getTargetAspectRatioFromQuirk() {
+        Rational outputRatio = null;
+
+        // Gets the corrected aspect ratio due to device constraints or null if no correction is
+        // needed.
+        @TargetAspectRatio.Ratio int targetAspectRatio =
+                new TargetAspectRatio().get(mCameraId, mCharacteristics);
+        switch (targetAspectRatio) {
+            case TargetAspectRatio.RATIO_4_3:
+                outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_4_3 : ASPECT_RATIO_3_4;
+                break;
+            case TargetAspectRatio.RATIO_16_9:
+                outputRatio = mIsSensorLandscapeResolution ? ASPECT_RATIO_16_9 : ASPECT_RATIO_9_16;
+                break;
+            case TargetAspectRatio.RATIO_MAX_JPEG:
+                Size maxJpegSize = fetchMaxNormalOutputSize(ImageFormat.JPEG);
+                outputRatio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
+                break;
+            case TargetAspectRatio.RATIO_ORIGINAL:
+                break;
+        }
+
+        return outputRatio;
+    }
+
+    @Nullable
+    static Size getTargetSizeByResolutionSelector(@NonNull ResolutionSelector resolutionSelector,
+            int targetRotation, int sensorOrientation, int lensFacing) {
+        Size targetSize = resolutionSelector.getPreferredResolution();
+
+        // Calibrate targetSize by the target rotation value if it is set by the Android View
+        // coordinate orientation.
+        if (resolutionSelector.getSizeCoordinate() == SizeCoordinate.ANDROID_VIEW) {
+            targetSize = flipSizeByRotation(targetSize, targetRotation, lensFacing,
+                    sensorOrientation);
+        }
+        return targetSize;
+    }
+
+    private static boolean isRotationNeeded(int targetRotation, int lensFacing,
+            int sensorOrientation) {
+        int relativeRotationDegrees =
+                CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
+
+        // Currently this assumes that a back-facing camera is always opposite to the screen.
+        // This may not be the case for all devices, so in the future we may need to handle that
+        // scenario.
+        boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
+
+        int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
+                relativeRotationDegrees,
+                sensorOrientation,
+                isOppositeFacingScreen);
+        return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
+    }
+
+    @NonNull
+    private List<Size> excludeProblematicSizes(@NonNull List<Size> resolutionCandidateList,
+            int imageFormat) {
+        List<Size> excludedSizes = fetchExcludedSizes(imageFormat);
+        resolutionCandidateList.removeAll(excludedSizes);
+        return resolutionCandidateList;
+    }
+
+    @NonNull
+    private List<Size> fetchExcludedSizes(int imageFormat) {
+        List<Size> excludedSizes = mExcludedSizeListCache.get(imageFormat);
+
+        if (excludedSizes == null) {
+            excludedSizes = mExcludedSupportedSizesContainer.get(imageFormat);
+            mExcludedSizeListCache.put(imageFormat, excludedSizes);
+        }
+
+        return excludedSizes;
+    }
+
+    /**
+     * Sorts the resolution candidate list according to the target aspect ratio and size settings.
+     *
+     * 1. The resolution candidate list will be grouped by aspect ratio.
+     * 2. Each group only keeps one size which is not smaller than the target size.
+     * 3. The aspect ratios of groups will be sorted against to the target aspect ratio setting by
+     * CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace.
+     * 4. Concatenate all sizes as the result list
+     */
+    @NonNull
+    private List<Size> sortResolutionCandidateListByTargetAspectRatioAndSize(
+            @NonNull List<Size> resolutionCandidateList, @NonNull Rational aspectRatio,
+            @Nullable Size miniBoundingSize) {
+        // Rearrange the supported size to put the ones with the same aspect ratio in the front
+        // of the list and put others in the end from large to small. Some low end devices may
+        // not able to get an supported resolution that match the preferred aspect ratio.
+
+        // Group output sizes by aspect ratio.
+        Map<Rational, List<Size>> aspectRatioSizeListMap =
+                groupSizesByAspectRatio(resolutionCandidateList);
+
+        // If the target resolution is set, use it to remove unnecessary larger sizes.
+        if (miniBoundingSize != null) {
+            // Remove unnecessary larger sizes from each aspect ratio size list
+            for (Rational key : aspectRatioSizeListMap.keySet()) {
+                removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
+                        miniBoundingSize);
+            }
+        }
+
+        // Sort the aspect ratio key set by the target aspect ratio.
+        List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+        Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+                mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
+        Collections.sort(aspectRatios,
+                new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                        aspectRatio, fullFovRatio));
+
+        List<Size> resultList = new ArrayList<>();
+
+        // Put available sizes into final result list by aspect ratio distance to target ratio.
+        for (Rational rational : aspectRatios) {
+            for (Size size : aspectRatioSizeListMap.get(rational)) {
+                // A size may exist in multiple groups in mod16 condition. Keep only one in
+                // the final list.
+                if (!resultList.contains(size)) {
+                    resultList.add(size);
+                }
+            }
+        }
+
+        return resultList;
+    }
+
+    /**
+     * Returns {@code true} if the input resolution can be moved to the head of resolution
+     * candidate list.
+     *
+     * The resolution possibly can't be moved to head due to some quirks that sizes of
+     * specific aspect ratio must be used to avoid problems.
+     */
+    private boolean canResolutionBeMovedToHead(@NonNull Size resolution) {
+        @TargetAspectRatio.Ratio int targetAspectRatio =
+                new TargetAspectRatio().get(mCameraId, mCharacteristics);
+
+        switch (targetAspectRatio) {
+            case TargetAspectRatio.RATIO_4_3:
+                return hasMatchingAspectRatio(resolution, ASPECT_RATIO_4_3);
+            case TargetAspectRatio.RATIO_16_9:
+                return hasMatchingAspectRatio(resolution, ASPECT_RATIO_16_9);
+            case TargetAspectRatio.RATIO_MAX_JPEG:
+                Size maxJpegSize = fetchMaxNormalOutputSize(ImageFormat.JPEG);
+                Rational maxJpegRatio = new Rational(maxJpegSize.getWidth(),
+                        maxJpegSize.getHeight());
+                return hasMatchingAspectRatio(resolution, maxJpegRatio);
+        }
+
+        return true;
+    }
+
+    private Size fetchMaxNormalOutputSize(int imageFormat) {
+        Size size = mMaxSizeCache.get(imageFormat);
+        if (size != null) {
+            return size;
+        }
+        Size maxSize = getMaxNormalOutputSizeByFormat(imageFormat);
+        mMaxSizeCache.put(imageFormat, maxSize);
+        return maxSize;
+    }
+
+    /**
+     * Gets max normal supported output size for specific image format.
+     *
+     * <p>Normal supported output sizes mean the sizes retrieved by the
+     * {@link StreamConfigurationMap#getOutputSizes(int)}. The high resolution sizes retrieved by
+     * the {@link StreamConfigurationMap#getHighResolutionOutputSizes(int)} are not included.
+     *
+     * @param imageFormat the image format info
+     * @return the max normal supported output size for the image format
+     */
+    private Size getMaxNormalOutputSizeByFormat(int imageFormat) {
+        Size[] outputSizes = getAllOutputSizesByFormat(imageFormat, false);
+
+        return SizeUtil.getMaxSize(Arrays.asList(outputSizes));
+    }
+
+    private boolean isBurstCaptureSupported() {
+        int[] availableCapabilities =
+                mCharacteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
+
+        if (availableCapabilities != null) {
+            for (int capability : availableCapabilities) {
+                if (capability
+                        == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////
+    // The following functions can be reused by the legacy resolution selection logic. The can be
+    // changed as private function after the legacy resolution API is completely removed.
+    //////////////////////////////////////////////////////////////////////////////////////////
+
+    // Use target rotation to calibrate the size.
+    @Nullable
+    static Size flipSizeByRotation(@Nullable Size size, int targetRotation, int lensFacing,
+            int sensorOrientation) {
+        Size outputSize = size;
+        // Calibrates the size with the display and sensor rotation degrees values.
+        if (size != null && isRotationNeeded(targetRotation, lensFacing, sensorOrientation)) {
+            outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
+        }
+        return outputSize;
+    }
+
+    static Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
+        Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
+
+        List<Rational> aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes);
+
+        for (Rational aspectRatio: aspectRatioKeys) {
+            aspectRatioSizeListMap.put(aspectRatio, new ArrayList<>());
+        }
+
+        for (Size outputSize : sizes) {
+            for (Rational key : aspectRatioSizeListMap.keySet()) {
+                // Put the size into all groups that is matched in mod16 condition since a size
+                // may match multiple aspect ratio in mod16 algorithm.
+                if (hasMatchingAspectRatio(outputSize, key)) {
+                    aspectRatioSizeListMap.get(key).add(outputSize);
+                }
+            }
+        }
+
+        return aspectRatioSizeListMap;
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * <p>Some sizes might be mod16 case. When grouping, those sizes will be grouped into an
+     * existing aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    @NonNull
+    static List<Rational> getResolutionListGroupingAspectRatioKeys(
+            @NonNull List<Size> resolutionCandidateList) {
+        List<Rational> aspectRatios = new ArrayList<>();
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(ASPECT_RATIO_4_3);
+        aspectRatios.add(ASPECT_RATIO_16_9);
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        for (Size size : resolutionCandidateList) {
+            Rational newRatio = new Rational(size.getWidth(), size.getHeight());
+            boolean aspectRatioFound = aspectRatios.contains(newRatio);
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                boolean hasMatchingAspectRatio = false;
+                for (Rational aspectRatio : aspectRatios) {
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true;
+                        break;
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio);
+                }
+            }
+        }
+
+        return aspectRatios;
+    }
+
+    /**
+     * Removes unnecessary sizes by target size.
+     *
+     * <p>If the target resolution is set, a size that is equal to or closest to the target
+     * resolution will be selected. If the list includes more than one size equal to or larger
+     * than the target resolution, only one closest size needs to be kept. The other larger sizes
+     * can be removed so that they won't be selected to use.
+     *
+     * @param supportedSizesList The list should have been sorted in descending order.
+     * @param miniBoundingSize The target size used to remove unnecessary sizes.
+     */
+    static void removeSupportedSizesByMiniBoundingSize(List<Size> supportedSizesList,
+            Size miniBoundingSize) {
+        if (supportedSizesList == null || supportedSizesList.isEmpty()) {
+            return;
+        }
+
+        int indexBigEnough = -1;
+        List<Size> removeSizes = new ArrayList<>();
+
+        // Get the index of the item that is equal to or closest to the target size.
+        for (int i = 0; i < supportedSizesList.size(); i++) {
+            Size outputSize = supportedSizesList.get(i);
+            if (outputSize.getWidth() >= miniBoundingSize.getWidth()
+                    && outputSize.getHeight() >= miniBoundingSize.getHeight()) {
+                // New big enough item closer to the target size is found. Adding the previous
+                // one into the sizes list that will be removed.
+                if (indexBigEnough >= 0) {
+                    removeSizes.add(supportedSizesList.get(indexBigEnough));
+                }
+
+                indexBigEnough = i;
+            } else {
+                break;
+            }
+        }
+
+        // Remove the unnecessary items that are larger than the item closest to the target size.
+        supportedSizesList.removeAll(removeSizes);
+    }
+
+    static boolean isSensorLandscapeResolution(
+            @NonNull CameraCharacteristicsCompat characteristicsCompat) {
+        Size pixelArraySize =
+                characteristicsCompat.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
+
+        // Make the default value is true since usually the sensor resolution is landscape.
+        return pixelArraySize == null || pixelArraySize.getWidth() >= pixelArraySize.getHeight();
+    }
+
+    //////////////////////////////////////////////////////////////////////////////////////////
+    // The above functions can be reused by the legacy resolution selection logic. The can be
+    // changed as private function after the legacy resolution API is completely removed.
+    //////////////////////////////////////////////////////////////////////////////////////////
+
+    @RequiresApi(23)
+    private static class Api23Impl {
+        private Api23Impl() {
+            // This class is not instantiable.
+        }
+
+        @DoNotInline
+        static Size[] getHighResolutionOutputSizes(StreamConfigurationMap streamConfigurationMap,
+                int format) {
+            return streamConfigurationMap.getHighResolutionOutputSizes(format);
+        }
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
index 721b2ddf..25bf0f8 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/SupportedSurfaceCombination.java
@@ -16,10 +16,17 @@
 
 package androidx.camera.camera2.internal;
 
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.flipSizeByRotation;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getResolutionListGroupingAspectRatioKeys;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.getTargetSizeByResolutionSelector;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.groupSizesByAspectRatio;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.isSensorLandscapeResolution;
+import static androidx.camera.camera2.internal.SupportedOutputSizesCollector.removeSupportedSizesByMiniBoundingSize;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_3_4;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3;
 import static androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_9_16;
+import static androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_480P;
 import static androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA;
@@ -28,6 +35,7 @@
 
 import android.content.Context;
 import android.graphics.ImageFormat;
+import android.graphics.Rect;
 import android.graphics.SurfaceTexture;
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.params.StreamConfigurationMap;
@@ -53,6 +61,7 @@
 import androidx.camera.core.AspectRatio;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.impl.AttachedSurfaceInfo;
 import androidx.camera.core.impl.ImageFormatConstants;
 import androidx.camera.core.impl.ImageOutputConfig;
@@ -61,7 +70,6 @@
 import androidx.camera.core.impl.SurfaceSizeDefinition;
 import androidx.camera.core.impl.UseCaseConfig;
 import androidx.camera.core.impl.utils.AspectRatioUtil;
-import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.camera.core.impl.utils.CompareSizesByArea;
 import androidx.core.util.Preconditions;
 
@@ -103,6 +111,10 @@
     @NonNull
     private final DisplayInfoManager mDisplayInfoManager;
     private final ResolutionCorrector mResolutionCorrector = new ResolutionCorrector();
+    private final Size mActiveArraySize;
+    private final int mSensorOrientation;
+    private final int mLensFacing;
+    private final SupportedOutputSizesCollector mSupportedOutputSizesCollector;
 
     SupportedSurfaceCombination(@NonNull Context context, @NonNull String cameraId,
             @NonNull CameraManagerCompat cameraManagerCompat,
@@ -121,7 +133,7 @@
                     CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
             mHardwareLevel = keyValue != null ? keyValue
                     : CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
-            mIsSensorLandscapeResolution = isSensorLandscapeResolution();
+            mIsSensorLandscapeResolution = isSensorLandscapeResolution(mCharacteristics);
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
@@ -140,9 +152,18 @@
             }
         }
 
+        Rect rect = mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
+        mActiveArraySize = rect != null ? new Size(rect.width(), rect.height()) : null;
+
+        mSensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
+        mLensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
+
         generateSupportedCombinationList();
         generateSurfaceSizeDefinition();
         checkCustomization();
+
+        mSupportedOutputSizesCollector = new SupportedOutputSizesCollector(mCameraId,
+                mCharacteristics, mDisplayInfoManager);
     }
 
     String getCameraId() {
@@ -289,7 +310,26 @@
         return suggestedResolutionsMap;
     }
 
-    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig) {
+    /**
+     * Returns the target aspect ratio value corrected by quirks.
+     *
+     * The final aspect ratio is determined by the following order:
+     * 1. The aspect ratio returned by {@link TargetAspectRatio} if it is
+     * {@link TargetAspectRatio#RATIO_4_3}, {@link TargetAspectRatio#RATIO_16_9} or
+     * {@link TargetAspectRatio#RATIO_MAX_JPEG}.
+     * 2. The use case's original aspect ratio if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has target aspect ratio setting.
+     * 3. The aspect ratio of use case's target size setting if {@link TargetAspectRatio} returns
+     * {@link TargetAspectRatio#RATIO_ORIGINAL} and the use case has no target aspect ratio but has
+     * target size setting.
+     *
+     * @param imageOutputConfig       the image output config of the use case.
+     * @param resolutionCandidateList the resolution candidate list which will be used to
+     *                                determine the aspect ratio by target size when target
+     *                                aspect ratio setting is not set.
+     */
+    private Rational getTargetAspectRatio(@NonNull ImageOutputConfig imageOutputConfig,
+            @NonNull List<Size> resolutionCandidateList) {
         Rational outputRatio = null;
         // Gets the corrected aspect ratio due to device constraints or null if no correction is
         // needed.
@@ -307,7 +347,6 @@
                 outputRatio = new Rational(maxJpegSize.getWidth(), maxJpegSize.getHeight());
                 break;
             case TargetAspectRatio.RATIO_ORIGINAL:
-                Size targetSize = getTargetSize(imageOutputConfig);
                 if (imageOutputConfig.hasTargetAspectRatio()) {
                     @AspectRatio.Ratio int aspectRatio = imageOutputConfig.getTargetAspectRatio();
                     switch (aspectRatio) {
@@ -322,11 +361,15 @@
                         default:
                             Logger.e(TAG, "Undefined target aspect ratio: " + aspectRatio);
                     }
-                } else if (targetSize != null) {
-                    // Target size is calculated from the target resolution. If target size is not
-                    // null, sizes which aspect ratio is nearest to the aspect ratio of target size
-                    // will be selected in priority.
-                    outputRatio = new Rational(targetSize.getWidth(), targetSize.getHeight());
+                } else {
+                    // The legacy resolution API will use the aspect ratio of the target size to
+                    // be the fallback target aspect ratio value when the use case has no target
+                    // aspect ratio setting.
+                    Size targetSize = getTargetSize(imageOutputConfig);
+                    if (targetSize != null) {
+                        outputRatio = getAspectRatioGroupKeyOfTargetSize(targetSize,
+                                resolutionCandidateList);
+                    }
                 }
                 break;
             default:
@@ -384,10 +427,30 @@
     List<Size> getSupportedOutputSizes(@NonNull UseCaseConfig<?> config) {
         int imageFormat = config.getInputFormat();
         ImageOutputConfig imageOutputConfig = (ImageOutputConfig) config;
+        ResolutionSelector resolutionSelector = imageOutputConfig.getResolutionSelector(null);
+
+        // Directly returns the output sizes retrieved from SupportedOutputSizesCollector when
+        // ResolutionSelector is used.
+        if (resolutionSelector != null) {
+            Size miniBoundingSize = imageOutputConfig.getDefaultResolution(null);
+
+            if (resolutionSelector.getPreferredResolution() != null) {
+                miniBoundingSize = getTargetSizeByResolutionSelector(resolutionSelector,
+                        mDisplayInfoManager.getMaxSizeDisplay().getRotation(), mSensorOrientation,
+                        mLensFacing);
+            }
+
+            return mSupportedOutputSizesCollector.getSupportedOutputSizes(resolutionSelector,
+                    imageFormat, miniBoundingSize, config.isHigResolutionDisabled(false),
+                    getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig));
+        }
+
         Size[] outputSizes = getCustomizedSupportSizesFromConfig(imageFormat, imageOutputConfig);
         if (outputSizes == null) {
             outputSizes = getAllOutputSizesByFormat(imageFormat);
         }
+        outputSizes = excludeProblematicSizesAndSort(outputSizes, imageFormat);
+
         List<Size> outputSizeCandidates = new ArrayList<>();
         Size maxSize = imageOutputConfig.getMaxResolution(null);
         Size maxOutputSizeByFormat = getMaxOutputSizeByFormat(imageFormat);
@@ -430,7 +493,7 @@
                             + imageFormat);
         }
 
-        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig);
+        Rational aspectRatio = getTargetAspectRatio(imageOutputConfig, outputSizeCandidates);
 
         // Check the default resolution if the target resolution is not set
         targetSize = targetSize == null ? imageOutputConfig.getDefaultResolution(null) : targetSize;
@@ -445,7 +508,7 @@
 
             // If the target resolution is set, use it to remove unnecessary larger sizes.
             if (targetSize != null) {
-                removeSupportedSizesByTargetSize(supportedResolutions, targetSize);
+                removeSupportedSizesByMiniBoundingSize(supportedResolutions, targetSize);
             }
         } else {
             // Rearrange the supported size to put the ones with the same aspect ratio in the front
@@ -459,14 +522,18 @@
             if (targetSize != null) {
                 // Remove unnecessary larger sizes from each aspect ratio size list
                 for (Rational key : aspectRatioSizeListMap.keySet()) {
-                    removeSupportedSizesByTargetSize(aspectRatioSizeListMap.get(key), targetSize);
+                    removeSupportedSizesByMiniBoundingSize(aspectRatioSizeListMap.get(key),
+                            targetSize);
                 }
             }
 
             // Sort the aspect ratio key set by the target aspect ratio.
             List<Rational> aspectRatios = new ArrayList<>(aspectRatioSizeListMap.keySet());
+            Rational fullFovRatio = mActiveArraySize != null ? new Rational(
+                    mActiveArraySize.getWidth(), mActiveArraySize.getHeight()) : null;
             Collections.sort(aspectRatios,
-                    new AspectRatioUtil.CompareAspectRatiosByDistanceToTargetRatio(aspectRatio));
+                    new AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                            aspectRatio, fullFovRatio));
 
             // Put available sizes into final result list by aspect ratio distance to target ratio.
             for (Rational rational : aspectRatios) {
@@ -492,129 +559,36 @@
         int targetRotation = imageOutputConfig.getTargetRotation(Surface.ROTATION_0);
         // Calibrate targetSize by the target rotation value.
         Size targetSize = imageOutputConfig.getTargetResolution(null);
-        targetSize = flipSizeByRotation(targetSize, targetRotation);
+        targetSize = flipSizeByRotation(targetSize, targetRotation, mLensFacing,
+                mSensorOrientation);
         return targetSize;
     }
 
-    // Use target rotation to calibrate the size.
-    @Nullable
-    private Size flipSizeByRotation(@Nullable Size size, int targetRotation) {
-        Size outputSize = size;
-        // Calibrates the size with the display and sensor rotation degrees values.
-        if (size != null && isRotationNeeded(targetRotation)) {
-            outputSize = new Size(/* width= */size.getHeight(), /* height= */size.getWidth());
-        }
-        return outputSize;
-    }
-
-    private boolean isRotationNeeded(int targetRotation) {
-        Integer sensorOrientation = mCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
-        Preconditions.checkNotNull(sensorOrientation, "Camera HAL in bad state, unable to "
-                + "retrieve the SENSOR_ORIENTATION");
-        int relativeRotationDegrees =
-                CameraOrientationUtil.surfaceRotationToDegrees(targetRotation);
-
-        // Currently this assumes that a back-facing camera is always opposite to the screen.
-        // This may not be the case for all devices, so in the future we may need to handle that
-        // scenario.
-        Integer lensFacing = mCharacteristics.get(CameraCharacteristics.LENS_FACING);
-        Preconditions.checkNotNull(lensFacing, "Camera HAL in bad state, unable to retrieve the "
-                + "LENS_FACING");
-
-        boolean isOppositeFacingScreen = CameraCharacteristics.LENS_FACING_BACK == lensFacing;
-
-        int sensorRotationDegrees = CameraOrientationUtil.getRelativeImageRotation(
-                relativeRotationDegrees,
-                sensorOrientation,
-                isOppositeFacingScreen);
-        return sensorRotationDegrees == 90 || sensorRotationDegrees == 270;
-    }
-
-    private boolean isSensorLandscapeResolution() {
-        Size pixelArraySize =
-                mCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE);
-
-        // Make the default value is true since usually the sensor resolution is landscape.
-        return pixelArraySize != null ? pixelArraySize.getWidth() >= pixelArraySize.getHeight()
-                : true;
-    }
-
-    private Map<Rational, List<Size>> groupSizesByAspectRatio(List<Size> sizes) {
-        Map<Rational, List<Size>> aspectRatioSizeListMap = new HashMap<>();
-
-        // Add 4:3 and 16:9 entries first. Most devices should mainly have supported sizes of
-        // these two aspect ratios. Adding them first can avoid that if the first one 4:3 or 16:9
-        // size is a mod16 alignment size, the aspect ratio key may be different from the 4:3 or
-        // 16:9 value.
-        aspectRatioSizeListMap.put(ASPECT_RATIO_4_3, new ArrayList<>());
-        aspectRatioSizeListMap.put(ASPECT_RATIO_16_9, new ArrayList<>());
-
-        for (Size outputSize : sizes) {
-            Rational matchedKey = null;
-
-            for (Rational key : aspectRatioSizeListMap.keySet()) {
-                // Put the size into all groups that is matched in mod16 condition since a size
-                // may match multiple aspect ratio in mod16 algorithm.
-                if (AspectRatioUtil.hasMatchingAspectRatio(outputSize, key)) {
-                    matchedKey = key;
-
-                    List<Size> sizeList = aspectRatioSizeListMap.get(matchedKey);
-                    if (!sizeList.contains(outputSize)) {
-                        sizeList.add(outputSize);
-                    }
-                }
-            }
-
-            // Create new item if no matching group is found.
-            if (matchedKey == null) {
-                aspectRatioSizeListMap.put(
-                        new Rational(outputSize.getWidth(), outputSize.getHeight()),
-                        new ArrayList<>(Collections.singleton(outputSize)));
-            }
-        }
-
-        return aspectRatioSizeListMap;
-    }
-
     /**
-     * Removes unnecessary sizes by target size.
+     * Returns the aspect ratio group key of the target size when grouping the input resolution
+     * candidate list.
      *
-     * <p>If the target resolution is set, a size that is equal to or closest to the target
-     * resolution will be selected. If the list includes more than one size equal to or larger
-     * than the target resolution, only one closest size needs to be kept. The other larger sizes
-     * can be removed so that they won't be selected to use.
-     *
-     * @param supportedSizesList The list should have been sorted in descending order.
-     * @param targetSize         The target size used to remove unnecessary sizes.
+     * The resolution candidate list will be grouped with mod 16 consideration. Therefore, we
+     * also need to consider the mod 16 factor to find which aspect ratio of group the target size
+     * might be put in. So that sizes of the group will be selected to use in the highest priority.
      */
-    private void removeSupportedSizesByTargetSize(List<Size> supportedSizesList,
-            Size targetSize) {
-        if (supportedSizesList == null || supportedSizesList.isEmpty()) {
-            return;
+    @Nullable
+    private Rational getAspectRatioGroupKeyOfTargetSize(@Nullable Size targetSize,
+            @NonNull List<Size> resolutionCandidateList) {
+        if (targetSize == null) {
+            return null;
         }
 
-        int indexBigEnough = -1;
-        List<Size> removeSizes = new ArrayList<>();
+        List<Rational> aspectRatios = getResolutionListGroupingAspectRatioKeys(
+                resolutionCandidateList);
 
-        // Get the index of the item that is equal to or closest to the target size.
-        for (int i = 0; i < supportedSizesList.size(); i++) {
-            Size outputSize = supportedSizesList.get(i);
-            if (outputSize.getWidth() >= targetSize.getWidth()
-                    && outputSize.getHeight() >= targetSize.getHeight()) {
-                // New big enough item closer to the target size is found. Adding the previous
-                // one into the sizes list that will be removed.
-                if (indexBigEnough >= 0) {
-                    removeSizes.add(supportedSizesList.get(indexBigEnough));
-                }
-
-                indexBigEnough = i;
-            } else {
-                break;
+        for (Rational aspectRatio: aspectRatios) {
+            if (hasMatchingAspectRatio(targetSize, aspectRatio)) {
+                return aspectRatio;
             }
         }
 
-        // Remove the unnecessary items that are larger than the item closest to the target size.
-        supportedSizesList.removeAll(removeSizes);
+        return new Rational(targetSize.getWidth(), targetSize.getHeight());
     }
 
     private List<List<Size>> getAllPossibleSizeArrangements(
@@ -671,11 +645,18 @@
     }
 
     @NonNull
-    private Size[] excludeProblematicSizes(@NonNull Size[] outputSizes, int imageFormat) {
+    private Size[] excludeProblematicSizesAndSort(@NonNull Size[] outputSizes, int imageFormat) {
         List<Size> excludedSizes = fetchExcludedSizes(imageFormat);
         List<Size> resultSizesList = new ArrayList<>(Arrays.asList(outputSizes));
         resultSizesList.removeAll(excludedSizes);
-        return resultSizesList.toArray(new Size[0]);
+
+        Size[] resultSizes = resultSizesList.toArray(new Size[0]);
+
+        // Sort the result sizes. The Comparator result must be reversed to have a descending
+        // order result.
+        Arrays.sort(resultSizes, new CompareSizesByArea(true));
+
+        return resultSizes;
     }
 
     @Nullable
@@ -696,14 +677,6 @@
             }
         }
 
-        if (outputSizes != null) {
-            outputSizes = excludeProblematicSizes(outputSizes, imageFormat);
-
-            // Sort the output sizes. The Comparator result must be reversed to have a descending
-            // order result.
-            Arrays.sort(outputSizes, new CompareSizesByArea(true));
-        }
-
         return outputSizes;
     }
 
@@ -746,12 +719,6 @@
                     "Can not get supported output size for the format: " + imageFormat);
         }
 
-        outputSizes = excludeProblematicSizes(outputSizes, imageFormat);
-
-        // Sort the output sizes. The Comparator result must be reversed to have a descending order
-        // result.
-        Arrays.sort(outputSizes, new CompareSizesByArea(true));
-
         return outputSizes;
     }
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
new file mode 100644
index 0000000..54e5f41
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedOutputSizesCollectorTest.kt
@@ -0,0 +1,1364 @@
+/*
+ * 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.camera2.internal
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.params.StreamConfigurationMap
+import android.media.MediaRecorder
+import android.os.Build
+import android.util.Pair
+import android.util.Size
+import android.view.Surface
+import android.view.WindowManager
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.CameraManagerCompat
+import androidx.camera.core.AspectRatio
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraX
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.ResolutionSelector
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.SizeCoordinate
+import androidx.camera.core.impl.UseCaseConfigFactory
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CameraXUtil
+import androidx.camera.testing.Configs
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
+import androidx.camera.testing.fakes.FakeUseCaseConfig
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito
+import org.robolectric.ParameterizedRobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadow.api.Shadow
+import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.shadows.ShadowCameraManager
+
+private const val FAKE_USE_CASE = 0
+private const val PREVIEW_USE_CASE = 1
+private const val IMAGE_CAPTURE_USE_CASE = 2
+private const val IMAGE_ANALYSIS_USE_CASE = 3
+private const val UNKNOWN_ASPECT_RATIO = -1
+private const val DEFAULT_CAMERA_ID = "0"
+private const val SENSOR_ORIENTATION_0 = 0
+private const val SENSOR_ORIENTATION_90 = 90
+private val LANDSCAPE_PIXEL_ARRAY_SIZE = Size(4032, 3024)
+private val PORTRAIT_PIXEL_ARRAY_SIZE = Size(3024, 4032)
+private val DISPLAY_SIZE = Size(720, 1280)
+private val DEFAULT_SUPPORTED_SIZES = arrayOf(
+    Size(4032, 3024), // 4:3
+    Size(3840, 2160), // 16:9
+    Size(1920, 1440), // 4:3
+    Size(1920, 1080), // 16:9
+    Size(1280, 960), // 4:3
+    Size(1280, 720), // 16:9
+    Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+    Size(800, 450), // 16:9
+    Size(640, 480), // 4:3
+    Size(320, 240), // 4:3
+    Size(320, 180), // 16:9
+    Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
+)
+
+/** Robolectric test for [SupportedOutputSizesCollector] class */
+@RunWith(ParameterizedRobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class SupportedOutputSizesCollectorTest(
+    private val sizeCoordinate: SizeCoordinate
+) {
+    private val mockCamcorderProfileHelper = Mockito.mock(CamcorderProfileHelper::class.java)
+    private lateinit var cameraManagerCompat: CameraManagerCompat
+    private lateinit var cameraCharacteristicsCompat: CameraCharacteristicsCompat
+    private lateinit var displayInfoManager: DisplayInfoManager
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private var cameraFactory: FakeCameraFactory? = null
+    private var useCaseConfigFactory: UseCaseConfigFactory? = null
+
+    @Suppress("DEPRECATION") // defaultDisplay
+    @Before
+    fun setUp() {
+        DisplayInfoManager.releaseInstance()
+        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(DISPLAY_SIZE.width)
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(DISPLAY_SIZE.height)
+        Mockito.`when`(
+            mockCamcorderProfileHelper.hasProfile(
+                ArgumentMatchers.anyInt(),
+                ArgumentMatchers.anyInt()
+            )
+        ).thenReturn(true)
+
+        displayInfoManager = DisplayInfoManager.getInstance(context)
+    }
+
+    @After
+    fun tearDown() {
+        CameraXUtil.shutdown()[10000, TimeUnit.MILLISECONDS]
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio4x3() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_4_3
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_aspectRatio16x9_inLegacyDevice() {
+        setupCameraAndInitCameraX()
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList: List<Size> = if (Build.VERSION.SDK_INT == 21) {
+            listOf(
+                // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched maximum JPEG resolution AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        } else {
+            listOf(
+                // Matched preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+                // Mismatched preferred AspectRatio items, sorted by area size.
+                Size(4032, 3024),
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240)
+            )
+        }
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1920x1080_InLimitedDevice() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
+        // after the preferred resolution.
+        val expectedList =
+            listOf(
+                // Matched preferred resolution size will be put in first priority.
+                Size(1920, 1080),
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by area size.
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144),
+            )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1920x1080_InLegacyDevice() {
+        setupCameraAndInitCameraX()
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest priority just
+        // after the preferred resolution.
+        val expectedList = if (Build.VERSION.SDK_INT == 21) {
+                listOf(
+                    // Matched maximum JPEG resolution AspectRatio items, sorted by area size.
+                    Size(1920, 1440),
+                    Size(1280, 960),
+                    Size(640, 480),
+                    Size(320, 240),
+                    // Mismatched maximum JPEG resolution AspectRatio items, sorted by area size.
+                    Size(1920, 1080),
+                    Size(1280, 720),
+                    Size(960, 544),
+                    Size(800, 450),
+                    Size(320, 180),
+                    Size(256, 144)
+                )
+            } else {
+                // The 4:3 default aspect ratio will make sizes of 4/3 have the 2nd highest
+                // priority just after the preferred resolution size.
+                listOf(
+                    // Matched default preferred resolution size will be put in first priority.
+                    Size(1920, 1080),
+                    // Matched preferred AspectRatio items, sorted by area size.
+                    Size(1920, 1440),
+                    Size(1280, 960),
+                    Size(640, 480),
+                    Size(320, 240),
+                    // Mismatched preferred default AspectRatio items, sorted by area size.
+                    Size(1280, 720),
+                    Size(960, 544),
+                    Size(800, 450),
+                    Size(320, 180),
+                    Size(256, 144),
+                )
+            }
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    @Suppress("DEPRECATION") /* defaultDisplay */
+    fun getSupportedOutputSizes_smallDisplay_withMaxResolution1920x1080() {
+        // Sets up small display.
+        val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealWidth(240)
+        Shadows.shadowOf(windowManager.defaultDisplay).setRealHeight(320)
+        displayInfoManager = DisplayInfoManager.getInstance(context)
+
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            PREVIEW_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            maxResolution = Size(1920, 1080)
+        )
+        // Max resolution setting will remove sizes larger than 1920x1080. The auto-resolution
+        // mechanism will try to select the sizes which aspect ratio is nearest to the aspect ratio
+        // of target resolution in priority. Therefore, sizes of aspect ratio 16/9 will be in front
+        // of the returned sizes list and the list is sorted in descending order. Other items will
+        // be put in the following that are sorted by aspect ratio delta and then area size.
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1800x1440NearTo4x3() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1800, 1440),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList =
+            listOf(
+                // No matched preferred resolution size found.
+                // Matched default preferred AspectRatio items, sorted by area size.
+                Size(1920, 1440),
+                Size(1280, 960),
+                Size(640, 480),
+                Size(320, 240),
+                // Mismatched default preferred AspectRatio items, sorted by area size.
+                Size(3840, 2160),
+                Size(1920, 1080),
+                Size(1280, 720),
+                Size(960, 544),
+                Size(800, 450),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_preferredResolution1280x600NearTo16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1280, 600),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_maxResolution1280x720() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(1280, 720)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_maxResolution720x1280() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(720, 1280)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_defaultResolution1280x720_noTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            defaultResolution = Size(1280, 720)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_defaultResolution1280x720_preferredResolution1920x1080() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            defaultResolution = Size(1280, 720),
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred resolution size will be put in first priority.
+            Size(1920, 1080),
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_fallbackToGuaranteedResolution_whenNotFulfillConditions() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1920, 1080),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxSizeSmallerThanSmallTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(320, 240),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(320, 180)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 180), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxSizeSmallerThanBigPreferredResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(3840, 2160),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(1920, 1080)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found after filtering by max resolution setting.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenNoSizeBetweenMaxSizeAndPreferredResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(320, 190),
+            sizeCoordinate = sizeCoordinate,
+            maxResolution = Size(320, 200)
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 180), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenPreferredResolutionSmallerThanAnySize() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(192, 144),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(Size(320, 240), Size(256, 144))
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMaxResolutionSmallerThanAnySize() {
+        setupCameraAndInitCameraX(
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            maxResolution = Size(192, 144)
+        )
+        // All sizes will be filtered out by the max resolution 192x144 setting and an
+        // IllegalArgumentException will be thrown.
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        }
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenMod16IsIgnoredForSmallSizes() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(640, 480),
+                Size(320, 240),
+                Size(320, 180),
+                Size(296, 144),
+                Size(256, 144)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(185, 90),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(320, 240),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(256, 144),
+            Size(296, 144)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizes_whenOneMod16SizeClosestToTargetResolution() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedSizes = arrayOf(
+                Size(1920, 1080),
+                Size(1440, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(864, 480), // This is a 16:9 mod16 size that is closest to 2016x1080
+                Size(768, 432),
+                Size(640, 480),
+                Size(640, 360),
+                Size(480, 360),
+                Size(384, 288)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredResolution = Size(1080, 2016),
+            sizeCoordinate = sizeCoordinate
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // No matched preferred resolution size found.
+            // Matched default preferred AspectRatio items, sorted by area size.
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(480, 360),
+            Size(384, 288),
+            // Mismatched default preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(864, 480),
+            Size(768, 432),
+            Size(640, 360)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesWithPortraitPixelArraySize_aspectRatio16x9() {
+        // Sets the sensor orientation as 0 and pixel array size as a portrait size to simulate a
+        // phone device which majorly supports portrait output sizes.
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0,
+            pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
+            supportedSizes = arrayOf(
+                Size(1080, 1920),
+                Size(1080, 1440),
+                Size(960, 1280),
+                Size(720, 1280),
+                Size(960, 540),
+                Size(480, 640),
+                Size(640, 480),
+                Size(360, 480)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(720, 1280),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(360, 480),
+            Size(640, 480),
+            Size(960, 540)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTabletWithPortraitPixelArraySize_aspectRatio16x9() {
+        // Sets the sensor orientation as 90 and pixel array size as a portrait size to simulate a
+        // tablet device which majorly supports portrait output sizes.
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_90,
+            pixelArraySize = PORTRAIT_PIXEL_ARRAY_SIZE,
+            supportedSizes = arrayOf(
+                Size(1080, 1920),
+                Size(1080, 1440),
+                Size(960, 1280),
+                Size(720, 1280),
+                Size(960, 540),
+                Size(480, 640),
+                Size(640, 480),
+                Size(360, 480)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        // Due to the pixel array size is portrait, sizes of aspect ratio 9/16 will be in front of
+        // the returned sizes list and the list is sorted in descending order. Other items will be
+        // put in the following that are sorted by aspect ratio delta and then area size.
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1080, 1920),
+            Size(720, 1280),
+            // Mismatched preferred AspectRatio items, sorted by aspect ratio delta then area size.
+            Size(1080, 1440),
+            Size(960, 1280),
+            Size(480, 640),
+            Size(360, 480),
+            Size(640, 480),
+            Size(960, 540)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTablet_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0,
+            pixelArraySize = LANDSCAPE_PIXEL_ARRAY_SIZE
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Test
+    fun getSupportedOutputSizesOnTabletWithPortraitSizes_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            sensorOrientation = SENSOR_ORIENTATION_0, supportedSizes = arrayOf(
+                Size(1920, 1080),
+                Size(1440, 1080),
+                Size(1280, 960),
+                Size(1280, 720),
+                Size(540, 960),
+                Size(640, 480),
+                Size(480, 640),
+                Size(480, 360)
+            )
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(1920, 1080),
+            Size(1280, 720),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(1440, 1080),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(480, 360),
+            Size(480, 640),
+            Size(540, 960)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun getSupportedOutputSizes_whenHighResolutionIsEnabled_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(8000, 4500),
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(8000, 6000),
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionCanNotBeSelected_whenHighResolutionForceDisabled() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedOutputSizesCollector = SupportedOutputSizesCollector(
+            DEFAULT_CAMERA_ID,
+            cameraCharacteristicsCompat,
+            displayInfoManager
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true,
+            highResolutionForceDisabled = true
+        )
+        val resultList = getSupportedOutputSizes(supportedOutputSizesCollector, useCase)
+        val expectedList = listOf(
+            // Matched preferred AspectRatio items, sorted by area size.
+            Size(3840, 2160),
+            Size(1920, 1080),
+            Size(1280, 720),
+            Size(960, 544),
+            Size(800, 450),
+            Size(320, 180),
+            Size(256, 144),
+            // Mismatched preferred AspectRatio items, sorted by area size.
+            Size(4032, 3024),
+            Size(1920, 1440),
+            Size(1280, 960),
+            Size(640, 480),
+            Size(320, 240)
+        )
+        assertThat(resultList).isEqualTo(expectedList)
+    }
+
+    /**
+     * Sets up camera according to the specified settings and initialize [CameraX].
+     *
+     * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
+     * @param hardwareLevel the hardware level of the camera. Default value is
+     * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
+     * @param sensorOrientation the sensor orientation of the camera. Default value is
+     * [SENSOR_ORIENTATION_90].
+     * @param pixelArraySize the active pixel array size of the camera. Default value is
+     * [LANDSCAPE_PIXEL_ARRAY_SIZE].
+     * @param supportedSizes the supported sizes of the camera. Default value is
+     * [DEFAULT_SUPPORTED_SIZES].
+     * @param capabilities the capabilities of the camera. Default value is null.
+     */
+    private fun setupCameraAndInitCameraX(
+        cameraId: String = DEFAULT_CAMERA_ID,
+        hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+        sensorOrientation: Int = SENSOR_ORIENTATION_90,
+        pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
+        supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+        supportedHighResolutionSizes: Array<Size>? = null,
+        capabilities: IntArray? = null
+    ) {
+        setupCamera(
+            cameraId,
+            hardwareLevel,
+            sensorOrientation,
+            pixelArraySize,
+            supportedSizes,
+            supportedHighResolutionSizes,
+            capabilities
+        )
+
+        @CameraSelector.LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+        cameraManagerCompat = CameraManagerCompat.from(context)
+        val cameraInfo = FakeCameraInfoInternal(
+            cameraId,
+            sensorOrientation,
+            CameraCharacteristics.LENS_FACING_BACK
+        )
+
+        cameraFactory = FakeCameraFactory().apply {
+            insertCamera(lensFacingEnum, cameraId) {
+                FakeCamera(cameraId, null, cameraInfo)
+            }
+        }
+
+        cameraCharacteristicsCompat = cameraManagerCompat.getCameraCharacteristicsCompat(cameraId)
+
+        initCameraX()
+    }
+
+    /**
+     * Initializes the [CameraX].
+     */
+    private fun initCameraX() {
+        val surfaceManagerProvider =
+            CameraDeviceSurfaceManager.Provider { context, _, availableCameraIds ->
+                Camera2DeviceSurfaceManager(
+                    context,
+                    mockCamcorderProfileHelper,
+                    CameraManagerCompat.from(this@SupportedOutputSizesCollectorTest.context),
+                    availableCameraIds
+                )
+            }
+        val cameraXConfig = CameraXConfig.Builder.fromConfig(Camera2Config.defaultConfig())
+            .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
+            .setCameraFactoryProvider { _, _, _ -> cameraFactory!! }
+            .build()
+        val cameraX: CameraX = try {
+            CameraXUtil.getOrCreateInstance(context) { cameraXConfig }.get()
+        } catch (e: ExecutionException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        } catch (e: InterruptedException) {
+            throw IllegalStateException("Unable to initialize CameraX for test.")
+        }
+        useCaseConfigFactory = cameraX.defaultConfigFactory
+    }
+
+    /**
+     * Gets the supported output sizes by the converted ResolutionSelector use case config which
+     * will also be converted when a use case is bound to the lifecycle.
+     */
+    private fun getSupportedOutputSizes(
+        supportedOutputSizesCollector: SupportedOutputSizesCollector,
+        useCase: UseCase,
+        cameraId: String = DEFAULT_CAMERA_ID,
+        sensorOrientation: Int = SENSOR_ORIENTATION_90,
+        useCaseConfigFactory: UseCaseConfigFactory = this.useCaseConfigFactory!!
+    ): List<Size?> {
+        // Converts the use case config to new ResolutionSelector config
+        val useCaseToConfigMap = Configs.useCaseConfigMapWithDefaultSettingsFromUseCaseList(
+            cameraFactory!!.getCamera(cameraId).cameraInfoInternal,
+            listOf(useCase),
+            useCaseConfigFactory
+        )
+
+        val useCaseConfig = useCaseToConfigMap[useCase]!!
+        val resolutionSelector = (useCaseConfig as ImageOutputConfig).resolutionSelector
+        val imageFormat = useCaseConfig.inputFormat
+        val isHighResolutionDisabled = useCaseConfig.isHigResolutionDisabled(false)
+        val customizedSupportSizes = getCustomizedSupportSizesFromConfig(imageFormat, useCaseConfig)
+        val miniBoundingSize = SupportedOutputSizesCollector.getTargetSizeByResolutionSelector(
+            resolutionSelector,
+            Surface.ROTATION_0,
+            sensorOrientation,
+            CameraCharacteristics.LENS_FACING_BACK
+        ) ?: useCaseConfig.getDefaultResolution(null)
+
+        return supportedOutputSizesCollector.getSupportedOutputSizes(
+            resolutionSelector,
+            imageFormat,
+            miniBoundingSize,
+            isHighResolutionDisabled,
+            customizedSupportSizes
+        )
+    }
+
+    companion object {
+
+        /**
+         * Sets up camera according to the specified settings.
+         *
+         * @param cameraId the camera id to be set up. Default value is [DEFAULT_CAMERA_ID].
+         * @param hardwareLevel the hardware level of the camera. Default value is
+         * [CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY].
+         * @param sensorOrientation the sensor orientation of the camera. Default value is
+         * [SENSOR_ORIENTATION_90].
+         * @param pixelArraySize the active pixel array size of the camera. Default value is
+         * [LANDSCAPE_PIXEL_ARRAY_SIZE].
+         * @param supportedSizes the supported sizes of the camera. Default value is
+         * [DEFAULT_SUPPORTED_SIZES].
+         * @param capabilities the capabilities of the camera. Default value is null.
+         */
+        @JvmStatic
+        fun setupCamera(
+            cameraId: String = DEFAULT_CAMERA_ID,
+            hardwareLevel: Int = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
+            sensorOrientation: Int = SENSOR_ORIENTATION_90,
+            pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
+            supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+            supportedHighResolutionSizes: Array<Size>? = null,
+            capabilities: IntArray? = null
+        ) {
+            val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
+                // Sets up the supported sizes
+                Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt()))
+                    .thenReturn(supportedSizes)
+                // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
+                // output sizes need to be retrieved via SurfaceTexture.class.
+                Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java))
+                    .thenReturn(supportedSizes)
+                // This is setup for the test to determine RECORD size from StreamConfigurationMap
+                Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java))
+                    .thenReturn(supportedSizes)
+
+                // Sets up the supported high resolution sizes
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+                    Mockito.`when`(it.getHighResolutionOutputSizes(ArgumentMatchers.anyInt()))
+                        .thenReturn(supportedHighResolutionSizes)
+                }
+            }
+
+            val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
+            Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
+                set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
+                set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
+                set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
+                set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
+                set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
+                capabilities?.let {
+                    set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
+                }
+            }
+
+            val cameraManager = ApplicationProvider.getApplicationContext<Context>()
+                .getSystemService(Context.CAMERA_SERVICE) as CameraManager
+            (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
+                .addCamera(cameraId, characteristics)
+        }
+
+        /**
+         * Creates [Preview], [ImageCapture], [ImageAnalysis], [androidx.camera.core.VideoCapture] or
+         * FakeUseCase by the legacy or new ResolutionSelector API according to the specified settings.
+         *
+         * @param useCaseType Which of [Preview], [ImageCapture], [ImageAnalysis],
+         * [androidx.camera.core.VideoCapture] and FakeUseCase should be created.
+         * @param preferredAspectRatio the target aspect ratio setting. Default is UNKNOWN_ASPECT_RATIO
+         * and no target aspect ratio will be set to the created use case.
+         * @param preferredResolution the preferred resolution setting which should be specified in the
+         * camera sensor coordinate. The resolution will be transformed to set via
+         * [ResolutionSelector.Builder.setPreferredResolutionByViewSize] if size coordinate is
+         * [SizeCoordinate.ANDROID_VIEW]. Default is null.
+         * @param maxResolution the max resolution setting. Default is null.
+         * @param highResolutionEnabled the high resolution setting, Default is false.
+         * @param highResolutionForceDisabled the high resolution force disabled setting, Default
+         * is false. This will be set in the use case config to force disable high resolution.
+         * @param defaultResolution the default resolution setting. Default is null.
+         * @param supportedResolutions the customized supported resolutions. Default is null.
+         */
+        @JvmStatic
+        fun createUseCaseByResolutionSelector(
+            useCaseType: Int,
+            preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
+            preferredResolution: Size? = null,
+            sizeCoordinate: SizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
+            maxResolution: Size? = null,
+            highResolutionEnabled: Boolean = false,
+            highResolutionForceDisabled: Boolean = false,
+            defaultResolution: Size? = null,
+            supportedResolutions: List<Pair<Int, Array<Size>>>? = null
+        ): UseCase {
+            val builder = when (useCaseType) {
+                PREVIEW_USE_CASE -> Preview.Builder()
+                IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
+                IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
+                else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
+            }
+
+            val resolutionSelectorBuilder = ResolutionSelector.Builder()
+
+            if (preferredAspectRatio != UNKNOWN_ASPECT_RATIO) {
+                resolutionSelectorBuilder.setPreferredAspectRatio(preferredAspectRatio)
+            }
+
+            preferredResolution?.let {
+                if (sizeCoordinate == SizeCoordinate.CAMERA_SENSOR) {
+                    resolutionSelectorBuilder.setPreferredResolution(it)
+                } else {
+                    val flippedResolution = Size(
+                        /* width= */ it.height,
+                        /* height= */ it.width
+                    )
+                    resolutionSelectorBuilder.setPreferredResolutionByViewSize(flippedResolution)
+                }
+            }
+
+            maxResolution?.let { resolutionSelectorBuilder.setMaxResolution(it) }
+            resolutionSelectorBuilder.setHighResolutionEnabled(highResolutionEnabled)
+
+            builder.setResolutionSelector(resolutionSelectorBuilder.build())
+            builder.setHighResolutionDisabled(highResolutionForceDisabled)
+
+            defaultResolution?.let { builder.setDefaultResolution(it) }
+            supportedResolutions?.let { builder.setSupportedResolutions(it) }
+            return builder.build()
+        }
+
+        @JvmStatic
+        fun getCustomizedSupportSizesFromConfig(
+            imageFormat: Int,
+            config: ImageOutputConfig
+        ): Array<Size>? {
+            var outputSizes: Array<Size>? = null
+
+            // Try to retrieve customized supported resolutions from config.
+            val formatResolutionsPairList = config.getSupportedResolutions(null)
+            if (formatResolutionsPairList != null) {
+                for (formatResolutionPair in formatResolutionsPairList) {
+                    if (formatResolutionPair.first == imageFormat) {
+                        outputSizes = formatResolutionPair.second
+                        break
+                    }
+                }
+            }
+            return outputSizes
+        }
+
+        @JvmStatic
+        @ParameterizedRobolectricTestRunner.Parameters(name = "sizeCoordinate = {0}")
+        fun data() = listOf(
+            SizeCoordinate.CAMERA_SENSOR,
+            SizeCoordinate.ANDROID_VIEW
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
index 63f772c1..1d8366d5 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.kt
@@ -17,13 +17,9 @@
 package androidx.camera.camera2.internal
 import android.content.Context
 import android.graphics.ImageFormat
-import android.graphics.SurfaceTexture
 import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CameraManager
 import android.hardware.camera2.CameraMetadata
-import android.hardware.camera2.params.StreamConfigurationMap
 import android.media.CamcorderProfile
-import android.media.MediaRecorder
 import android.os.Build
 import android.util.Pair
 import android.util.Rational
@@ -32,6 +28,8 @@
 import android.view.WindowManager
 import androidx.annotation.NonNull
 import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.createUseCaseByResolutionSelector
+import androidx.camera.camera2.internal.SupportedOutputSizesCollectorTest.Companion.setupCamera
 import androidx.camera.camera2.internal.compat.CameraManagerCompat
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector.LensFacing
@@ -46,6 +44,7 @@
 import androidx.camera.core.impl.CameraDeviceSurfaceManager
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.MutableStateObservable
+import androidx.camera.core.impl.SizeCoordinate
 import androidx.camera.core.impl.SurfaceCombination
 import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.SurfaceConfig.ConfigSize
@@ -53,9 +52,9 @@
 import androidx.camera.core.impl.UseCaseConfig
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.impl.utils.CompareSizesByArea
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
 import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_VGA
 import androidx.camera.testing.CamcorderProfileUtil
 import androidx.camera.testing.CameraUtil
@@ -93,15 +92,11 @@
 import org.robolectric.Shadows
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import org.robolectric.shadow.api.Shadow
-import org.robolectric.shadows.ShadowCameraCharacteristics
-import org.robolectric.shadows.ShadowCameraManager
 
 private const val FAKE_USE_CASE = 0
 private const val PREVIEW_USE_CASE = 1
 private const val IMAGE_CAPTURE_USE_CASE = 2
 private const val IMAGE_ANALYSIS_USE_CASE = 3
-private const val VIDEO_CAPTURE_USE_CASE = 4
 private const val UNKNOWN_ROTATION = -1
 private const val UNKNOWN_ASPECT_RATIO = -1
 private const val DEFAULT_CAMERA_ID = "0"
@@ -124,7 +119,6 @@
     Size(1920, 1080), // 16:9
     Size(1280, 960), // 4:3
     Size(1280, 720), // 16:9
-    Size(1280, 720), // duplicate the size since Nexus 5X emulator has the case.
     Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
     Size(800, 450), // 16:9
     Size(640, 480), // 4:3
@@ -137,7 +131,7 @@
 @RunWith(RobolectricTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
-class SupportedSurfaceCombinationTest() {
+class SupportedSurfaceCombinationTest {
     private val mockCamcorderProfileHelper = Mockito.mock(
         CamcorderProfileHelper::class.java
     )
@@ -146,22 +140,95 @@
     )
     private var cameraManagerCompat: CameraManagerCompat? = null
     private val profileUhd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_2160P, RECORD_SIZE.getWidth(), RECORD_SIZE.getHeight()
+        CamcorderProfile.QUALITY_2160P, RECORD_SIZE.width, RECORD_SIZE.height
     )
     private val profileFhd = CamcorderProfileUtil.createCamcorderProfileProxy(
         CamcorderProfile.QUALITY_1080P, 1920, 1080
     )
     private val profileHd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_720P, PREVIEW_SIZE.getWidth(), PREVIEW_SIZE.getHeight()
+        CamcorderProfile.QUALITY_720P, PREVIEW_SIZE.width, PREVIEW_SIZE.height
     )
     private val profileSd = CamcorderProfileUtil.createCamcorderProfileProxy(
-        CamcorderProfile.QUALITY_480P, RESOLUTION_VGA.getWidth(),
-        RESOLUTION_VGA.getHeight()
+        CamcorderProfile.QUALITY_480P, RESOLUTION_VGA.width,
+        RESOLUTION_VGA.height
     )
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private var cameraFactory: FakeCameraFactory? = null
     private var useCaseConfigFactory: UseCaseConfigFactory? = null
 
+    private val legacyUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByLegacyApi(
+                useCaseType,
+                targetRotation,
+                preferredAspectRatio,
+                preferredResolution,
+                maxResolution,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
+    private val resolutionSelectorUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByResolutionSelector(
+                useCaseType,
+                preferredAspectRatio,
+                preferredResolution,
+                sizeCoordinate = SizeCoordinate.CAMERA_SENSOR,
+                maxResolution,
+                highResolutionEnabled,
+                highResolutionForceDisabled = false,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
+    private val viewSizeResolutionSelectorUseCaseCreator = object : UseCaseCreator {
+        override fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int,
+            preferredAspectRatio: Int,
+            preferredResolution: Size?,
+            maxResolution: Size?,
+            highResolutionEnabled: Boolean,
+            defaultResolution: Size?,
+            supportedResolutions: List<Pair<Int, Array<Size>>>?
+        ): UseCase {
+            return createUseCaseByResolutionSelector(
+                useCaseType,
+                preferredAspectRatio,
+                preferredResolution,
+                sizeCoordinate = SizeCoordinate.ANDROID_VIEW,
+                maxResolution,
+                highResolutionEnabled,
+                highResolutionForceDisabled = false,
+                defaultResolution,
+                supportedResolutions
+            )
+        }
+    }
+
     @Suppress("DEPRECATION") // defaultDisplay
     @Before
     fun setUp() {
@@ -416,13 +483,25 @@
     }
 
     @Test
-    fun checkTargetAspectRatioInLegacyDevice() {
+    fun checkTargetAspectRatioInLegacyDevice_LegacyApi() {
+        checkTargetAspectRatioInLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkTargetAspectRatioInLegacyDevice_ResolutionSelector() {
+        checkTargetAspectRatioInLegacyDevice(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkTargetAspectRatioInLegacyDevice(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX()
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         val targetAspectRatio = ASPECT_RATIO_16_9
-        val useCase = createUseCase(FAKE_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
+        val useCase = useCaseCreator.createUseCase(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
@@ -443,15 +522,30 @@
     }
 
     @Test
-    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice() {
+    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_LegacyApi() {
+        checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice_ResolutionSelector() {
+        checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun checkResolutionForMixedUseCase_AfterBindToLifecycle_InLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX()
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // The test case make sure the selected result is expected after the regular flow.
         val targetAspectRatio = ASPECT_RATIO_16_9
-        val preview =
-            createUseCase(PREVIEW_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9) as Preview
+        val preview = useCaseCreator.createUseCase(
+            PREVIEW_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        ) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -460,10 +554,14 @@
                 )
             )
         )
-        val imageCapture =
-            createUseCase(IMAGE_CAPTURE_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
-        val imageAnalysis =
-            createUseCase(IMAGE_ANALYSIS_USE_CASE, targetAspectRatio = AspectRatio.RATIO_16_9)
+        val imageCapture = useCaseCreator.createUseCase(
+            IMAGE_CAPTURE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
+        val imageAnalysis = useCaseCreator.createUseCase(
+            IMAGE_ANALYSIS_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9
+        )
         val maxJpegSize = supportedSurfaceCombination.getMaxOutputSizeByFormat(ImageFormat.JPEG)
         val maxJpegAspectRatio = Rational(maxJpegSize.width, maxJpegSize.height)
         val suggestedResolutionMap = getSuggestedResolutionMap(
@@ -517,14 +615,25 @@
     }
 
     @Test
-    fun checkDefaultAspectRatioAndResolutionForMixedUseCase() {
+    fun checkDefaultAspectRatioAndResolutionForMixedUseCase_LegacyApi() {
+        checkDefaultAspectRatioAndResolutionForMixedUseCase(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkDefaultAspectRatioAndResolutionForMixedUseCase_ResolutionSelector() {
+        checkDefaultAspectRatioAndResolutionForMixedUseCase(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkDefaultAspectRatioAndResolutionForMixedUseCase(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -533,8 +642,8 @@
                 )
             )
         )
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE)
-        val imageAnalysis = createUseCase(IMAGE_ANALYSIS_USE_CASE)
+        val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
+        val imageAnalysis = useCaseCreator.createUseCase(IMAGE_ANALYSIS_USE_CASE)
 
         // Preview/ImageCapture/ImageAnalysis' default config settings that will be applied after
         // bound to lifecycle. Calling bindToLifecycle here to make sure sizes matching to
@@ -577,8 +686,10 @@
         */
         val displayWidth = 1080
         val displayHeight = 2220
-        val preview =
-            createUseCase(PREVIEW_USE_CASE, targetResolution = Size(displayHeight, displayWidth))
+        val preview = createUseCaseByLegacyApi(
+            PREVIEW_USE_CASE,
+            targetResolution = Size(displayHeight, displayWidth)
+        )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, preview)
         // Checks the preconditions.
         val preconditionSize = Size(256, 144)
@@ -593,7 +704,21 @@
     }
 
     @Test
-    fun checkAspectRatioMatchedSizeCanBeSelected() {
+    fun checkAllSupportedSizesCanBeSelected_LegacyApi() {
+        checkAllSupportedSizesCanBeSelected(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_SensorSize() {
+        checkAllSupportedSizesCanBeSelected(resolutionSelectorUseCaseCreator)
+    }
+
+    @Test
+    fun checkAllSupportedSizesCanBeSelected_ResolutionSelector_ViewSize() {
+        checkAllSupportedSizesCanBeSelected(viewSizeResolutionSelectorUseCaseCreator)
+    }
+
+    private fun checkAllSupportedSizesCanBeSelected(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -605,10 +730,10 @@
         // will be selected as the result. This test can also verify that size smaller than
         // 640x480 can be selected after set as target resolution.
         DEFAULT_SUPPORTED_SIZES.forEach {
-            val imageCapture = createUseCase(
+            val imageCapture = useCaseCreator.createUseCase(
                 IMAGE_CAPTURE_USE_CASE,
                 Surface.ROTATION_90,
-                targetResolution = it
+                preferredResolution = it
             )
             val suggestedResolutionMap =
                 getSuggestedResolutionMap(supportedSurfaceCombination, imageCapture)
@@ -617,76 +742,83 @@
     }
 
     @Test
-    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected() {
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_LegacyApi() {
+        // Sets target resolution as 1280x640, all supported resolutions will be put into
+        // aspect ratio not matched list. Then, 1280x720 will be the nearest matched one.
+        // Finally, checks whether 1280x720 is selected or not.
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(legacyUseCaseCreator, Size(1280, 720))
+    }
+
+    // 1280x640 is not included in the supported sizes list. So, the smallest size of the
+    // default aspect ratio 4:3 which is 1280x960 will be finally selected.
+    @Test
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_SensorSize() {
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+            resolutionSelectorUseCaseCreator,
+            Size(1280, 960)
+        )
+    }
+
+    @Test
+    fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected_ResolutionSelector_ViewSize() {
+        checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+            viewSizeResolutionSelectorUseCaseCreator,
+            Size(1280, 960)
+        )
+    }
+
+    private fun checkCorrectAspectRatioNotMatchedSizeCanBeSelected(
+        useCaseCreator: UseCaseCreator,
+        expectedResult: Size
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        // Sets target resolution as 1200x720, all supported resolutions will be put into aspect
+        // Sets target resolution as 1280x640, all supported resolutions will be put into aspect
         // ratio not matched list. Then, 1280x720 will be the nearest matched one. Finally,
         // checks whether 1280x720 is selected or not.
-        val resolution = Size(1200, 720)
-        val useCase = createUseCase(
+        val resolution = Size(1280, 640)
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             Surface.ROTATION_90,
-            targetResolution = resolution
+            preferredResolution = resolution
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
-        assertThat(Size(1280, 720)).isEqualTo(
-            suggestedResolutionMap[useCase]
-        )
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(expectedResult)
     }
 
     @Test
-    fun legacyVideo_suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
+    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        // An IllegalArgumentException will be thrown because a LEGACY level device can't support
-        // ImageCapture + VideoCapture + Preview
-        assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(
-                supportedSurfaceCombination,
-                imageCapture,
-                videoCapture,
-                preview
-            )
-        }
-    }
-
-    @Test
-    fun suggestedResolutionsForMixedUseCaseNotSupportedInLegacyDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture()
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         // An IllegalArgumentException will be thrown because a LEGACY level device can't support
         // ImageCapture + VideoCapture + Preview
@@ -701,38 +833,20 @@
     }
 
     @Test
-    fun legacyVideo_suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        // Legacy camera only support (PRIV, PREVIEW) + (PRIV, PREVIEW)
-        val videoResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(RECORD_SIZE))
-        )
-        val previewResolutionsPairs = listOf(
-            Pair.create(ImageFormat.PRIVATE, arrayOf(PREVIEW_SIZE))
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            maxResolution = RECORD_SIZE, // Override the default max resolution in VideoCapture
-            supportedResolutions = videoResolutionsPairs
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            supportedResolutions = previewResolutionsPairs
-        )
-        // An IllegalArgumentException will be thrown because the VideoCapture requests to only
-        // support a RECORD size but the configuration can't be supported on a LEGACY level device.
-        assertThrows(IllegalArgumentException::class.java) {
-            getSuggestedResolutionMap(supportedSurfaceCombination, videoCapture, preview)
-        }
+    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_LegacyApi() {
+        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice() {
+    fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice_ResolutionSelector() {
+        suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun suggestedResolutionsForCustomizeResolutionsNotSupportedInLegacyDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
         )
@@ -744,7 +858,7 @@
             Pair.create(ImageFormat.PRIVATE, arrayOf(PREVIEW_SIZE))
         )
         val videoCapture: VideoCapture<TestVideoOutput> = createVideoCapture(Quality.UHD)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             supportedResolutions = previewResolutionsPairs
         )
@@ -756,53 +870,32 @@
     }
 
     @Test
-    fun legacyVideo_getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
-        )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
-            supportedSurfaceCombination,
-            imageCapture,
-            videoCapture,
-            preview
-        )
-        // (PRIV, PREVIEW) + (PRIV, RECORD) + (JPEG, RECORD)
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RECORD_SIZE)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(LEGACY_VIDEO_MAXIMUM_SIZE)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(PREVIEW_SIZE)
+    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_LegacyApi() {
+        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice() {
+    fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice_ResolutionSelector() {
+        getSuggestedResolutionsForMixedUseCaseInLimitedDevice(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun getSuggestedResolutionsForMixedUseCaseInLimitedDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture(Quality.HIGHEST)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -821,24 +914,38 @@
     // VideoCapture should have higher priority to choose size than ImageCapture.
     @Test
     @Throws(CameraUnavailableException::class)
-    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage() {
+    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_LegacyApi() {
+        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(legacyUseCaseCreator)
+    }
+
+    @Test
+    @Throws(CameraUnavailableException::class)
+    fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage_ResolutionSelector() {
+        getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun getSuggestedResolutionsInFullDevice_videoHasHigherPriorityThanImage(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val videoCapture = createVideoCapture(QualitySelector.from(
             Quality.UHD,
             FallbackStrategy.lowerQualityOrHigherThan(Quality.UHD)
         ))
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -855,25 +962,38 @@
     }
 
     @Test
-    fun getSuggestedResolutionsInFullDevice_videoRecordSizeLowPriority_imageCanGetMaxSize() {
+    fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_LegacyApi() {
+        imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority_ResolutionSelector() {
+        imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun imageCaptureCanGetMaxSizeInFullDevice_videoRecordSizeLowPriority(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_4_3 // mMaximumSize(4032x3024) is 4:3
+            preferredAspectRatio = AspectRatio.RATIO_4_3 // mMaximumSize(4032x3024) is 4:3
         )
         val videoCapture = createVideoCapture(
             QualitySelector.fromOrderedList(
-                listOf<androidx.camera.video.Quality>(Quality.HD, Quality.FHD, Quality.UHD)
+                listOf<Quality>(Quality.HD, Quality.FHD, Quality.UHD)
             )
         )
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -890,9 +1010,50 @@
     }
 
     @Test
-    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases() {
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_LegacyApi() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            legacyUseCaseCreator,
+            DISPLAY_SIZE
+        )
+    }
+
+    @Test
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_SensorSize() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            resolutionSelectorUseCaseCreator,
+            PREVIEW_SIZE
+        )
+    }
+
+    @Test
+    fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases_RS_ViewSize() {
+        getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+            viewSizeResolutionSelectorUseCaseCreator,
+            PREVIEW_SIZE
+        )
+    }
+
+    private fun getSuggestedResolutionsWithSameSupportedListForDifferentUseCases(
+        useCaseCreator: UseCaseCreator,
+        preferredResolution: Size
+    ) {
         setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
+            supportedSizes = arrayOf(
+                Size(4032, 3024), // 4:3
+                Size(3840, 2160), // 16:9
+                Size(1920, 1440), // 4:3
+                Size(1920, 1080), // 16:9
+                Size(1280, 960), // 4:3
+                Size(1280, 720), // 16:9
+                Size(1280, 720), // duplicate the size since Nexus 5X emulator has the case.
+                Size(960, 544), // a mod16 version of resolution with 16:9 aspect ratio.
+                Size(800, 450), // 16:9
+                Size(640, 480), // 4:3
+                Size(320, 240), // 4:3
+                Size(320, 180), // 16:9
+                Size(256, 144) // 16:9 For checkSmallSizesAreFilteredOut test.
+            )
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
@@ -903,9 +1064,18 @@
         2. supportedOutputSizes for ImageCapture and Preview in
         SupportedSurfaceCombination#getAllPossibleSizeArrangements are the same.
         */
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE, targetResolution = DISPLAY_SIZE)
-        val preview = createUseCase(PREVIEW_USE_CASE, targetResolution = DISPLAY_SIZE)
-        val imageAnalysis = createUseCase(IMAGE_ANALYSIS_USE_CASE, targetResolution = DISPLAY_SIZE)
+        val imageCapture = useCaseCreator.createUseCase(
+            IMAGE_CAPTURE_USE_CASE,
+            preferredResolution = preferredResolution
+        )
+        val preview = useCaseCreator.createUseCase(
+            PREVIEW_USE_CASE,
+            preferredResolution = preferredResolution
+        )
+        val imageAnalysis = useCaseCreator.createUseCase(
+            IMAGE_ANALYSIS_USE_CASE,
+            preferredResolution = preferredResolution
+        )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
             imageCapture,
@@ -918,24 +1088,33 @@
     }
 
     @Test
-    fun setTargetAspectRatioForMixedUseCases() {
+    fun setTargetAspectRatioForMixedUseCases_LegacyApi() {
+        setTargetAspectRatioForMixedUseCases(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun setTargetAspectRatioForMixedUseCases_ResolutionSelector() {
+        setTargetAspectRatioForMixedUseCases(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun setTargetAspectRatioForMixedUseCases(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9
+            preferredAspectRatio = AspectRatio.RATIO_16_9
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -964,45 +1143,18 @@
     }
 
     @Test
-    fun legacyVideo_getSuggestedResolutionsForCustomizedSupportedResolutions() {
-        setupCameraAndInitCameraX(
-            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
-        )
-        val supportedSurfaceCombination = SupportedSurfaceCombination(
-            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
-        )
-        val formatResolutionsPairList = arrayListOf<Pair<Int, Array<Size>>>().apply {
-            add(Pair.create(ImageFormat.JPEG, arrayOf(RESOLUTION_VGA)))
-            add(Pair.create(ImageFormat.YUV_420_888, arrayOf(RESOLUTION_VGA)))
-            add(Pair.create(ImageFormat.PRIVATE, arrayOf(RESOLUTION_VGA)))
-        }
-        // Sets use cases customized supported resolutions to 640x480 only.
-        val imageCapture = createUseCase(
-            IMAGE_CAPTURE_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val videoCapture = createUseCase(
-            VIDEO_CAPTURE_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val preview = createUseCase(
-            PREVIEW_USE_CASE,
-            supportedResolutions = formatResolutionsPairList
-        )
-        val suggestedResolutionMap = getSuggestedResolutionMap(
-            supportedSurfaceCombination,
-            imageCapture,
-            videoCapture,
-            preview
-        )
-        // Checks all suggested resolutions will become 640x480.
-        assertThat(suggestedResolutionMap[imageCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[videoCapture]).isEqualTo(RESOLUTION_VGA)
-        assertThat(suggestedResolutionMap[preview]).isEqualTo(RESOLUTION_VGA)
+    fun getSuggestedResolutionsForCustomizedSupportedResolutions_LegacyApi() {
+        getSuggestedResolutionsForCustomizedSupportedResolutions(legacyUseCaseCreator)
     }
 
     @Test
-    fun getSuggestedResolutionsForCustomizedSupportedResolutions() {
+    fun getSuggestedResolutionsForCustomizedSupportedResolutions_ResolutionSelector() {
+        getSuggestedResolutionsForCustomizedSupportedResolutions(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun getSuggestedResolutionsForCustomizedSupportedResolutions(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -1015,12 +1167,12 @@
             add(Pair.create(ImageFormat.PRIVATE, arrayOf(RESOLUTION_VGA)))
         }
         // Sets use cases customized supported resolutions to 640x480 only.
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
             supportedResolutions = formatResolutionsPairList
         )
         val videoCapture = createVideoCapture(Quality.SD)
-        val preview = createUseCase(
+        val preview = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             supportedResolutions = formatResolutionsPairList
         )
@@ -1154,17 +1306,27 @@
     }
 
     @Test
-    fun isAspectRatioMatchWithSupportedMod16Resolution() {
+    fun isAspectRatioMatchWithSupportedMod16Resolution_LegacyApi() {
+        isAspectRatioMatchWithSupportedMod16Resolution(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun isAspectRatioMatchWithSupportedMod16Resolution_ResolutionSelector() {
+        isAspectRatioMatchWithSupportedMod16Resolution(resolutionSelectorUseCaseCreator)
+    }
+
+    private fun isAspectRatioMatchWithSupportedMod16Resolution(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
-            targetAspectRatio = AspectRatio.RATIO_16_9,
-            defaultResolution = MOD16_SIZE
+            Surface.ROTATION_90,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            preferredResolution = MOD16_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
         assertThat(suggestedResolutionMap[useCase]).isEqualTo(MOD16_SIZE)
@@ -1196,7 +1358,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(FAKE_USE_CASE)
+        val useCase = createUseCaseByLegacyApi(FAKE_USE_CASE)
         // There is default minimum size 640x480 setting. Sizes smaller than 640x480 will be
         // removed. No any aspect ratio related setting. The returned sizes list will be sorted in
         // descending order.
@@ -1223,7 +1385,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_4_3
         )
@@ -1249,14 +1411,14 @@
     }
 
     @Test
-    fun getSupportedOutputSizes_aspectRatio16x9() {
+    fun getSupportedOutputSizes_aspectRatio16x9_InLimitedDevice() {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -1287,7 +1449,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -1335,14 +1497,14 @@
     }
 
     @Test
-    fun getSupportedOutputSizes_targetResolution1080x1920InRotation0() {
+    fun getSupportedOutputSizes_targetResolution1080x1920InRotation0_InLimitedDevice() {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 1920)
         )
@@ -1375,7 +1537,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 1920)
         )
@@ -1429,7 +1591,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1280, 960)
@@ -1464,7 +1626,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 240)
@@ -1494,7 +1656,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(320, 240)
         )
@@ -1518,7 +1680,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1800, 1440)
@@ -1553,7 +1715,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1280, 600)
@@ -1585,7 +1747,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(1280, 720)
         )
@@ -1598,7 +1760,20 @@
     }
 
     @Test
-    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution() {
+    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_LegacyApi() {
+        previewCanSelectResolutionLargerThanDisplay_withMaxResolution(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution_ResolutionSelector() {
+        previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun previewCanSelectResolutionLargerThanDisplay_withMaxResolution(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
@@ -1606,7 +1781,7 @@
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // The max resolution is expressed in the sensor coordinate.
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             PREVIEW_USE_CASE,
             maxResolution = MAXIMUM_SIZE
         )
@@ -1623,7 +1798,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             defaultResolution = Size(1280, 720)
         )
@@ -1644,7 +1819,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             defaultResolution = Size(1280, 720),
@@ -1685,7 +1860,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(1920, 1080)
@@ -1712,7 +1887,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(320, 240)
         )
@@ -1739,7 +1914,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 240),
@@ -1769,7 +1944,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 180),
@@ -1793,7 +1968,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(3840, 2160),
@@ -1834,7 +2009,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(320, 190),
@@ -1864,7 +2039,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(192, 144)
@@ -1891,7 +2066,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             maxResolution = Size(192, 144)
         )
@@ -1917,7 +2092,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
             targetResolution = Size(185, 90)
@@ -1951,7 +2126,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetResolution = Size(1080, 2016)
         )
@@ -1990,7 +2165,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2036,7 +2211,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2070,7 +2245,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2113,7 +2288,7 @@
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val useCase = createUseCase(
+        val useCase = createUseCaseByLegacyApi(
             FAKE_USE_CASE,
             targetAspectRatio = AspectRatio.RATIO_16_9
         )
@@ -2152,7 +2327,21 @@
     }
 
     @Test
-    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists() {
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_LegacyApi() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_SensorSize() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(resolutionSelectorUseCaseCreator)
+    }
+
+    @Test
+    fun canGet640x480_whenAnotherGroupMatchedInMod16Exists_RS_ViewSize() {
+        canGet640x480_whenAnotherGroupMatchedInMod16Exists(viewSizeResolutionSelectorUseCaseCreator)
+    }
+
+    private fun canGet640x480_whenAnotherGroupMatchedInMod16Exists(useCaseCreator: UseCaseCreator) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
             supportedSizes = arrayOf(
@@ -2171,10 +2360,10 @@
         )
         // Sets the target resolution as 640x480 with target rotation as ROTATION_90 because the
         // sensor orientation is 90.
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RESOLUTION_VGA
+            preferredResolution = RESOLUTION_VGA
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
         // Checks 640x480 is final selected for the use case.
@@ -2182,7 +2371,20 @@
     }
 
     @Test
-    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet() {
+    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_LegacyApi() {
+        canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(legacyUseCaseCreator)
+    }
+
+    @Test
+    fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet_ResolutionSelector() {
+        canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun canGetSupportedSizeSmallerThan640x480_whenLargerMaxResolutionIsSet(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
             supportedSizes = arrayOf(Size(480, 480))
@@ -2191,7 +2393,7 @@
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
         // Sets the max resolution as 720x1280
-        val useCase = createUseCase(
+        val useCase = useCaseCreator.createUseCase(
             FAKE_USE_CASE,
             maxResolution = DISPLAY_SIZE
         )
@@ -2201,14 +2403,40 @@
     }
 
     @Test
-    fun previewSizeIsSelectedForImageAnalysis_imageCaptureHasNoSetSizeInLimitedDevice() {
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_LegacyApi() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            legacyUseCaseCreator, PREVIEW_SIZE
+        )
+    }
+
+    // For the ResolutionSelector API, RECORD_SIZE can't be used because it exceeds
+    // PREVIEW_SIZE. Therefore, the logic will fallback to select a 4:3 PREVIEW_SIZE. Then,
+    // 640x480 will be selected.
+    @Test
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_SensorSize() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            resolutionSelectorUseCaseCreator, RESOLUTION_VGA
+        )
+    }
+
+    @Test
+    fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice_RS_ViewSize() {
+        previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+            viewSizeResolutionSelectorUseCaseCreator, RESOLUTION_VGA
+        )
+    }
+
+    private fun previewSizeIsSelectedForImageAnalysis_withImageCaptureInLimitedDevice(
+        useCaseCreator: UseCaseCreator,
+        expectedResult: Size
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -2218,7 +2446,7 @@
             )
         )
         // ImageCapture has no explicit target resolution setting
-        val imageCapture = createUseCase(IMAGE_CAPTURE_USE_CASE)
+        val imageCapture = useCaseCreator.createUseCase(IMAGE_CAPTURE_USE_CASE)
         // A LEGACY-level above device supports the following configuration.
         //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
         //
@@ -2228,10 +2456,10 @@
         // Even there is a RECORD size target resolution setting for ImageAnalysis, ImageCapture
         // will still have higher priority to have a MAXIMUM size resolution if the app doesn't
         // explicitly specify a RECORD size target resolution to ImageCapture.
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -2239,18 +2467,40 @@
             imageCapture,
             imageAnalysis
         )
-        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(PREVIEW_SIZE)
+        assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(expectedResult)
     }
 
     @Test
-    fun recordSizeIsSelectedForImageAnalysis_imageCaptureHasExplicitSizeInLimitedDevice() {
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_LegacyApi() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            legacyUseCaseCreator
+        )
+    }
+
+    @Test
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_SensorSize() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            resolutionSelectorUseCaseCreator
+        )
+    }
+
+    @Test
+    fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice_RS_ViewSize() {
+        imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+            viewSizeResolutionSelectorUseCaseCreator
+        )
+    }
+
+    private fun imageAnalysisSelectRecordSize_imageCaptureHasExplicitSizeInLimitedDevice(
+        useCaseCreator: UseCaseCreator
+    ) {
         setupCameraAndInitCameraX(
             hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED
         )
         val supportedSurfaceCombination = SupportedSurfaceCombination(
             context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
         )
-        val preview = createUseCase(PREVIEW_USE_CASE) as Preview
+        val preview = useCaseCreator.createUseCase(PREVIEW_USE_CASE) as Preview
         preview.setSurfaceProvider(
             CameraXExecutors.directExecutor(),
             SurfaceTextureProvider.createSurfaceTextureProvider(
@@ -2260,10 +2510,10 @@
             )
         )
         // ImageCapture has no explicit RECORD size target resolution setting
-        val imageCapture = createUseCase(
+        val imageCapture = useCaseCreator.createUseCase(
             IMAGE_CAPTURE_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         // A LEGACY-level above device supports the following configuration.
         //     PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM
@@ -2274,10 +2524,10 @@
         // A RECORD can be selected for ImageAnalysis if the ImageCapture has a explicit RECORD
         // size target resolution setting. It means that the application know the trade-off and
         // the ImageAnalysis has higher priority to get a larger resolution than ImageCapture.
-        val imageAnalysis = createUseCase(
+        val imageAnalysis = useCaseCreator.createUseCase(
             IMAGE_ANALYSIS_USE_CASE,
             targetRotation = Surface.ROTATION_90,
-            targetResolution = RECORD_SIZE
+            preferredResolution = RECORD_SIZE
         )
         val suggestedResolutionMap = getSuggestedResolutionMap(
             supportedSurfaceCombination,
@@ -2288,6 +2538,92 @@
         assertThat(suggestedResolutionMap[imageAnalysis]).isEqualTo(RECORD_SIZE)
     }
 
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsSelected_whenHighResolutionIsEnabled() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 6000))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsNotSelected_whenHighResolutionIsEnabled_withoutBurstCaptureCapability() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(FAKE_USE_CASE, highResolutionEnabled = true)
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsNotSelected_whenHighResolutionIsNotEnabled_targetResolution8000x6000() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase =
+            createUseCaseByResolutionSelector(FAKE_USE_CASE, preferredResolution = Size(8000, 6000))
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(4032, 3024))
+    }
+
+    @Config(minSdk = Build.VERSION_CODES.M)
+    @Test
+    fun highResolutionIsSelected_whenHighResolutionIsEnabled_aspectRatio16x9() {
+        setupCameraAndInitCameraX(
+            hardwareLevel = CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
+            capabilities = intArrayOf(
+                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE
+            ),
+            supportedHighResolutionSizes = arrayOf(Size(8000, 6000), Size(8000, 4500))
+        )
+        val supportedSurfaceCombination = SupportedSurfaceCombination(
+            context, DEFAULT_CAMERA_ID, cameraManagerCompat!!, mockCamcorderProfileHelper
+        )
+
+        val useCase = createUseCaseByResolutionSelector(
+            FAKE_USE_CASE,
+            preferredAspectRatio = AspectRatio.RATIO_16_9,
+            highResolutionEnabled = true
+        )
+        val suggestedResolutionMap = getSuggestedResolutionMap(supportedSurfaceCombination, useCase)
+
+        // Checks 8000x6000 is final selected for the use case.
+        assertThat(suggestedResolutionMap[useCase]).isEqualTo(Size(8000, 4500))
+    }
+
     /**
      * Sets up camera according to the specified settings and initialize [CameraX].
      *
@@ -2308,33 +2644,18 @@
         sensorOrientation: Int = SENSOR_ORIENTATION_90,
         pixelArraySize: Size = LANDSCAPE_PIXEL_ARRAY_SIZE,
         supportedSizes: Array<Size> = DEFAULT_SUPPORTED_SIZES,
+        supportedHighResolutionSizes: Array<Size>? = null,
         capabilities: IntArray? = null
     ) {
-        val mockMap = Mockito.mock(StreamConfigurationMap::class.java).also {
-            Mockito.`when`(it.getOutputSizes(ArgumentMatchers.anyInt())).thenReturn(supportedSizes)
-            // ImageFormat.PRIVATE was supported since API level 23. Before that, the supported
-            // output sizes need to be retrieved via SurfaceTexture.class.
-            Mockito.`when`(it.getOutputSizes(SurfaceTexture::class.java)).thenReturn(supportedSizes)
-            // This is setup for the test to determine RECORD size from StreamConfigurationMap
-            Mockito.`when`(it.getOutputSizes(MediaRecorder::class.java)).thenReturn(supportedSizes)
-        }
-
-        val characteristics = ShadowCameraCharacteristics.newCameraCharacteristics()
-        Shadow.extract<ShadowCameraCharacteristics>(characteristics).apply {
-            set(CameraCharacteristics.LENS_FACING, CameraCharacteristics.LENS_FACING_BACK)
-            set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL, hardwareLevel)
-            set(CameraCharacteristics.SENSOR_ORIENTATION, sensorOrientation)
-            set(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE, pixelArraySize)
-            set(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP, mockMap)
-            capabilities?.let {
-                set(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES, it)
-            }
-        }
-
-        val cameraManager = ApplicationProvider.getApplicationContext<Context>()
-            .getSystemService(Context.CAMERA_SERVICE) as CameraManager
-        (Shadow.extract<Any>(cameraManager) as ShadowCameraManager)
-            .addCamera(cameraId, characteristics)
+        setupCamera(
+            cameraId,
+            hardwareLevel,
+            sensorOrientation,
+            pixelArraySize,
+            supportedSizes,
+            supportedHighResolutionSizes,
+            capabilities
+        )
 
         @LensFacing val lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
             CameraCharacteristics.LENS_FACING_BACK
@@ -2482,7 +2803,7 @@
      * @param supportedResolutions the customized supported resolutions. Default is null.
      */
     @Suppress("DEPRECATION")
-    private fun createUseCase(
+    private fun createUseCaseByLegacyApi(
         useCaseType: Int,
         targetRotation: Int = UNKNOWN_ROTATION,
         targetAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
@@ -2495,7 +2816,6 @@
             PREVIEW_USE_CASE -> Preview.Builder()
             IMAGE_CAPTURE_USE_CASE -> ImageCapture.Builder()
             IMAGE_ANALYSIS_USE_CASE -> ImageAnalysis.Builder()
-            VIDEO_CAPTURE_USE_CASE -> androidx.camera.core.VideoCapture.Builder()
             else -> FakeUseCaseConfig.Builder(UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE)
         }
         if (targetRotation != UNKNOWN_ROTATION) {
@@ -2548,4 +2868,17 @@
             this.sourceState = sourceState
         }
     }
+
+    private interface UseCaseCreator {
+        fun createUseCase(
+            useCaseType: Int,
+            targetRotation: Int = UNKNOWN_ROTATION,
+            preferredAspectRatio: Int = UNKNOWN_ASPECT_RATIO,
+            preferredResolution: Size? = null,
+            maxResolution: Size? = null,
+            highResolutionEnabled: Boolean = false,
+            defaultResolution: Size? = null,
+            supportedResolutions: List<Pair<Int, Array<Size>>>? = null,
+        ): UseCase
+    }
 }
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
index 5771cbb..4fae45a 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/FakeOtherUseCaseConfig.java
@@ -193,5 +193,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
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 24f4e72..d5b344c 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
@@ -32,15 +32,15 @@
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth
+import java.lang.ref.PhantomReference
+import java.lang.ref.ReferenceQueue
+import java.util.concurrent.TimeoutException
+import java.util.concurrent.atomic.AtomicReference
 import org.junit.After
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentMatchers
 import org.mockito.Mockito
-import java.lang.ref.PhantomReference
-import java.lang.ref.ReferenceQueue
-import java.util.concurrent.TimeoutException
-import java.util.concurrent.atomic.AtomicReference
 
 @SmallTest
 @RunWith(AndroidJUnit4::class)
@@ -361,7 +361,8 @@
     companion object {
         private val FAKE_SIZE: Size by lazy { Size(0, 0) }
         private val FAKE_INFO: SurfaceRequest.TransformationInfo by lazy {
-            SurfaceRequest.TransformationInfo.of(Rect(), 0, Surface.ROTATION_0)
+            SurfaceRequest.TransformationInfo.of(Rect(), 0, Surface.ROTATION_0,
+                /*hasCameraTransform=*/true)
         }
         private val NO_OP_RESULT_LISTENER = Consumer { _: SurfaceRequest.Result? -> }
         private val MOCK_SURFACE = Mockito.mock(
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 3f88ffa..884de88 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
@@ -22,7 +22,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.ImageFormatConstants
@@ -315,7 +314,6 @@
             CameraEffect.PREVIEW,
             ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE,
             Size(WIDTH, HEIGHT),
-            USE_SURFACE_TEXTURE_TRANSFORM,
             Size(WIDTH, HEIGHT),
             Rect(0, 0, WIDTH, HEIGHT),
             /*rotationDegrees=*/0,
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
index 9cd621b..f6d8e3c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageAnalysis.java
@@ -23,6 +23,7 @@
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_FORMAT;
 import static androidx.camera.core.impl.ImageAnalysisConfig.OPTION_OUTPUT_IMAGE_ROTATION_ENABLED;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -31,6 +32,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_TARGET_CLASS;
@@ -263,9 +265,35 @@
                     ? mSubscribedAnalyzer.getDefaultTargetResolution() : null;
         }
 
-        if (analyzerResolution != null
-                && !builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
-            builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION, analyzerResolution);
+        if (analyzerResolution != null) {
+            if (!builder.getMutableConfig().containsOption(OPTION_RESOLUTION_SELECTOR)) {
+                int targetRotation = builder.getMutableConfig().retrieveOption(
+                        OPTION_TARGET_ROTATION, Surface.ROTATION_0);
+                // analyzerResolution is a size in the sensor coordinate system, but the legacy
+                // target resolution setting is in the view coordinate system. Flips the
+                // analyzerResolution according to the sensor rotation degrees.
+                if (cameraInfo.getSensorRotationDegrees(targetRotation) % 180 == 90) {
+                    analyzerResolution = new Size(/* width= */ analyzerResolution.getHeight(),
+                            /* height= */ analyzerResolution.getWidth());
+                }
+
+                if (!builder.getUseCaseConfig().containsOption(OPTION_TARGET_RESOLUTION)) {
+                    builder.getMutableConfig().insertOption(OPTION_TARGET_RESOLUTION,
+                            analyzerResolution);
+                }
+            } else {
+                // Merges analyzerResolution or default resolution to ResolutionSelector.
+                ResolutionSelector resolutionSelector =
+                        builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR);
+
+                if (resolutionSelector.getPreferredResolution() == null) {
+                    ResolutionSelector.Builder resolutionSelectorBuilder =
+                            ResolutionSelector.Builder.fromSelector(resolutionSelector);
+                    resolutionSelectorBuilder.setPreferredResolution(analyzerResolution);
+                    builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+                            resolutionSelectorBuilder.build());
+                }
+            }
         }
 
         return builder.getUseCaseConfig();
@@ -1119,15 +1147,9 @@
         @Override
         @NonNull
         public ImageAnalysis build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same"
-                                + " config.");
-            }
-            return new ImageAnalysis(getUseCaseConfig());
+            ImageAnalysisConfig imageAnalysisConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(imageAnalysisConfig);
+            return new ImageAnalysis(imageAnalysisConfig);
         }
 
         // Implementations of TargetConfig.Builder default methods
@@ -1317,6 +1339,55 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>ImageAnalysis has a default minimal bounding size as 640x480. The input
+         * {@link ResolutionSelector}'s' preferred resolution can override the minimal bounding
+         * size to find the best resolution.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, which resolution will
+         * be finally selected will depend on the camera device's hardware level, capabilities
+         * and the bound use cases combination. The device hardware level and capabilities
+         * information can be retrieved via the interop class
+         * {@link androidx.camera.camera2.interop.Camera2CameraInfo#getCameraCharacteristic(android.hardware.camera2.CameraCharacteristics.Key)}
+         * with
+         * {@link android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL} and
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES}.
+         *
+         * <p>A {@code LIMITED-level} above device can support a {@code RECORD} size resolution
+         * for {@link ImageAnalysis} when it is bound together with {@link Preview} and
+         * {@link ImageCapture}. The trade-off is the selected resolution for the
+         * {@link ImageCapture} will also be restricted by the {@code RECORD} size. To
+         * successfully select a {@code RECORD} size resolution for {@link ImageAnalysis}, a
+         * {@code RECORD} size preferred resolution should be set on both {@link ImageCapture} and
+         * {@link ImageAnalysis}. This indicates that the application clearly understand the
+         * trade-off and prefer the {@link ImageAnalysis} to have a larger resolution rather than
+         * the {@link ImageCapture} to have a {@code MAXIMUM} size resolution. For the
+         * definitions of {@code RECORD}, {@code MAXIMUM} sizes and more details see the
+         * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
+         * section in {@link android.hardware.camera2.CameraDevice}'s. The {@code RECORD} size
+         * refers to the camera device's maximum supported recording resolution, as determined by
+         * {@link CamcorderProfile}. The {@code MAXIMUM} size refers to the camera device's
+         * maximum output resolution for that format or target from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link ImageAnalysis} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1421,5 +1492,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return 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 a689360..df23742 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
@@ -41,7 +41,9 @@
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.core.impl.ImageCaptureConfig.OPTION_USE_SOFTWARE_JPEG_ENCODER;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.TransformUtils.is90or270;
@@ -1657,8 +1659,10 @@
         // RejectedExecutionException if a ProcessingImageReader is used to processing the
         // captured images.
         ExecutorService executorService = mExecutor;
-        imageReaderCloseFuture.addListener(executorService::shutdown,
-                CameraXExecutors.directExecutor());
+        if (executorService != null) {
+            imageReaderCloseFuture.addListener(executorService::shutdown,
+                    CameraXExecutors.directExecutor());
+        }
     }
 
     /**
@@ -1849,7 +1853,9 @@
      *  {@link ImagePipeline}/{@link TakePictureManager}.
      */
 
+    @Nullable
     private ImagePipeline mImagePipeline;
+    @Nullable
     private TakePictureManager mTakePictureManager;
 
     /**
@@ -2053,9 +2059,11 @@
     private void clearPipelineWithNode(boolean keepTakePictureManager) {
         Log.d(TAG, "clearPipelineWithNode");
         checkMainThread();
-        mImagePipeline.close();
-        mImagePipeline = null;
-        if (!keepTakePictureManager) {
+        if (mImagePipeline != null) {
+            mImagePipeline.close();
+            mImagePipeline = null;
+        }
+        if (!keepTakePictureManager && mTakePictureManager != null) {
             mTakePictureManager.abortRequests();
             mTakePictureManager = null;
         }
@@ -2102,7 +2110,7 @@
     @VisibleForTesting
     @NonNull
     TakePictureManager getTakePictureManager() {
-        return mTakePictureManager;
+        return requireNonNull(mTakePictureManager);
     }
 
     // ===== New architecture end =====
@@ -2799,15 +2807,6 @@
         @Override
         @NonNull
         public ImageCapture build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-
             // Update the input format base on the other options set (mainly whether processing
             // is done)
             Integer bufferFormat = getMutableConfig().retrieveOption(OPTION_BUFFER_FORMAT, null);
@@ -2824,7 +2823,9 @@
                 }
             }
 
-            ImageCapture imageCapture = new ImageCapture(getUseCaseConfig());
+            ImageCaptureConfig imageCaptureConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(imageCaptureConfig);
+            ImageCapture imageCapture = new ImageCapture(imageCaptureConfig);
 
             // Makes the crop aspect ratio match the target resolution setting as what mentioned
             // in javadoc of setTargetResolution(). When the target resolution is set, {@link
@@ -3142,6 +3143,33 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>If no resolution selector is set, the largest available resolution will be selected
+         * to use. Usually, users will intend to get the largest still image that the camera
+         * device can support. Unlike {@link Builder#setTargetResolution(Size)},
+         * {@link #setCropAspectRatio(Rational)} won't be automatically called to set the
+         * corresponding value and crop the output image when a target resolution is set. Use
+         * {@link ViewPort} instead if the output images need to be cropped in a specific
+         * aspect ratio.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link ImageCapture} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         /** @hide */
         @NonNull
         @RestrictTo(Scope.LIBRARY_GROUP)
@@ -3305,5 +3333,18 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /**
+         * {@inheritDoc}
+         *
+         * @hide
+         */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 3ab6e95..947ac42f 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -123,7 +123,7 @@
 
         SaveError saveError = null;
         String errorMessage = null;
-        Exception exception = null;
+        Throwable throwable = null;
         try (ImageProxy imageToClose = mImage;
              FileOutputStream output = new FileOutputStream(tempFile)) {
             byte[] bytes = imageToJpegByteArray(mImage, mJpegQuality);
@@ -151,10 +151,14 @@
             }
 
             exif.save();
+        } catch (OutOfMemoryError e) {
+            saveError = SaveError.UNKNOWN;
+            errorMessage = "Processing failed due to low memory.";
+            throwable = e;
         } catch (IOException | IllegalArgumentException e) {
             saveError = SaveError.FILE_IO_FAILED;
             errorMessage = "Failed to write temp file";
-            exception = e;
+            throwable = e;
         } catch (CodecFailedException e) {
             switch (e.getFailureType()) {
                 case ENCODE_FAILED:
@@ -171,10 +175,10 @@
                     errorMessage = "Failed to transcode mImage";
                     break;
             }
-            exception = e;
+            throwable = e;
         }
         if (saveError != null) {
-            postError(saveError, errorMessage, exception);
+            postError(saveError, errorMessage, throwable);
             tempFile.delete();
             return null;
         }
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 8eff18d..585948a 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
@@ -16,9 +16,9 @@
 
 package androidx.camera.core;
 
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM;
 import static androidx.camera.core.impl.ImageInputConfig.OPTION_INPUT_FORMAT;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_APP_TARGET_ROTATION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.PreviewConfig.IMAGE_INFO_PROCESSOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_BACKGROUND_EXECUTOR;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
@@ -34,10 +34,10 @@
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_CLASS;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_NAME;
-import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_RESOLUTION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_TARGET_ROTATION;
 import static androidx.camera.core.impl.PreviewConfig.OPTION_USE_CASE_EVENT_CALLBACK;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAMERA_SELECTOR;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
 
 import static java.util.Collections.singletonList;
@@ -314,7 +314,7 @@
         clearPipeline();
 
         // Create nodes and edges.
-        mNode = new SurfaceProcessorNode(camera, USE_SURFACE_TEXTURE_TRANSFORM, mSurfaceProcessor);
+        mNode = new SurfaceProcessorNode(camera, mSurfaceProcessor);
         SettableSurface cameraSurface = new SettableSurface(
                 CameraEffect.PREVIEW,
                 resolution,
@@ -323,7 +323,7 @@
                 /*hasEmbeddedTransform=*/true,
                 requireNonNull(getCropRect(resolution)),
                 getRelativeRotation(camera),
-                /*mirroring=*/false,
+                /*mirroring=*/isFrontCamera(camera),
                 this::notifyReset);
         SurfaceEdge inputEdge = SurfaceEdge.create(singletonList(cameraSurface));
         SurfaceEdge outputEdge = mNode.transform(inputEdge);
@@ -343,6 +343,11 @@
         return sessionConfigBuilder;
     }
 
+    private static boolean isFrontCamera(@NonNull CameraInternal camera) {
+        Integer lensFacing = camera.getCameraInfoInternal().getLensFacing();
+        return lensFacing != null && lensFacing == CameraSelector.LENS_FACING_FRONT;
+    }
+
     /**
      * Sets a {@link SurfaceProcessorInternal}.
      *
@@ -452,11 +457,16 @@
         SurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
         if (cameraInternal != null && surfaceProvider != null && cropRect != null
                 && surfaceRequest != null) {
-            // TODO: when SurfaceProcessorNode exists, use SettableSurface.setRotationDegrees(int)
-            //  instead. However, this requires PreviewView to rely on relative rotation but not
-            //  target rotation.
-            surfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(cropRect,
-                    getRelativeRotation(cameraInternal), getAppTargetRotation()));
+            if (mNode == null) {
+                surfaceRequest.updateTransformationInfo(SurfaceRequest.TransformationInfo.of(
+                        cropRect,
+                        getRelativeRotation(cameraInternal),
+                        getAppTargetRotation(),
+                        /*hasCameraTransform=*/true));
+            } else {
+                ((SettableSurface) mSessionDeferrableSurface).setRotationDegrees(
+                        getRelativeRotation(cameraInternal));
+            }
         }
     }
 
@@ -628,6 +638,21 @@
             builder.getMutableConfig().insertOption(OPTION_INPUT_FORMAT,
                     ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE);
         }
+
+        // Merges Preview's default max resolution setting when resolution selector is used
+        ResolutionSelector resolutionSelector =
+                builder.getMutableConfig().retrieveOption(OPTION_RESOLUTION_SELECTOR, null);
+        if (resolutionSelector != null && resolutionSelector.getMaxResolution() == null) {
+            Size maxResolution = builder.getMutableConfig().retrieveOption(OPTION_MAX_RESOLUTION);
+            if (maxResolution != null) {
+                ResolutionSelector.Builder resolutionSelectorBuilder =
+                        ResolutionSelector.Builder.fromSelector(resolutionSelector);
+                resolutionSelectorBuilder.setMaxResolution(maxResolution);
+                builder.getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR,
+                        resolutionSelectorBuilder.build());
+            }
+        }
+
         return builder.getUseCaseConfig();
     }
 
@@ -861,16 +886,9 @@
         @NonNull
         @Override
         public Preview build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-
-            return new Preview(getUseCaseConfig());
+            PreviewConfig previewConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(previewConfig);
+            return new Preview(previewConfig);
         }
 
         // Implementations of TargetConfig.Builder default methods
@@ -1069,6 +1087,38 @@
             return this;
         }
 
+        /**
+         * Sets the resolution selector to select the preferred supported resolution.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, the selected
+         * resolution will be limited by the {@code PREVIEW} size which is defined as the best
+         * size match to the device's screen resolution, or to 1080p (1920x1080), whichever is
+         * smaller. See the
+         * <a href="https://developer.android.com/reference/android/hardware/camera2/CameraDevice#regular-capture">Regular capture</a>
+         * section in {@link android.hardware.camera2.CameraDevice}'. If the
+         * {@link ResolutionSelector} contains the max resolution setting larger than the {@code
+         * PREVIEW} size, a size larger than the device's screen resolution or 1080p can be
+         * selected to use for {@link Preview}.
+         *
+         * <p>Note that due to compatibility reasons, CameraX may select a resolution that is
+         * larger than the default screen resolution on certain devices.
+         *
+         * <p>The existing {@link #setTargetResolution(Size)} and
+         * {@link #setTargetAspectRatio(int)} APIs are deprecated and are not compatible with
+         * {@link ResolutionSelector}. Calling any of these APIs together with
+         * {@link ResolutionSelector} will throw an {@link IllegalArgumentException} while
+         * {@link #build()} is called to create the {@link Preview} instance.
+         *
+         * @hide
+         **/
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1200,5 +1250,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
new file mode 100644
index 0000000..6e4a640
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ResolutionSelector.java
@@ -0,0 +1,387 @@
+/*
+ * 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;
+
+import android.util.Size;
+import android.view.View;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.camera.core.impl.SizeCoordinate;
+
+/**
+ * A set of requirements and priorities used to select a resolution for the use case.
+ *
+ * <p>The resolution selection mechanism is determined by the following three steps:
+ * <ol>
+ *     <li> Collect supported output sizes to the candidate resolution list
+ *     <li> Determine the selecting priority of the candidate resolution list by the preference
+ *     settings
+ *     <li> Consider all the resolution selector settings of bound use cases to find the best
+ *     resolution for each use case
+ * </ol>
+ *
+ * <p>For the first step, all supported resolution output sizes are put into the candidate
+ * resolution list as the base in the beginning.
+ *
+ * <p>ResolutionSelector provides the following two functions for applications to adjust the
+ * conditions of the candidate resolutions.
+ * <ul>
+ *     <li> {@link Builder#setMaxResolution(Size)}
+ *     <li> {@link Builder#setHighResolutionEnabled(boolean)}
+ * </ul>
+ *
+ * <p>For the second step, ResolutionSelector provides the following three functions for
+ * applications to determine which resolution has higher priority to be selected.
+ * <ul>
+ *     <li> {@link Builder#setPreferredResolution(Size)}
+ *     <li> {@link Builder#setPreferredResolutionByViewSize(Size)}
+ *     <li> {@link Builder#setPreferredAspectRatio(int)}
+ * </ul>
+ *
+ * <p>The resolution that exactly matches the preferred resolution is selected in first priority.
+ * If the resolution can't be found, CameraX falls back to use the sizes of the preferred aspect
+ * ratio. In this case, the preferred resolution is treated as the minimal bounding size to find
+ * the best resolution.
+ *
+ * <p>Different types of use cases might have their own additional conditions. Please see the use
+ * case config builders’ {@code setResolutionSelector()} function to know the condition details
+ * for each type of use case.
+ *
+ * <p>For the third step, CameraX selects the final resolution for the use case based on the
+ * camera device's hardware level, capabilities and the bound use case combination. Applications
+ * can check which resolution is finally selected by using the use case's {@code
+ * getResolutionInfo()} function.
+ *
+ * @hide
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ResolutionSelector {
+    @Nullable
+    private final Size mPreferredResolution;
+
+    private final SizeCoordinate mSizeCoordinate;
+
+    private final int mPreferredAspectRatio;
+
+    @Nullable
+    private final Size mMaxResolution;
+
+    private final boolean mIsHighResolutionEnabled;
+
+    ResolutionSelector(int preferredAspectRatio,
+            @Nullable Size preferredResolution,
+            @NonNull SizeCoordinate sizeCoordinate,
+            @Nullable Size maxResolution,
+            boolean isHighResolutionEnabled) {
+        mPreferredAspectRatio = preferredAspectRatio;
+        mPreferredResolution = preferredResolution;
+        mSizeCoordinate = sizeCoordinate;
+        mMaxResolution = maxResolution;
+        mIsHighResolutionEnabled = isHighResolutionEnabled;
+    }
+
+    /**
+     * Retrieves the preferred aspect ratio in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @AspectRatio.Ratio
+    public int getPreferredAspectRatio() {
+        return mPreferredAspectRatio;
+    }
+
+    /**
+     * Retrieves the preferred resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public Size getPreferredResolution() {
+        return mPreferredResolution;
+    }
+
+    /**
+     * Retrieves the size coordinate of the preferred resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public SizeCoordinate getSizeCoordinate() {
+        return mSizeCoordinate;
+    }
+
+    /**
+     * Returns the max resolution in the ResolutionSelector.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Nullable
+    public Size getMaxResolution() {
+        return mMaxResolution;
+    }
+
+    /**
+     * Returns {@code true} if high resolutions are allowed to be selected, otherwise {@code false}.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public boolean isHighResolutionEnabled() {
+        return mIsHighResolutionEnabled;
+    }
+
+    /**
+     * Builder for a {@link ResolutionSelector}.
+     */
+    public static final class Builder {
+        @AspectRatio.Ratio
+        private int mPreferredAspectRatio = AspectRatio.RATIO_4_3;
+        @Nullable
+        private Size mPreferredResolution = null;
+        @NonNull
+        private SizeCoordinate mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
+        @Nullable
+        private Size mMaxResolution = null;
+        private boolean mIsHighResolutionEnabled = false;
+
+        /**
+         * Creates a new Builder object.
+         */
+        public Builder() {
+        }
+
+        private Builder(@NonNull ResolutionSelector selector) {
+            mPreferredAspectRatio = selector.getPreferredAspectRatio();
+            mPreferredResolution = selector.getPreferredResolution();
+            mSizeCoordinate = selector.getSizeCoordinate();
+            mMaxResolution = selector.getMaxResolution();
+            mIsHighResolutionEnabled = selector.isHighResolutionEnabled();
+        }
+
+        /**
+         * Generates a Builder from another {@link ResolutionSelector} object.
+         *
+         * @param selector an existing {@link ResolutionSelector}.
+         * @return the new Builder.
+         */
+        @NonNull
+        public static Builder fromSelector(@NonNull ResolutionSelector selector) {
+            return new Builder(selector);
+        }
+
+        /**
+         * Sets the preferred aspect ratio that the output images are expected to have.
+         *
+         * <p>The aspect ratio is the ratio of width to height in the camera sensor's natural
+         * orientation. If set, CameraX finds the sizes that match the aspect ratio with priority
+         * . Among the sizes that match the aspect ratio, the larger the size, the higher the
+         * priority.
+         *
+         * <p>If CameraX can't find any available sizes that match the preferred aspect ratio,
+         * CameraX falls back to select the sizes with the nearest aspect ratio that can contain
+         * the full field of view of the sizes with preferred aspect ratio.
+         *
+         * <p>If preferred aspect ratio is not set, the default aspect ratio is
+         * {@link AspectRatio#RATIO_4_3}, which usually has largest field of view because most
+         * camera sensor are {@code 4:3}.
+         *
+         * <p>This API is useful for apps that want to capture images matching the {@code 16:9}
+         * display aspect ratio. Apps can set preferred aspect ratio as
+         * {@link AspectRatio#RATIO_16_9} to achieve this.
+         *
+         * <p>The actual aspect ratio of the output may differ from the specified preferred
+         * aspect ratio value. Application code should check the resulting output's resolution.
+         *
+         * @param preferredAspectRatio the aspect ratio you prefer to use.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredAspectRatio(@AspectRatio.Ratio int preferredAspectRatio) {
+            mPreferredAspectRatio = preferredAspectRatio;
+            return this;
+        }
+
+        /**
+         * Sets the preferred resolution you expect to select. The resolution is expressed in the
+         * camera sensor's natural orientation (landscape), which means you can set the size
+         * retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes} directly.
+         *
+         * <p>Once the preferred resolution is set, CameraX finds exactly matched size first
+         * regardless of the preferred aspect ratio. This API is useful for apps that want to
+         * select an exact size retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes}.
+         *
+         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
+         * to establish a minimal bound for the given resolution. The actual resolution is the
+         * closest available resolution that is not smaller than the preferred resolution.
+         * However, if no resolution exists that is equal to or larger than the preferred
+         * resolution, the nearest available resolution smaller than the preferred resolution is
+         * chosen.
+         *
+         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
+         * the preferred aspect ratio to find the sizes that either match it or are close to it.
+         * Using preferred resolution as the minimal bound is useful for apps that want to shrink
+         * the size for the surface. For example, for apps that just show the camera preview in a
+         * small view, apps can specify a size smaller than display size. CameraX can effectively
+         * select a smaller size for better efficiency.
+         *
+         * <p>If both {@link Builder#setPreferredResolution(Size)} and
+         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, which one set
+         * later overrides the one set before.
+         *
+         * @param preferredResolution the preferred resolution expressed in the orientation of
+         *                            the device camera sensor coordinate to choose the preferred
+         *                            resolution from supported output sizes list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredResolution(@NonNull Size preferredResolution) {
+            mPreferredResolution = preferredResolution;
+            mSizeCoordinate = SizeCoordinate.CAMERA_SENSOR;
+            return this;
+        }
+
+        /**
+         * Sets the preferred resolution you expect to select. The resolution is expressed in the
+         * Android {@link View} coordinate system.
+         *
+         * <p>For phone devices, the sensor coordinate orientation usually has 90 degrees
+         * difference from the phone device display’s natural orientation. Depending on the
+         * display rotation value when the use case is bound, CameraX transforms the input
+         * resolution into the camera sensor's natural orientation to find the best suitable
+         * resolution.
+         *
+         * <p>Once the preferred resolution is set, CameraX finds the size that exactly matches
+         * the preferred resolution first regardless of the preferred aspect ratio.
+         *
+         * <p>If CameraX can't find the size that matches the preferred resolution, it attempts
+         * to establish a minimal bound for the given resolution. The actual resolution is the
+         * closest available resolution that is not smaller than the preferred resolution.
+         * However, if no resolution exists that is equal to or larger than the preferred
+         * resolution, the nearest available resolution smaller than the preferred resolution is
+         * chosen.
+         *
+         * <p>When the preferred resolution is used as a minimal bound, CameraX also considers
+         * the preferred aspect ratio to find the sizes that either match it or are close to it.
+         * Using Android {@link View} size as preferred resolution is useful for apps that want
+         * to shrink the size for the surface. For example, for apps that just show the camera
+         * preview in a small view, apps can specify the small size of Android {@link View}.
+         * CameraX can effectively select a smaller size for better efficiency.
+         *
+         * <p>If both {@link Builder#setPreferredResolution(Size)} and
+         * {@link Builder#setPreferredResolutionByViewSize(Size)} are invoked, the later setting
+         * overrides the former one.
+         *
+         * @param preferredResolutionByViewSize the preferred resolution expressed in the
+         *                                      orientation of the app layout's Android
+         *                                      {@link View} to choose the preferred resolution
+         *                                      from supported output sizes list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setPreferredResolutionByViewSize(
+                @NonNull Size preferredResolutionByViewSize) {
+            mPreferredResolution = preferredResolutionByViewSize;
+            mSizeCoordinate = SizeCoordinate.ANDROID_VIEW;
+            return this;
+        }
+
+        /**
+         * Sets the max resolution condition for the use case.
+         *
+         * <p>The max resolution prevents the use case to select the sizes which either width or
+         * height exceeds the specified resolution.
+         *
+         * <p>The resolution should be expressed in the camera sensor's natural orientation
+         * (landscape).
+         *
+         * <p>For example, if applications want to select a resolution smaller than a specific
+         * resolution to have better performance, a {@link ResolutionSelector} which sets this
+         * specific resolution as the max resolution can be used. Or, if applications want to
+         * select a larger resolution for a {@link Preview} which has the default max resolution
+         * of the small one of device's screen size and 1080p (1920x1080), use a
+         * {@link ResolutionSelector} with max resolution.
+         *
+         * @param resolution the max resolution limitation to choose from supported output sizes
+         *                   list.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setMaxResolution(@NonNull Size resolution) {
+            mMaxResolution = resolution;
+            return this;
+        }
+
+        /**
+         * Sets whether high resolutions are allowed to be selected for the use cases.
+         *
+         * <p>Calling this function allows the use case to select the high resolution output
+         * sizes if it is supported for the camera device.
+         *
+         * <p>When high resolution is enabled, if an {@link ImageCapture} with
+         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is bound, the
+         * {@link ImageCapture#CAPTURE_MODE_ZERO_SHUTTER_LAG} mode is forced disabled.
+         *
+         * <p>When using the {@code camera-extensions} to enable an extension mode, even if high
+         * resolution is enabled, the supported high resolution output sizes are still excluded
+         * from the candidate resolution list.
+         *
+         * <p>When using the {@code camera-camera2} CameraX implementation, the supported
+         * high resolutions are retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getHighResolutionOutputSizes(int)}.
+         * Be noticed that the high resolution sizes might cause the entire capture session to
+         * not meet the 20 fps frame rate. Even if only an ImageCapture use case selects a high
+         * resolution, it might still impact the FPS of the Preview, ImageAnalysis or
+         * VideoCapture use cases which are bound together. This function only takes effect on
+         * devices with
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
+         * capability. For devices without
+         * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE}
+         * capability, all resolutions can be retrieved from
+         * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputSizes(int)},
+         * but it is not guaranteed to meet >= 20 fps for any resolution in the list.
+         *
+         * @param enabled {@code true} to allow to select high resolution for the use case.
+         * @return the current Builder.
+         */
+        @NonNull
+        public Builder setHighResolutionEnabled(boolean enabled) {
+            mIsHighResolutionEnabled = enabled;
+            return this;
+        }
+
+        /**
+         * Builds the {@link ResolutionSelector}.
+         *
+         * @return the {@link ResolutionSelector} built with the specified resolution settings.
+         */
+        @NonNull
+        public ResolutionSelector build() {
+            return new ResolutionSelector(mPreferredAspectRatio, mPreferredResolution,
+                    mSizeCoordinate, mMaxResolution, mIsHighResolutionEnabled);
+        }
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
index 6bf27c5..1ee0da0 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/SurfaceOutput.java
@@ -177,16 +177,4 @@
             return new AutoValue_SurfaceOutput_Event(code, surfaceOutput);
         }
     }
-
-    /** OpenGL transformation options for SurfaceOutput. */
-    enum GlTransformOptions {
-        /** Apply only the value of {@link SurfaceTexture#getTransformMatrix(float[])}. */
-        USE_SURFACE_TEXTURE_TRANSFORM,
-
-        /**
-         * Discard the value of {@link SurfaceTexture#getTransformMatrix(float[])} and calculate
-         * the transform based on crop rect, rotation degrees and mirroring.
-         */
-        APPLY_CROP_ROTATE_AND_MIRRORING,
-    }
 }
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 523ad9d..88986d5 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
@@ -858,6 +858,29 @@
         public abstract int getTargetRotation();
 
         /**
+         * Whether the {@link Surface} contains the camera transform.
+         *
+         * <p>The {@link Surface} may contain a transformation, which will be used by Android
+         * components such as {@link TextureView} and {@link SurfaceView} to transform the output.
+         * The app may need to handle the transformation differently based on whether this value
+         * exists.
+         *
+         * <ul>
+         * <li>If the producer is the camera, then the {@link Surface} will contain a
+         * transformation that represents the camera orientation. In that case, this method will
+         * return {@code true}.
+         * <li>If the producer is not the camera, for example, if the stream has been edited by
+         * CameraX, then the {@link Surface} will not contain any transformation. In that case,
+         * this method will return {@code false}.
+         * </ul>
+         *
+         * @return true if the producer writes the camera transformation to the {@link Surface}.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        public abstract boolean hasCameraTransform();
+
+        /**
          * Creates new {@link TransformationInfo}
          *
          * <p> Internally public to be used in view artifact tests.
@@ -868,9 +891,10 @@
         @NonNull
         public static TransformationInfo of(@NonNull Rect cropRect,
                 @ImageOutputConfig.RotationDegreesValue int rotationDegrees,
-                @ImageOutputConfig.OptionalRotationValue int targetRotation) {
+                @ImageOutputConfig.OptionalRotationValue int targetRotation,
+                boolean hasCameraTransform) {
             return new AutoValue_SurfaceRequest_TransformationInfo(cropRect, rotationDegrees,
-                    targetRotation);
+                    targetRotation, hasCameraTransform);
         }
 
         // Hides public constructor.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
index fc294a5..515f2b18 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCase.java
@@ -236,6 +236,13 @@
             mergedConfig.removeOption(ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO);
         }
 
+        // Forces disable ZSL when high resolution is enabled.
+        if (mergedConfig.containsOption(ImageOutputConfig.OPTION_RESOLUTION_SELECTOR)
+                && mergedConfig.retrieveOption(
+                ImageOutputConfig.OPTION_RESOLUTION_SELECTOR).isHighResolutionEnabled()) {
+            mergedConfig.insertOption(UseCaseConfig.OPTION_ZSL_DISABLED, true);
+        }
+
         return onMergeConfig(cameraInfo, getUseCaseConfigBuilder(mergedConfig));
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
index 3848aa5..5008b5c 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/UseCaseGroup.java
@@ -16,8 +16,13 @@
 
 package androidx.camera.core;
 
+import static androidx.camera.core.CameraEffect.IMAGE_CAPTURE;
+import static androidx.camera.core.CameraEffect.PREVIEW;
+import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
 import static androidx.core.util.Preconditions.checkArgument;
 
+import static java.util.Objects.requireNonNull;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -25,7 +30,11 @@
 import androidx.lifecycle.Lifecycle;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 
 /**
  * Represents a collection of {@link UseCase}.
@@ -82,10 +91,17 @@
      * A builder for generating {@link UseCaseGroup}.
      */
     public static final class Builder {
+
+        // Allow-list effect targets supported by CameraX.
+        private static final List<Integer> SUPPORTED_TARGETS = Arrays.asList(
+                PREVIEW,
+                IMAGE_CAPTURE);
+
         private ViewPort mViewPort;
         private final List<UseCase> mUseCases;
         private final List<CameraEffect> mEffects;
 
+
         public Builder() {
             mUseCases = new ArrayList<>();
             mEffects = new ArrayList<>();
@@ -101,7 +117,13 @@
         }
 
         /**
-         * Adds a {@link CameraEffect} to the collection
+         * Adds a {@link CameraEffect} to the collection.
+         *
+         * <p>The value of {@link CameraEffect#getTargets()} must be unique and must be one of
+         * the supported values below:
+         * <ul>
+         * <li>{@link CameraEffect#PREVIEW}
+         * </ul>
          *
          * <p>Once added, CameraX will use the {@link CameraEffect}s to process the outputs of
          * the {@link UseCase}s.
@@ -116,6 +138,57 @@
         }
 
         /**
+         * Checks effect targets and throw {@link IllegalArgumentException}.
+         *
+         * <p>Throws exception if the effects 1) contains duplicate targets or 2) contains
+         * effects that is not in the allowlist.
+         */
+        private void checkEffectTargets() {
+            Map<Integer, CameraEffect> targetEffectMap = new HashMap<>();
+            for (CameraEffect effect : mEffects) {
+                int targets = effect.getTargets();
+                if (!SUPPORTED_TARGETS.contains(targets)) {
+                    throw new IllegalArgumentException(String.format(Locale.US,
+                            "Target %s is not in the supported list %s.",
+                            getHumanReadableTargets(targets),
+                            getHumanReadableSupportedTargets()));
+                }
+                if (targetEffectMap.containsKey(effect.getTargets())) {
+                    throw new IllegalArgumentException(String.format(Locale.US,
+                            "%s and %s contain duplicate targets %s.",
+                            requireNonNull(
+                                    targetEffectMap.get(effect.getTargets())).getClass().getName(),
+                            effect.getClass().getName(),
+                            getHumanReadableTargets(targets)));
+                }
+                targetEffectMap.put(effect.getTargets(), effect);
+            }
+        }
+
+        static String getHumanReadableSupportedTargets() {
+            List<String> targetNameList = new ArrayList<>();
+            for (Integer targets : SUPPORTED_TARGETS) {
+                targetNameList.add(getHumanReadableTargets(targets));
+            }
+            return "[" + String.join(", ", targetNameList) + "]";
+        }
+
+        static String getHumanReadableTargets(int targets) {
+            List<String> names = new ArrayList<>();
+            if ((targets & IMAGE_CAPTURE) != 0) {
+                names.add("IMAGE_CAPTURE");
+            }
+            if ((targets & PREVIEW) != 0) {
+                names.add("PREVIEW");
+            }
+
+            if ((targets & VIDEO_CAPTURE) != 0) {
+                names.add("VIDEO_CAPTURE");
+            }
+            return String.join("|", names);
+        }
+
+        /**
          * Adds {@link UseCase} to the collection.
          */
         @NonNull
@@ -130,6 +203,7 @@
         @NonNull
         public UseCaseGroup build() {
             checkArgument(!mUseCases.isEmpty(), "UseCase must not be empty.");
+            checkEffectTargets();
             return new UseCaseGroup(mViewPort, mUseCases, mEffects);
         }
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 6e77562..bd6734b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_RESOLUTION;
@@ -26,6 +27,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
@@ -1495,15 +1497,9 @@
         @Override
         @NonNull
         public VideoCapture build() {
-            // Error at runtime for using both setTargetResolution and setTargetAspectRatio on
-            // the same config.
-            if (getMutableConfig().retrieveOption(OPTION_TARGET_ASPECT_RATIO, null) != null
-                    && getMutableConfig().retrieveOption(OPTION_TARGET_RESOLUTION, null) != null) {
-                throw new IllegalArgumentException(
-                        "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
-                                + "config.");
-            }
-            return new VideoCapture(getUseCaseConfig());
+            VideoCaptureConfig videoCaptureConfig = getUseCaseConfig();
+            ImageOutputConfig.validateConfig(videoCaptureConfig);
+            return new VideoCapture(videoCaptureConfig);
         }
 
         /**
@@ -1753,6 +1749,15 @@
             return this;
         }
 
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1849,6 +1854,15 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 
     /**
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index fbe5b37..b4e15fc 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -37,6 +37,8 @@
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.ImageProxy;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.core.internal.compat.quirk.LowMemoryQuirk;
 import androidx.camera.core.processing.Edge;
 import androidx.camera.core.processing.InternalImageProcessor;
 import androidx.camera.core.processing.Node;
@@ -57,7 +59,7 @@
 public class ProcessingNode implements Node<ProcessingNode.In, Void> {
 
     @NonNull
-    private final Executor mBlockingExecutor;
+    final Executor mBlockingExecutor;
     @Nullable
     final InternalImageProcessor mImageProcessor;
 
@@ -86,7 +88,12 @@
      */
     ProcessingNode(@NonNull Executor blockingExecutor,
             @Nullable InternalImageProcessor imageProcessor) {
-        mBlockingExecutor = blockingExecutor;
+        boolean isLowMemoryDevice = DeviceQuirks.get(LowMemoryQuirk.class) != null;
+        if (isLowMemoryDevice) {
+            mBlockingExecutor = CameraXExecutors.newSequentialExecutor(blockingExecutor);
+        } else {
+            mBlockingExecutor = blockingExecutor;
+        }
         mImageProcessor = imageProcessor;
     }
 
@@ -142,6 +149,9 @@
             }
         } catch (ImageCaptureException e) {
             sendError(request, e);
+        } catch (OutOfMemoryError e) {
+            sendError(request, new ImageCaptureException(
+                    ERROR_UNKNOWN, "Processing failed due to low memory.", e));
         } catch (RuntimeException e) {
             // For unexpected exceptions, throw an ERROR_UNKNOWN ImageCaptureException.
             sendError(request, new ImageCaptureException(ERROR_UNKNOWN, "Processing failed.", e));
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 dd26326..00c3ad3 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
@@ -19,10 +19,13 @@
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.core.util.Preconditions.checkState;
 
+import static java.util.Objects.requireNonNull;
+
 import android.os.Build;
 
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
@@ -50,6 +53,8 @@
     // 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;
+    @Nullable
+    private ListenableFuture<Void> mCaptureRequestFuture;
 
     RequestWithCallback(@NonNull TakePictureRequest takePictureRequest,
             @NonNull TakePictureRequest.RetryControl retryControl) {
@@ -67,6 +72,18 @@
                 });
     }
 
+    /**
+     * Sets the {@link ListenableFuture} associated with camera2 capture request.
+     *
+     * <p>Canceling this future should cancel the request sent to camera2.
+     */
+    @MainThread
+    public void setCaptureRequestFuture(@NonNull ListenableFuture<Void> captureRequestFuture) {
+        checkMainThread();
+        checkState(mCaptureRequestFuture == null, "CaptureRequestFuture can only be set once.");
+        mCaptureRequestFuture = captureRequestFuture;
+    }
+
     @MainThread
     @Override
     public void onImageCaptured() {
@@ -167,6 +184,8 @@
     private void abort() {
         checkMainThread();
         mIsAborted = true;
+        // Cancel the capture request sent to camera2.
+        requireNonNull(mCaptureRequestFuture).cancel(true);
         mCaptureCompleter.set(null);
         mCompleteCompleter.set(null);
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
index 388a07b..a087c88 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/RgbaImageProxy.java
@@ -35,7 +35,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ExperimentalGetImage;
 import androidx.camera.core.ImageInfo;
 import androidx.camera.core.ImageProxy;
@@ -91,7 +90,6 @@
      *
      * <p>The {@link Bitmap} must be {@link Bitmap.Config#ARGB_8888}.
      */
-    @VisibleForTesting
     public RgbaImageProxy(@NonNull Bitmap bitmap, @NonNull Rect cropRect, int rotationDegrees,
             @NonNull Matrix sensorToBuffer, long timestamp) {
         this(createDirectByteBuffer(bitmap),
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 49f346b..184c961 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
@@ -205,7 +205,9 @@
                 mImagePipeline.createRequests(request, requestWithCallback);
         CameraRequest cameraRequest = requireNonNull(requests.first);
         ProcessingRequest processingRequest = requireNonNull(requests.second);
-        submitCameraRequest(cameraRequest, () -> mImagePipeline.postProcess(processingRequest));
+        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest,
+                () -> mImagePipeline.postProcess(processingRequest));
+        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
     }
 
     /**
@@ -234,14 +236,14 @@
      * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
      */
     @MainThread
-    private void submitCameraRequest(
+    private ListenableFuture<Void> submitCameraRequest(
             @NonNull CameraRequest cameraRequest,
             @NonNull Runnable successRunnable) {
         checkMainThread();
         mImageCaptureControl.lockFlashMode();
-        ListenableFuture<Void> submitRequestFuture =
+        ListenableFuture<Void> captureRequestFuture =
                 mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
-        Futures.addCallback(submitRequestFuture, new FutureCallback<Void>() {
+        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
             @Override
             public void onSuccess(@Nullable Void result) {
                 successRunnable.run();
@@ -261,6 +263,7 @@
                 mImageCaptureControl.unlockFlashMode();
             }
         }, mainThreadExecutor());
+        return captureRequestFuture;
     }
 
     @VisibleForTesting
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
index 334bc6c..aa1eaed 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/ImageOutputConfig.java
@@ -26,6 +26,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.AspectRatio;
+import androidx.camera.core.ResolutionSelector;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -88,6 +89,12 @@
     Option<List<Pair<Integer, Size[]>>> OPTION_SUPPORTED_RESOLUTIONS =
             Option.create("camerax.core.imageOutput.supportedResolutions", List.class);
 
+    /**
+     * Option: camerax.core.imageOutput.resolutionSelector
+     */
+    Option<ResolutionSelector> OPTION_RESOLUTION_SELECTOR =
+            Option.create("camerax.core.imageOutput.resolutionSelector", ResolutionSelector.class);
+
     // *********************************************************************************************
 
     /**
@@ -243,6 +250,29 @@
     }
 
     /**
+     * Retrieves the resolution selector can be used by the target from this configuration.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in this
+     * configuration.
+     */
+    @Nullable
+    default ResolutionSelector getResolutionSelector(@Nullable ResolutionSelector valueIfMissing) {
+        return retrieveOption(OPTION_RESOLUTION_SELECTOR, valueIfMissing);
+    }
+
+    /**
+     * Retrieves the resolution selector can be used by the target from this configuration.
+     *
+     * @return The stored value, if it exists in this configuration.
+     * @throws IllegalArgumentException if the option does not exist in this configuration.
+     */
+    @NonNull
+    default ResolutionSelector getResolutionSelector() {
+        return retrieveOption(OPTION_RESOLUTION_SELECTOR);
+    }
+
+    /**
      * Retrieves the supported resolutions can be used by the target from this configuration.
      *
      * <p>Pair list is composed with {@link ImageFormat} and {@link Size} array. The returned
@@ -258,6 +288,39 @@
     }
 
     /**
+     * Checks whether the input config contains any conflicted settings.
+     *
+     * @param config to be validated.
+     * @throws IllegalArgumentException if both the target aspect ratio and the target resolution
+     * settings are contained in the config, or if either the target aspect ratio or the target
+     * resolution is contained when a resolution selector has been set in the config.
+     */
+    static void validateConfig(@NonNull ImageOutputConfig config) {
+        boolean hasTargetAspectRatio = config.hasTargetAspectRatio();
+        boolean hasTargetResolution = config.getTargetResolution(null) != null;
+
+        // Case 1. Error at runtime for using both setTargetResolution and setTargetAspectRatio on
+        // the same config.
+        if (hasTargetAspectRatio && hasTargetResolution) {
+            throw new IllegalArgumentException(
+                    "Cannot use both setTargetResolution and setTargetAspectRatio on the same "
+                            + "config.");
+        }
+
+        ResolutionSelector resolutionSelector = config.getResolutionSelector(null);
+
+        if (resolutionSelector != null) {
+            // Case 2. Error at runtime for using setTargetResolution or setTargetAspectRatio
+            // with setResolutionSelector on the same config.
+            if (hasTargetAspectRatio || hasTargetResolution) {
+                throw new IllegalArgumentException(
+                        "Cannot use setTargetResolution or setTargetAspectRatio with "
+                                + "setResolutionSelector on the same config.");
+            }
+        }
+    }
+
+    /**
      * Builder for a {@link ImageOutputConfig}.
      *
      * @param <B> The top level builder type for which this builder is composed with.
@@ -336,6 +399,15 @@
          */
         @NonNull
         B setSupportedResolutions(@NonNull List<Pair<Integer, Size[]>> resolutionsList);
+
+        /**
+         * Sets the resolution selector can be used by target from this configuration.
+         *
+         * @param resolutionSelector The resolution selector to select a preferred resolution.
+         * @return The current Builder.
+         */
+        @NonNull
+        B setResolutionSelector(@NonNull ResolutionSelector resolutionSelector);
     }
 
     /**
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
similarity index 65%
copy from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
copy to camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
index dfb2685..b1cfbf1 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/SizeCoordinate.java
@@ -14,13 +14,19 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.camera.core.impl;
 
-import androidx.compose.ui.graphics.Color
+/**
+ * The size coordinate system enum.
+ */
+public enum SizeCoordinate {
+    /**
+     * Size is expressed in the camera sensor's natural orientation (landscape).
+     */
+    CAMERA_SENSOR,
 
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+    /**
+     * Size is expressed in the Android View's orientation.
+     */
+    ANDROID_VIEW
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 4acaae6..15c47e1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -88,6 +88,12 @@
     Option<Boolean> OPTION_ZSL_DISABLED =
             Option.create("camerax.core.useCase.zslDisabled", boolean.class);
 
+    /**
+     * Option: camerax.core.useCase.highResolutionDisabled
+     */
+    Option<Boolean> OPTION_HIGH_RESOLUTION_DISABLED =
+            Option.create("camerax.core.useCase.highResolutionDisabled", boolean.class);
+
 
     // *********************************************************************************************
 
@@ -297,6 +303,17 @@
     }
 
     /**
+     * Retrieves the flag whether high resolution is disabled.
+     *
+     * @param valueIfMissing The value to return if this configuration option has not been set.
+     * @return The stored value or <code>valueIfMissing</code> if the value does not exist in
+     * this configuration
+     */
+    default boolean isHigResolutionDisabled(boolean valueIfMissing) {
+        return retrieveOption(OPTION_HIGH_RESOLUTION_DISABLED, valueIfMissing);
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
@@ -392,6 +409,17 @@
         B setZslDisabled(boolean disabled);
 
         /**
+         * Sets high resolution disabled or not.
+         *
+         * <p> High resolution will be disabled when Extension is ON.
+         *
+         * @param disabled True if high resolution should be disabled. Otherwise, should not be
+         *                 disabled.
+         */
+        @NonNull
+        B setHighResolutionDisabled(boolean disabled);
+
+        /**
          * Retrieves the configuration used by this builder.
          *
          * @return the configuration used by this builder.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
index 7528936..e0eac35 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/utils/AspectRatioUtil.java
@@ -18,6 +18,7 @@
 
 import static androidx.camera.core.internal.utils.SizeUtil.getArea;
 
+import android.graphics.RectF;
 import android.util.Rational;
 import android.util.Size;
 
@@ -38,6 +39,7 @@
     public static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
     public static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
     public static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
+
     private static final int ALIGN16 = 16;
 
     private AspectRatioUtil() {
@@ -94,7 +96,6 @@
         return false;
     }
 
-
     private static boolean ratioIntersectsMod16Segment(int height, int mod16Width,
             Rational aspectRatio) {
         Preconditions.checkArgument(mod16Width % 16 == 0);
@@ -104,14 +105,26 @@
                 mod16Width + ALIGN16);
     }
 
-    /** Comparator based on how close they are to the target aspect ratio. */
+    /**
+     * Comparator based on how close they are to the target aspect ratio by comparing the
+     * transformed mapping area in the full FOV ratio space.
+     *
+     * The mapping area will be the region that the images of the specific aspect ratio cropped
+     * from the full FOV images. Therefore, we can compare the mapping areas to know which one is
+     * closer to the mapping area of the target aspect ratio setting.
+     */
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-    public static final class CompareAspectRatiosByDistanceToTargetRatio implements
+    public static final class CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace implements
             Comparator<Rational> {
-        private Rational mTargetRatio;
+        private final Rational mTargetRatio;
+        private final RectF mTransformedMappingArea;
+        private final Rational mFullFovRatio;
 
-        public CompareAspectRatiosByDistanceToTargetRatio(@NonNull Rational targetRatio) {
+        public CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                @NonNull Rational targetRatio, @Nullable Rational fullFovRatio) {
             mTargetRatio = targetRatio;
+            mFullFovRatio = fullFovRatio != null ? fullFovRatio : new Rational(4, 3);
+            mTransformedMappingArea = getTransformedMappingArea(mTargetRatio);
         }
 
         @Override
@@ -120,11 +133,81 @@
                 return 0;
             }
 
-            final Float lhsRatioDelta = Math.abs(lhs.floatValue() - mTargetRatio.floatValue());
-            final Float rhsRatioDelta = Math.abs(rhs.floatValue() - mTargetRatio.floatValue());
+            RectF lhsMappingArea = getTransformedMappingArea(lhs);
+            RectF rhsMappingArea = getTransformedMappingArea(rhs);
 
-            int result = (int) Math.signum(lhsRatioDelta - rhsRatioDelta);
-            return result;
+            boolean isCoveredByLhs = isMappingAreaCovered(lhsMappingArea,
+                    mTransformedMappingArea);
+            boolean isCoveredByRhs = isMappingAreaCovered(rhsMappingArea,
+                    mTransformedMappingArea);
+
+            if (isCoveredByLhs && isCoveredByRhs) {
+                // When both ratios can cover the transformed target aspect mapping area in the
+                // full FOV space, checks which area is smaller to determine which ratio is
+                // closer to the target aspect ratio.
+                return (int) Math.signum(
+                        getMappingAreaSize(lhsMappingArea) - getMappingAreaSize(rhsMappingArea));
+            } else if (isCoveredByLhs) {
+                return -1;
+            } else if (isCoveredByRhs) {
+                return 1;
+            } else {
+                // When both ratios can't cover the transformed target aspect mapping area in the
+                // full FOV space, checks which overlapping area is larger to determine which
+                // ratio is closer to the target aspect ratio.
+                float lhsOverlappingArea = getOverlappingAreaSize(lhsMappingArea,
+                        mTransformedMappingArea);
+                float rhsOverlappingArea = getOverlappingAreaSize(rhsMappingArea,
+                        mTransformedMappingArea);
+                return -((int) Math.signum(lhsOverlappingArea - rhsOverlappingArea));
+            }
+        }
+
+        /**
+         * Returns the rectangle after transforming the input rational into full FOV aspect ratio
+         * space.
+         */
+        private RectF getTransformedMappingArea(Rational ratio) {
+            if (ratio.floatValue() == mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        mFullFovRatio.getDenominator());
+            } else if (ratio.floatValue() > mFullFovRatio.floatValue()) {
+                return new RectF(0, 0, mFullFovRatio.getNumerator(),
+                        (float) ratio.getDenominator() * (float) mFullFovRatio.getNumerator()
+                                / (float) ratio.getNumerator());
+            } else {
+                return new RectF(0, 0,
+                        (float) ratio.getNumerator() * (float) mFullFovRatio.getDenominator()
+                                / (float) ratio.getDenominator(), mFullFovRatio.getDenominator());
+            }
+        }
+
+        /**
+         * Returns {@code true} if the source transformed mapping area can fully cover the target
+         * transformed mapping area. Otherwise, returns {@code false};
+         */
+        private boolean isMappingAreaCovered(RectF sourceMappingArea, RectF targetMappingArea) {
+            return sourceMappingArea.width() >= targetMappingArea.width()
+                    && sourceMappingArea.height() >= targetMappingArea.height();
+        }
+
+        /**
+         * Returns the input mapping area's size value.
+         */
+        private float getMappingAreaSize(RectF mappingArea) {
+            return mappingArea.width() * mappingArea.height();
+        }
+
+        /**
+         * Returns the overlapping area value between the input two mapping areas in the full FOV
+         * space.
+         */
+        private float getOverlappingAreaSize(RectF mappingArea1, RectF mappingArea2) {
+            float overlappingAreaWidth = mappingArea1.width() < mappingArea2.width()
+                    ? mappingArea1.width() : mappingArea2.width();
+            float overlappingAreaHeight = mappingArea1.height() < mappingArea2.height()
+                    ? mappingArea1.height() : mappingArea2.height();
+            return overlappingAreaWidth * overlappingAreaHeight;
         }
     }
 }
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 afcaf8e..801f381 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
@@ -49,6 +49,9 @@
         if (CaptureFailedRetryQuirk.load()) {
             quirks.add(new CaptureFailedRetryQuirk());
         }
+        if (LowMemoryQuirk.load()) {
+            quirks.add(new LowMemoryQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.java
new file mode 100644
index 0000000..c38eefd
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/compat/quirk/LowMemoryQuirk.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.core.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * <p>QuirkSummary
+ *     Bug Id: 235321365
+ *     Description: For the devices in low spec which may not have enough memory to process the
+ *     image cropping and apply effects in parallel.
+ *     Device(s): Samsung Galaxy A5, Motorola Moto G (3rd gen)
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class LowMemoryQuirk implements Quirk {
+
+    // TODO(b/258618028): Making a public API and giving developers the option to set the devices
+    //  having the Quirk.
+    private static final Set<String> DEVICE_MODELS = new HashSet<>(Arrays.asList(
+            "SM-A520W", // Samsung Galaxy A5
+            "MOTOG3" // Motorola Moto G (3rd gen)
+    ));
+
+    static boolean load() {
+        return DEVICE_MODELS.contains(Build.MODEL.toUpperCase(Locale.US));
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
index 0c8df56..1eb7a4a 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/internal/utils/SizeUtil.java
@@ -19,7 +19,12 @@
 import android.util.Size;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.utils.CompareSizesByArea;
+
+import java.util.Collections;
+import java.util.List;
 
 /**
  * Utility class for size related operations.
@@ -40,4 +45,34 @@
     public static int getArea(@NonNull Size size) {
         return size.getWidth() * size.getHeight();
     }
+
+    /**
+     * Returns {@code true} if the source size area is smaller than the target size area.
+     * Otherwise, returns {@code false}.
+     */
+    public static boolean isSmallerByArea(@NonNull Size sourceSize, @NonNull Size targetSize) {
+        return getArea(sourceSize) < getArea(targetSize);
+    }
+
+    /**
+     * Returns {@code true} if any edge of the source size is longer than the corresponding edge of
+     * the target size. Otherwise, returns {@code false}.
+     */
+    public static boolean isLongerInAnyEdge(@NonNull Size sourceSize, @NonNull Size targetSize) {
+        return sourceSize.getWidth() > targetSize.getWidth()
+                || sourceSize.getHeight() > targetSize.getHeight();
+    }
+
+    /**
+     * Returns the size which has the max area in the input size list. Returns null if the input
+     * size list is empty.
+     */
+    @Nullable
+    public static Size getMaxSize(@NonNull List<Size> sizeList) {
+        if (sizeList.isEmpty()) {
+            return null;
+        }
+
+        return Collections.max(sizeList, new CompareSizesByArea());
+    }
 }
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 6fae56c..8584670 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
@@ -37,7 +37,6 @@
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraEffect;
 import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceOutput.GlTransformOptions;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.SurfaceRequest.TransformationInfo;
@@ -254,7 +253,6 @@
      * <p>Do not provide the {@link SurfaceOutput} to external target if the
      * {@link ListenableFuture} fails.
      *
-     * @param glTransformOptions OpenGL transformation options for SurfaceOutput
      * @param resolution         resolution of input image buffer
      * @param cropRect           crop rect of input image buffer
      * @param rotationDegrees    expected rotation to the input image buffer
@@ -262,8 +260,7 @@
      */
     @MainThread
     @NonNull
-    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(
-            @NonNull GlTransformOptions glTransformOptions, @NonNull Size resolution,
+    public ListenableFuture<SurfaceOutput> createSurfaceOutputFuture(@NonNull Size resolution,
             @NonNull Rect cropRect, int rotationDegrees, boolean mirroring) {
         checkMainThread();
         Preconditions.checkState(!mHasConsumer, "Consumer can only be linked once.");
@@ -276,9 +273,9 @@
                     } catch (SurfaceClosedException e) {
                         return Futures.immediateFailedFuture(e);
                     }
-                    SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(
-                            surface, getTargets(), getFormat(), getSize(), glTransformOptions,
-                            resolution, cropRect, rotationDegrees, mirroring);
+                    SurfaceOutputImpl surfaceOutputImpl = new SurfaceOutputImpl(surface,
+                            getTargets(), getFormat(), getSize(), resolution, cropRect,
+                            rotationDegrees, mirroring);
                     surfaceOutputImpl.getCloseFuture().addListener(this::decrementUseCount,
                             directExecutor());
                     mConsumerToNotify = surfaceOutputImpl;
@@ -411,7 +408,8 @@
     private void notifyTransformationInfoUpdate() {
         if (mProviderSurfaceRequest != null) {
             mProviderSurfaceRequest.updateTransformationInfo(
-                    TransformationInfo.of(mCropRect, mRotationDegrees, ROTATION_NOT_SPECIFIED));
+                    TransformationInfo.of(mCropRect, mRotationDegrees, ROTATION_NOT_SPECIFIED,
+                            /*hasCameraTransform=*/hasEmbeddedTransform()));
         }
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 6a2894a..2918de6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
@@ -16,7 +16,6 @@
 
 package androidx.camera.core.processing;
 
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING;
 import static androidx.camera.core.impl.utils.MatrixExt.preRotate;
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
 import static androidx.camera.core.impl.utils.TransformUtils.rotateSize;
@@ -64,7 +63,6 @@
     private final int mFormat;
     @NonNull
     private final Size mSize;
-    private final GlTransformOptions mGlTransformOptions;
     private final Size mInputSize;
     private final Rect mInputCropRect;
     private final int mRotationDegrees;
@@ -93,8 +91,6 @@
             int targets,
             int format,
             @NonNull Size size,
-            // TODO(b/241910577): remove this flag when PreviewView handles cropped stream.
-            @NonNull GlTransformOptions glTransformOptions,
             @NonNull Size inputSize,
             @NonNull Rect inputCropRect,
             int rotationDegree,
@@ -103,20 +99,11 @@
         mTargets = targets;
         mFormat = format;
         mSize = size;
-        mGlTransformOptions = glTransformOptions;
         mInputSize = inputSize;
         mInputCropRect = new Rect(inputCropRect);
         mMirroring = mirroring;
-
-        if (mGlTransformOptions == APPLY_CROP_ROTATE_AND_MIRRORING) {
-            mRotationDegrees = rotationDegree;
-            calculateGlTransform();
-        } else {
-            // TODO(b/241910577): remove this assignment when the PreviewView handles cropped
-            //  stream.
-            mRotationDegrees = 0;
-        }
-
+        mRotationDegrees = rotationDegree;
+        calculateGlTransform();
         mCloseFuture = CallbackToFutureAdapter.getFuture(
                 completer -> {
                     mCloseFutureCompleter = completer;
@@ -248,16 +235,7 @@
     @AnyThread
     @Override
     public void updateTransformMatrix(@NonNull float[] output, @NonNull float[] input) {
-        switch (mGlTransformOptions) {
-            case USE_SURFACE_TEXTURE_TRANSFORM:
-                System.arraycopy(input, 0, output, 0, 16);
-                break;
-            case APPLY_CROP_ROTATE_AND_MIRRORING:
-                System.arraycopy(mGlTransform, 0, output, 0, 16);
-                break;
-            default:
-                throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
-        }
+        System.arraycopy(mGlTransform, 0, output, 0, 16);
     }
 
     /**
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 6007535..347a8ca 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
@@ -36,7 +36,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceOutput.GlTransformOptions;
 import androidx.camera.core.SurfaceProcessor;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.impl.CameraInternal;
@@ -60,7 +59,6 @@
 @SuppressWarnings("UnusedVariable")
 public class SurfaceProcessorNode implements Node<SurfaceEdge, SurfaceEdge> {
 
-    private final GlTransformOptions mGlTransformOptions;
     @NonNull
     final SurfaceProcessorInternal mSurfaceProcessor;
     @NonNull
@@ -74,15 +72,12 @@
     /**
      * Constructs the {@link SurfaceProcessorNode}.
      *
-     * @param cameraInternal     the associated camera instance.
-     * @param glTransformOptions the OpenGL transformation options.
-     * @param surfaceProcessor   the interface to wrap around.
+     * @param cameraInternal   the associated camera instance.
+     * @param surfaceProcessor the interface to wrap around.
      */
     public SurfaceProcessorNode(@NonNull CameraInternal cameraInternal,
-            @NonNull GlTransformOptions glTransformOptions,
             @NonNull SurfaceProcessorInternal surfaceProcessor) {
         mCameraInternal = cameraInternal;
-        mGlTransformOptions = glTransformOptions;
         mSurfaceProcessor = surfaceProcessor;
     }
 
@@ -112,63 +107,43 @@
         final Runnable onSurfaceInvalidated = inputSurface::invalidate;
 
         SettableSurface outputSurface;
-        switch (mGlTransformOptions) {
-            case APPLY_CROP_ROTATE_AND_MIRRORING:
-                Size resolution = inputSurface.getSize();
-                Rect cropRect = inputSurface.getCropRect();
-                int rotationDegrees = inputSurface.getRotationDegrees();
-                boolean mirroring = inputSurface.getMirroring();
+        Size resolution = inputSurface.getSize();
+        Rect cropRect = inputSurface.getCropRect();
+        int rotationDegrees = inputSurface.getRotationDegrees();
+        boolean mirroring = inputSurface.getMirroring();
 
-                // Calculate rotated resolution and cropRect
-                Size rotatedCroppedSize = is90or270(rotationDegrees)
-                        ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
-                        : rectToSize(cropRect);
+        // Calculate rotated resolution and cropRect
+        Size rotatedCroppedSize = is90or270(rotationDegrees)
+                ? new Size(/*width=*/cropRect.height(), /*height=*/cropRect.width())
+                : rectToSize(cropRect);
 
-                // Calculate sensorToBufferTransform
-                android.graphics.Matrix sensorToBufferTransform =
-                        new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
-                android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
-                        new RectF(cropRect), rotationDegrees, mirroring);
-                sensorToBufferTransform.postConcat(imageTransform);
+        // Calculate sensorToBufferTransform
+        android.graphics.Matrix sensorToBufferTransform =
+                new android.graphics.Matrix(inputSurface.getSensorToBufferTransform());
+        android.graphics.Matrix imageTransform = getRectToRect(sizeToRectF(resolution),
+                new RectF(cropRect), rotationDegrees, mirroring);
+        sensorToBufferTransform.postConcat(imageTransform);
 
-                outputSurface = new SettableSurface(
-                        inputSurface.getTargets(),
-                        rotatedCroppedSize,
-                        inputSurface.getFormat(),
-                        sensorToBufferTransform,
-                        // The Surface transform cannot be carried over during buffer copy.
-                        /*hasEmbeddedTransform=*/false,
-                        sizeToRect(rotatedCroppedSize),
-                        /*rotationDegrees=*/0,
-                        /*mirroring=*/false,
-                        onSurfaceInvalidated);
-                break;
-            case USE_SURFACE_TEXTURE_TRANSFORM:
-                // No transform output as placeholder.
-                outputSurface = new SettableSurface(
-                        inputSurface.getTargets(),
-                        inputSurface.getSize(),
-                        inputSurface.getFormat(),
-                        inputSurface.getSensorToBufferTransform(),
-                        // The Surface transform cannot be carried over during buffer copy.
-                        /*hasEmbeddedTransform=*/false,
-                        inputSurface.getCropRect(),
-                        inputSurface.getRotationDegrees(),
-                        inputSurface.getMirroring(),
-                        onSurfaceInvalidated);
-                break;
-            default:
-                throw new AssertionError("Unknown GlTransformOptions: " + mGlTransformOptions);
-        }
+        outputSurface = new SettableSurface(
+                inputSurface.getTargets(),
+                rotatedCroppedSize,
+                inputSurface.getFormat(),
+                sensorToBufferTransform,
+                // The Surface transform cannot be carried over during buffer copy.
+                /*hasEmbeddedTransform=*/false,
+                sizeToRect(rotatedCroppedSize),
+                /*rotationDegrees=*/0,
+                /*mirroring=*/false,
+                onSurfaceInvalidated);
+
         return outputSurface;
     }
 
     private void sendSurfacesToProcessorWhenReady(@NonNull SettableSurface input,
             @NonNull SettableSurface output) {
         SurfaceRequest surfaceRequest = input.createSurfaceRequest(mCameraInternal);
-        Futures.addCallback(output.createSurfaceOutputFuture(mGlTransformOptions,
-                        input.getSize(), input.getCropRect(), input.getRotationDegrees(),
-                        input.getMirroring()),
+        Futures.addCallback(output.createSurfaceOutputFuture(input.getSize(), input.getCropRect(),
+                        input.getRotationDegrees(), input.getMirroring()),
                 new FutureCallback<SurfaceOutput>() {
                     @Override
                     public void onSuccess(@Nullable SurfaceOutput surfaceOutput) {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 9cb4ec3e..0bc5cd9 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.content.Context;
@@ -37,6 +38,7 @@
 import androidx.camera.core.impl.TagBundle;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.internal.CameraUseCaseAdapter;
+import androidx.camera.core.internal.utils.SizeUtil;
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CameraXUtil;
 import androidx.camera.testing.fakes.FakeAppConfig;
@@ -75,6 +77,8 @@
 
     private static final Size APP_RESOLUTION = new Size(100, 200);
     private static final Size ANALYZER_RESOLUTION = new Size(300, 400);
+    private static final Size FLIPPED_ANALYZER_RESOLUTION = new Size(400, 300);
+    private static final Size DEFAULT_RESOLUTION = new Size(640, 480);
 
     private static final int QUEUE_DEPTH = 8;
     private static final int IMAGE_TAG = 0;
@@ -139,31 +143,71 @@
     }
 
     @Test
-    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution() {
-        assertThat(getMergedAnalyzerResolution(APP_RESOLUTION, ANALYZER_RESOLUTION)).isEqualTo(
+    public void canSetQueueDepth() {
+        assertThat(getMergedImageAnalysisConfig(null, null, QUEUE_DEPTH,
+                false).getImageQueueDepth()).isEqualTo(QUEUE_DEPTH);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_legacyApi() {
+        assertThat(getMergedImageAnalysisConfig(APP_RESOLUTION, ANALYZER_RESOLUTION, -1,
+                false).getTargetResolution()).isEqualTo(APP_RESOLUTION);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_doesNotOverridesUseCaseResolution_resolutionSelector() {
+        ImageAnalysisConfig config = getMergedImageAnalysisConfig(APP_RESOLUTION,
+                ANALYZER_RESOLUTION, -1, true);
+        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
                 APP_RESOLUTION);
     }
 
     @Test
-    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution() {
-        assertThat(getMergedAnalyzerResolution(null, ANALYZER_RESOLUTION)).isEqualTo(
+    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_legacyApi() {
+        assertThat(
+                getMergedImageAnalysisConfig(null, ANALYZER_RESOLUTION, -1,
+                        false).getTargetResolution()).isEqualTo(FLIPPED_ANALYZER_RESOLUTION);
+    }
+
+    @Test
+    public void setAnalyzerWithResolution_usedAsDefaultUseCaseResolution_resolutionSelector() {
+        ImageAnalysisConfig config = getMergedImageAnalysisConfig(null,
+                ANALYZER_RESOLUTION, -1, true);
+        assertThat(config.getResolutionSelector().getPreferredResolution()).isEqualTo(
                 ANALYZER_RESOLUTION);
     }
 
     @Test(expected = IllegalArgumentException.class)
-    public void noAppOrAnalyzerResolution_noMergedOption() {
-        getMergedAnalyzerResolution(null, null);
+    public void noAppOrAnalyzerResolution_noMergedOption_legacyApi() {
+        getMergedImageAnalysisConfig(null, null, -1, false).getTargetResolution();
     }
 
     @NonNull
-    private Size getMergedAnalyzerResolution(
+    private ImageAnalysisConfig getMergedImageAnalysisConfig(
             @Nullable Size appResolution,
-            @Nullable Size analyzerResolution) {
-        // Arrange: set up ImageAnalysis with target resolution.
-        ImageAnalysis.Builder builder = new ImageAnalysis.Builder().setImageQueueDepth(QUEUE_DEPTH);
-        if (appResolution != null) {
-            builder.setTargetResolution(appResolution);
+            @Nullable Size analyzerResolution,
+            int queueDepth,
+            boolean useResolutionSelector) {
+        // Arrange: set up ImageAnalysis.
+        ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
+
+        // Sets preferred resolution by ResolutionSelector or legacy API
+        if (useResolutionSelector) {
+            ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder();
+            if (appResolution != null) {
+                resolutionSelectorBuilder.setPreferredResolution(appResolution);
+            }
+            builder.setResolutionSelector(resolutionSelectorBuilder.build());
+        } else {
+            if (appResolution != null) {
+                builder.setTargetResolution(appResolution);
+            }
         }
+
+        if (queueDepth >= 0) {
+            builder.setImageQueueDepth(QUEUE_DEPTH);
+        }
+
         mImageAnalysis = builder.build();
         // Analyzer that overrides the resolution.
         ImageAnalysis.Analyzer analyzer = new ImageAnalysis.Analyzer() {
@@ -181,12 +225,16 @@
         // Act: set the analyzer.
         mImageAnalysis.setAnalyzer(mBackgroundExecutor, analyzer);
 
-        // Assert: only the target resolution is overridden.
-        ImageAnalysisConfig mergedConfig = (ImageAnalysisConfig) mImageAnalysis.mergeConfigs(
-                new FakeCameraInfoInternal(), null, null);
+        return (ImageAnalysisConfig) mImageAnalysis.mergeConfigs(
+                new FakeCameraInfoInternal(90, CameraSelector.LENS_FACING_BACK), null,
+                null);
+    }
 
-        assertThat(mergedConfig.getImageQueueDepth()).isEqualTo(QUEUE_DEPTH);
-        return mergedConfig.getTargetResolution();
+    @NonNull
+    private ImageAnalysisConfig createDefaultConfig() {
+        ImageAnalysis.Builder builder = new ImageAnalysis.Builder();
+        builder.setDefaultResolution(DEFAULT_RESOLUTION);
+        return builder.getUseCaseConfig();
     }
 
     @Test
@@ -360,6 +408,33 @@
         assertCanReceiveAnalysisImage(mImageAnalysis);
     }
 
+    @Test
+    public void throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                        .build());
+    }
+
+    @Test
+    public void throwException_whenSetTargetResolutionWithResolutionSelector() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                        .setResolutionSelector(new ResolutionSelector.Builder().build())
+                        .build());
+    }
+
+    @Test
+    public void throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        assertThrows(IllegalArgumentException.class,
+                () -> new ImageAnalysis.Builder()
+                        .setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                        .setResolutionSelector(new ResolutionSelector.Builder().build())
+                        .build());
+    }
+
     void assertCanReceiveAnalysisImage(ImageAnalysis imageAnalysis) throws InterruptedException {
         CountDownLatch latch = new CountDownLatch(1);
         imageAnalysis.setAnalyzer(CameraXExecutors.directExecutor(), image -> {
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 1ef77cb..8bfe101 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
@@ -40,6 +40,7 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.internal.utils.SizeUtil
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
@@ -60,6 +61,7 @@
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicReference
 import org.junit.After
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -231,6 +233,11 @@
     }
 
     @Test
+    fun detachWithoutAttach_doesNotCrash() {
+        ImageCapture.Builder().build().onDetached()
+    }
+
+    @Test
     fun useImageReaderProvider_pipelineDisabled() {
         assertThat(
             bindImageCapture(
@@ -576,6 +583,32 @@
         assertThat(cameraControl.isZslConfigAdded).isTrue()
     }
 
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        assertThrows(IllegalArgumentException::class.java) {
+            ImageCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
     private fun bindImageCapture(
         captureMode: Int = ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
         viewPort: ViewPort? = null,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 5f64956..8a1c98f 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -24,6 +24,7 @@
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
+import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
@@ -35,6 +36,8 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.internal.CameraUseCaseAdapter
+import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.processing.SettableSurface
 import androidx.camera.core.processing.SurfaceProcessorInternal
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -42,6 +45,7 @@
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
 import androidx.camera.testing.fakes.FakeCameraFactory
+import androidx.camera.testing.fakes.FakeCameraInfoInternal
 import androidx.camera.testing.fakes.FakeSurfaceProcessorInternal
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.test.core.app.ApplicationProvider
@@ -49,6 +53,7 @@
 import java.util.Collections
 import java.util.concurrent.ExecutionException
 import org.junit.After
+import org.junit.Assert
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -68,27 +73,34 @@
     minSdk = Build.VERSION_CODES.LOLLIPOP
 )
 class PreviewTest {
+
     var cameraUseCaseAdapter: CameraUseCaseAdapter? = null
 
     private lateinit var appSurface: Surface
     private lateinit var appSurfaceTexture: SurfaceTexture
-    private lateinit var camera: FakeCamera
+    private lateinit var backCamera: FakeCamera
+    private lateinit var frontCamera: FakeCamera
     private lateinit var cameraXConfig: CameraXConfig
     private lateinit var context: Context
+    private lateinit var previewToDetach: Preview
 
     @Before
     @Throws(ExecutionException::class, InterruptedException::class)
     fun setUp() {
         appSurfaceTexture = SurfaceTexture(0)
         appSurface = Surface(appSurfaceTexture)
-        camera = FakeCamera()
+        backCamera = FakeCamera("back")
+        frontCamera = FakeCamera("front", null, FakeCameraInfoInternal(0, LENS_FACING_FRONT))
 
         val cameraFactoryProvider =
             CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(
-                    camera.cameraInfoInternal.cameraId
-                ) { camera }
+                    backCamera.cameraInfoInternal.cameraId
+                ) { backCamera }
+                cameraFactory.insertDefaultFrontCamera(
+                    frontCamera.cameraInfoInternal.cameraId
+                ) { frontCamera }
                 cameraFactory
             }
         cameraXConfig = CameraXConfig.Builder.fromConfig(
@@ -107,6 +119,9 @@
             this?.removeUseCases(useCases)
         }
         cameraUseCaseAdapter = null
+        if (::previewToDetach.isInitialized) {
+            previewToDetach.onDetached()
+        }
         CameraXUtil.shutdown().get()
     }
 
@@ -215,6 +230,83 @@
     }
 
     @Test
+    fun createSurfaceRequestWithProcessor_noCameraTransform() {
+        // Arrange: attach Preview without a SurfaceProvider.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: create pipeline in Preview and provide Surface.
+        val preview = createPreview(processor)
+        preview.mCurrentSurfaceRequest!!.setTransformationInfoListener(mainThreadExecutor()) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Get pending SurfaceRequest created by pipeline.
+        assertThat(transformationInfo!!.hasCameraTransform()).isFalse()
+    }
+
+    @Test
+    fun createSurfaceRequestWithoutProcessor_hasCameraTransform() {
+        // Arrange: attach Preview without a SurfaceProvider.
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: create pipeline in Preview and provide Surface.
+        val preview = createPreview()
+        preview.mCurrentSurfaceRequest!!.setTransformationInfoListener(mainThreadExecutor()) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+
+        // Get pending SurfaceRequest created by pipeline.
+        assertThat(transformationInfo!!.hasCameraTransform()).isTrue()
+    }
+
+    @Test
+    fun backCameraWithProcessor_notMirrored() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+        // Act: create pipeline
+        val preview = createPreview(processor, backCamera)
+        // Assert
+        assertThat(preview.getCameraSurface().mirroring).isFalse()
+    }
+
+    @Test
+    fun frontCameraWithProcessor_mirrored() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+        // Act: create pipeline
+        val preview = createPreview(processor, frontCamera)
+        // Assert
+        assertThat(preview.getCameraSurface().mirroring).isTrue()
+    }
+
+    @Test
+    fun setTargetRotationWithProcessor_rotationChangesOnSettableSurface() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+
+        // Act: create pipeline
+        val preview = createPreview(processor)
+        // Act: update target rotation
+        preview.targetRotation = Surface.ROTATION_0
+        shadowOf(getMainLooper()).idle()
+        // Assert that the rotation of the SettableFuture is updated based on ROTATION_0.
+        assertThat(preview.getCameraSurface().rotationDegrees).isEqualTo(0)
+
+        // Act: update target rotation again.
+        preview.targetRotation = Surface.ROTATION_180
+        shadowOf(getMainLooper()).idle()
+        // Assert: the rotation of the SettableFuture is updated based on ROTATION_90.
+        assertThat(preview.getCameraSurface().rotationDegrees).isEqualTo(180)
+    }
+
+    private fun Preview.getCameraSurface(): SettableSurface {
+        return this.sessionConfig.surfaces.single() as SettableSurface
+    }
+
+    @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
         val processor = FakeSurfaceProcessorInternal(
@@ -223,7 +315,7 @@
         )
 
         // Act: create pipeline in Preview and provide Surface.
-        val preview = createPreviewPipelineAndAttachProcessor(processor)
+        val preview = createPreview(processor)
         val surfaceRequest = preview.mCurrentSurfaceRequest!!
         var appSurfaceReadyToRelease = false
         surfaceRequest.provideSurface(appSurface, mainThreadExecutor()) {
@@ -263,7 +355,7 @@
         val processor = FakeSurfaceProcessorInternal(
             mainThreadExecutor()
         )
-        val preview = createPreviewPipelineAndAttachProcessor(processor)
+        val preview = createPreview(processor)
         val originalSessionConfig = preview.sessionConfig
 
         // Act: invoke the error listener.
@@ -399,6 +491,32 @@
         assertThat(receivedAfterAttach).isTrue()
     }
 
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
     private fun bindToLifecycleAndGetSurfaceRequest(): SurfaceRequest {
         return bindToLifecycleAndGetResult(null).first
     }
@@ -438,23 +556,24 @@
         return Pair(surfaceRequest!!, transformationInfo!!)
     }
 
-    private fun createPreviewPipelineAndAttachProcessor(
-        surfaceProcessor: SurfaceProcessorInternal?
+    private fun createPreview(
+        surfaceProcessor: SurfaceProcessorInternal? = null,
+        camera: FakeCamera = backCamera
     ): Preview {
-        val preview = Preview.Builder()
+        previewToDetach = Preview.Builder()
             .setTargetRotation(Surface.ROTATION_0)
             .build()
-        preview.processor = surfaceProcessor
-        preview.setSurfaceProvider(CameraXExecutors.directExecutor()) {}
+        previewToDetach.processor = surfaceProcessor
+        previewToDetach.setSurfaceProvider(CameraXExecutors.directExecutor()) {}
         val previewConfig = PreviewConfig(
             cameraXConfig.getUseCaseConfigFactoryProvider(null)!!.newInstance(context).getConfig(
                 UseCaseConfigFactory.CaptureType.PREVIEW,
                 ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY
             )!! as OptionsBundle
         )
-        preview.onAttach(camera, null, previewConfig)
+        previewToDetach.onAttach(camera, null, previewConfig)
 
-        preview.onSuggestedResolutionUpdated(Size(640, 480))
-        return preview
+        previewToDetach.onSuggestedResolutionUpdated(Size(640, 480))
+        return previewToDetach
     }
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
new file mode 100644
index 0000000..a4eae79
--- /dev/null
+++ b/camera/camera-core/src/test/java/androidx/camera/core/UseCaseGroupTest.kt
@@ -0,0 +1,78 @@
+/*
+ * 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
+
+import android.os.Build
+import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
+import androidx.camera.core.CameraEffect.PREVIEW
+import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
+import androidx.camera.core.UseCaseGroup.Builder.getHumanReadableTargets
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.fakes.FakePreviewEffect
+import androidx.camera.testing.fakes.FakeSurfaceProcessor
+import androidx.camera.testing.fakes.FakeUseCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+
+/**
+ * Unit tests for [UseCaseGroup].
+ */
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class UseCaseGroupTest {
+
+    @Test
+    fun duplicateTargets_throwsException() {
+        // Arrange.
+        val previewEffect = FakePreviewEffect(
+            CameraXExecutors.mainThreadExecutor(),
+            FakeSurfaceProcessor(CameraXExecutors.mainThreadExecutor())
+        )
+        val builder = UseCaseGroup.Builder().addUseCase(FakeUseCase())
+            .addEffect(previewEffect)
+            .addEffect(previewEffect)
+
+        // Act.
+        var message: String? = null
+        try {
+            builder.build()
+        } catch (e: IllegalArgumentException) {
+            message = e.message
+        }
+
+        // Assert.
+        assertThat(message).isEqualTo(
+            "androidx.camera.testing.fakes.FakePreviewEffect " +
+                "and androidx.camera.testing.fakes.FakePreviewEffect " +
+                "contain duplicate targets PREVIEW."
+        )
+    }
+
+    @Test
+    fun verifyHumanReadableTargetsNames() {
+        assertThat(getHumanReadableTargets(PREVIEW)).isEqualTo("PREVIEW")
+        assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE))
+            .isEqualTo("PREVIEW|VIDEO_CAPTURE")
+        assertThat(getHumanReadableTargets(PREVIEW or VIDEO_CAPTURE or IMAGE_CAPTURE))
+            .isEqualTo("IMAGE_CAPTURE|PREVIEW|VIDEO_CAPTURE")
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
index 0d11439..184b2b8 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
@@ -21,6 +21,7 @@
 import android.os.Looper
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.utils.SizeUtil
 import androidx.camera.testing.CameraXUtil
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
@@ -40,6 +41,7 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.junit.Assert
 
 @RunWith(RobolectricTestRunner::class)
 @Suppress("DEPRECATION")
@@ -87,4 +89,30 @@
 
         verify(callback).onError(eq(VideoCapture.ERROR_INVALID_CAMERA), anyString(), any())
     }
+
+    @Test
+    fun throwException_whenSetBothTargetResolutionAndAspectRatio() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setTargetAspectRatio(AspectRatio.RATIO_4_3).build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetResolutionWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetResolution(SizeUtil.RESOLUTION_VGA)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
+
+    @Test
+    fun throwException_whenSetTargetAspectRatioWithResolutionSelector() {
+        Assert.assertThrows(IllegalArgumentException::class.java) {
+            VideoCapture.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3)
+                .setResolutionSelector(ResolutionSelector.Builder().build())
+                .build()
+        }
+    }
 }
\ No newline at end of file
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 26bba9a..ee31c80 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
@@ -61,6 +61,11 @@
         return IMMEDIATE_RESULT
     }
 
+    fun clear() {
+        // Cancel pending futures.
+        pendingResult.cancel(true)
+    }
+
     enum class Action {
         LOCK_FLASH,
         UNLOCK_FLASH,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
index 484950f..542d22b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ProcessingNodeTest.kt
@@ -27,6 +27,7 @@
 import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
 import androidx.camera.core.imagecapture.Utils.WIDTH
 import androidx.camera.core.imagecapture.Utils.createProcessingRequest
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.isSequentialExecutor
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.TestImageUtil.createJpegBytes
 import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
@@ -40,6 +41,7 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.util.ReflectionHelpers.setStaticField
 
 /**
  * Unit tests for [ProcessingNode].
@@ -100,4 +102,14 @@
         assertThat(takePictureCallback.processFailure)
             .isInstanceOf(ImageCaptureException::class.java)
     }
+
+    @Test
+    fun singleExecutorForLowMemoryQuirkEnabled() {
+        listOf("sm-a520w", "motog3").forEach { model ->
+            setStaticField(Build::class.java, "MODEL", model)
+            assertThat(
+                isSequentialExecutor(ProcessingNode(mainThreadExecutor()).mBlockingExecutor)
+            ).isTrue()
+        }
+    }
 }
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 acdc78e..425cdef 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
@@ -25,7 +25,10 @@
 import androidx.camera.core.ImageProxy
 import androidx.camera.testing.fakes.FakeImageInfo
 import androidx.camera.testing.fakes.FakeImageProxy
+import androidx.concurrent.futures.CallbackToFutureAdapter
 import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import org.junit.After
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -47,6 +50,7 @@
     private lateinit var imageResult: ImageProxy
     private lateinit var fileResult: ImageCapture.OutputFileResults
     private lateinit var retryControl: FakeRetryControl
+    private lateinit var captureRequestFuture: ListenableFuture<Void>
 
     @Before
     fun setUp() {
@@ -55,6 +59,12 @@
         imageResult = FakeImageProxy(FakeImageInfo())
         fileResult = ImageCapture.OutputFileResults(null)
         retryControl = FakeRetryControl()
+        captureRequestFuture = CallbackToFutureAdapter.getFuture { "captureRequestFuture" }
+    }
+
+    @After
+    fun tearDown() {
+        captureRequestFuture.cancel(true)
     }
 
     @Test
@@ -126,6 +136,7 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onCaptureFailure(otherError)
@@ -133,6 +144,7 @@
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.exceptionReceived).isEqualTo(abortError)
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 
     @Test
@@ -153,12 +165,14 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.imageReceived).isNull()
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 
     @Test
@@ -179,11 +193,13 @@
         // Arrange.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.ON_DISK)
         val callback = RequestWithCallback(request, retryControl)
+        callback.setCaptureRequestFuture(captureRequestFuture)
         // Act.
         callback.abortAndSendErrorToApp(abortError)
         callback.onFinalResult(imageResult)
         shadowOf(getMainLooper()).idle()
         // Assert.
         assertThat(request.imageReceived).isNull()
+        assertThat(captureRequestFuture.isCancelled).isTrue()
     }
 }
\ No newline at end of file
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 5ed2782..c49618db 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
@@ -46,12 +46,13 @@
     private val imagePipeline = FakeImagePipeline()
     private val imageCaptureControl = FakeImageCaptureControl()
     private val takePictureManager =
-        TakePictureManager(imageCaptureControl).also { it.setImagePipeline(imagePipeline) }
+        TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
     private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
 
     @After
     fun tearDown() {
         imagePipeline.close()
+        imageCaptureControl.clear()
     }
 
     @Test
@@ -74,6 +75,23 @@
     }
 
     @Test
+    fun abort_captureRequestFutureIsCanceled() {
+        // Arrange: configure ImageCaptureControl to not return immediately.
+        val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
+        imageCaptureControl.shouldUsePendingResult = true
+
+        // Act: offer request then abort.
+        takePictureManager.offerRequest(request)
+        takePictureManager.abortRequests()
+        shadowOf(getMainLooper()).idle()
+
+        // Assert: that the app receives exception and the capture future is canceled.
+        assertThat((request.exceptionReceived as ImageCaptureException).imageCaptureError)
+            .isEqualTo(ERROR_CAMERA_CLOSED)
+        assertThat(imageCaptureControl.pendingResult.isCancelled).isTrue()
+    }
+
+    @Test
     fun abortPostProcessingRequests_receiveErrorCallback() {
         // Arrange: setup a request that is captured but not processed.
         val request = FakeTakePictureRequest(FakeTakePictureRequest.Type.IN_MEMORY)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
index f42c007..c1ee35b 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/utils/AspectRatioUtilTest.kt
@@ -19,7 +19,9 @@
 import android.os.Build
 import android.util.Rational
 import android.util.Size
-import com.google.common.truth.Truth
+import androidx.camera.core.impl.utils.AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace
+import com.google.common.truth.Truth.assertThat
+import java.util.Collections
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
@@ -31,7 +33,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNullAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 null
@@ -41,7 +43,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withSameAspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(16, 9),
                 Rational(16, 9)
@@ -51,7 +53,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_720p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1280, 720),
                 Rational(16, 9)
@@ -61,7 +63,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1080p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1920, 1088),
                 Rational(16, 9)
@@ -71,7 +73,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1440p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(2560, 1440),
                 Rational(16, 9)
@@ -81,7 +83,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_2160p() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(3840, 2160),
                 Rational(16, 9)
@@ -91,7 +93,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_1x1() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1088, 1088),
                 Rational(1, 1)
@@ -101,7 +103,7 @@
 
     @Test
     fun testHasMatchingAspectRatio_withMod16AspectRatio_4x3() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1024, 768),
                 Rational(4, 3)
@@ -111,11 +113,44 @@
 
     @Test
     fun testHasMatchingAspectRatio_withNonMod16AspectRatio() {
-        Truth.assertThat(
+        assertThat(
             AspectRatioUtil.hasMatchingAspectRatio(
                 Size(1281, 721),
                 Rational(16, 9)
             )
         ).isFalse()
     }
+
+    @Test
+    fun sortAspectRatios() {
+        // Sort the aspect ratio key set by the target aspect ratio.
+        val aspectRatios = listOf(
+            Rational(1, 1),
+            Rational(4, 3),
+            Rational(16, 9),
+            Rational(18, 9),
+            Rational(14, 9),
+        )
+
+        val targetAspectRatio = Rational(16, 9)
+        val fullFovAspectRatio = Rational(4, 3)
+
+        Collections.sort(
+            aspectRatios,
+            CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                targetAspectRatio,
+                fullFovAspectRatio
+            )
+        )
+
+        val expectedResult = listOf(
+            Rational(16, 9),
+            Rational(14, 9),
+            Rational(4, 3),
+            Rational(18, 9),
+            Rational(1, 1),
+        )
+
+        assertThat(aspectRatios == expectedResult).isTrue()
+    }
 }
\ 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 96048cd..93b374f 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
@@ -26,7 +26,6 @@
 import android.view.Surface
 import androidx.camera.core.CameraEffect
 import androidx.camera.core.SurfaceOutput
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.Result.RESULT_REQUEST_CANCELLED
 import androidx.camera.core.SurfaceRequest.TransformationInfo
@@ -188,6 +187,35 @@
     }
 
     @Test
+    fun createSurfaceRequest_hasCameraTransformSetCorrectly() {
+        assertThat(getSurfaceRequestHasTransform(true)).isTrue()
+        assertThat(getSurfaceRequestHasTransform(false)).isFalse()
+    }
+
+    /**
+     * Creates a [SettableSurface] with the given hasEmbeddedTransform value, and returns the
+     * [TransformationInfo.hasCameraTransform] from the [SurfaceRequest].
+     */
+    private fun getSurfaceRequestHasTransform(hasEmbeddedTransform: Boolean): Boolean {
+        // Arrange.
+        val surface = SettableSurface(
+            CameraEffect.PREVIEW, Size(640, 480), ImageFormat.PRIVATE,
+            Matrix(), hasEmbeddedTransform, Rect(), 0, false
+        ) {}
+        var transformationInfo: TransformationInfo? = null
+
+        // Act: get the hasCameraTransform bit from the SurfaceRequest.
+        surface.createSurfaceRequest(FakeCamera()).setTransformationInfoListener(
+            mainThreadExecutor()
+        ) {
+            transformationInfo = it
+        }
+        shadowOf(getMainLooper()).idle()
+        surface.close()
+        return transformationInfo!!.hasCameraTransform()
+    }
+
+    @Test
     fun setSourceSurfaceFutureAndProvide_surfaceIsPropagated() {
         // Arrange: set a ListenableFuture<Surface> as the source.
         var completer: CallbackToFutureAdapter.Completer<Surface>? = null
@@ -281,7 +309,6 @@
 
     private fun createSurfaceOutputFuture(settableSurface: SettableSurface) =
         settableSurface.createSurfaceOutputFuture(
-            USE_SURFACE_TEXTURE_TRANSFORM,
             INPUT_SIZE,
             sizeToRect(INPUT_SIZE),
             /*rotationDegrees=*/0,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index 621eb1d..2ea4195 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
@@ -23,8 +23,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect
-import androidx.camera.core.SurfaceOutput.GlTransformOptions
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.impl.utils.TransformUtils.sizeToRect
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import com.google.common.truth.Truth.assertThat
@@ -102,6 +100,27 @@
     }
 
     @Test
+    fun updateMatrix_containsOpenGlFlipping() {
+        // Arrange.
+        val surfaceOut = createFakeSurfaceOutputImpl()
+        val input = FloatArray(16).also {
+            android.opengl.Matrix.setIdentityM(it, 0)
+        }
+
+        // Act.
+        val result = FloatArray(16)
+        surfaceOut.updateTransformMatrix(result, input)
+
+        // Assert: the result contains the flipping for OpenGL.
+        val expected = FloatArray(16).also {
+            android.opengl.Matrix.setIdentityM(it, 0)
+            android.opengl.Matrix.translateM(it, 0, 0f, 1f, 0f)
+            android.opengl.Matrix.scaleM(it, 0, 1f, -1f, 1f)
+        }
+        assertThat(result).usingTolerance(1E-4).containsExactly(expected)
+    }
+
+    @Test
     fun closedSurface_noLongerReceivesCloseRequest() {
         // Arrange.
         val surfaceOutImpl = createFakeSurfaceOutputImpl()
@@ -119,29 +138,11 @@
         assertThat(hasRequestedClose).isFalse()
     }
 
-    @Test
-    fun updateMatrix_useSurfaceTextureTransform_sameResult() {
-        // Arrange.
-        val surfaceOut =
-            createFakeSurfaceOutputImpl(glTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM)
-
-        // Act.
-        val input = floatArrayOf(1f, 1f, 1f, 1f, 2f, 2f, 2f, 2f, 3f, 3f, 3f, 3f, 4f, 4f, 4f, 4f)
-        val result = FloatArray(16)
-        surfaceOut.updateTransformMatrix(result, input)
-
-        // Assert.
-        assertThat(result).isEqualTo(input)
-    }
-
-    private fun createFakeSurfaceOutputImpl(
-        glTransformOptions: GlTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM
-    ) = SurfaceOutputImpl(
+    private fun createFakeSurfaceOutputImpl() = SurfaceOutputImpl(
         fakeSurface,
         TARGET,
         FORMAT,
         OUTPUT_SIZE,
-        glTransformOptions,
         INPUT_SIZE,
         sizeToRect(INPUT_SIZE),
         /*rotationDegrees=*/0,
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 3027ece..43fac3e 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
@@ -24,9 +24,6 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.CameraEffect.PREVIEW
-import androidx.camera.core.SurfaceOutput.GlTransformOptions
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING
-import androidx.camera.core.SurfaceOutput.GlTransformOptions.USE_SURFACE_TEXTURE_TRANSFORM
 import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.utils.TransformUtils.is90or270
@@ -94,33 +91,11 @@
     }
 
     @Test
-    fun transformInput_useSurfaceTextureTransform_outputHasTheSameProperty() {
-        // Arrange.
-        createSurfaceProcessorNode()
-        createInputEdge()
-        val inputSurface = inputEdge.surfaces[0]
-
-        // Act.
-        val outputEdge = node.transform(inputEdge)
-
-        // Assert: without transformation, the output has the same property as the input.
-        assertThat(outputEdge.surfaces).hasSize(1)
-        val outputSurface = outputEdge.surfaces[0]
-        assertThat(outputSurface.size).isEqualTo(inputSurface.size)
-        assertThat(outputSurface.format).isEqualTo(inputSurface.format)
-        assertThat(outputSurface.targets).isEqualTo(inputSurface.targets)
-        assertThat(outputSurface.cropRect).isEqualTo(inputSurface.cropRect)
-        assertThat(outputSurface.rotationDegrees).isEqualTo(inputSurface.rotationDegrees)
-        assertThat(outputSurface.mirroring).isEqualTo(inputSurface.mirroring)
-        assertThat(outputSurface.hasEmbeddedTransform()).isFalse()
-    }
-
-    @Test
     fun transformInput_applyCropRotateAndMirroring_outputIsCroppedAndRotated() {
         val cropRect = Rect(200, 100, 600, 400)
         for (rotationDegrees in arrayOf(0, 90, 180, 270)) {
             // Arrange.
-            createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+            createSurfaceProcessorNode()
             createInputEdge(
                 size = rectToSize(cropRect),
                 cropRect = cropRect,
@@ -153,7 +128,7 @@
     fun transformInput_applyCropRotateAndMirroring_outputHasNoMirroring() {
         for (mirroring in arrayOf(false, true)) {
             // Arrange.
-            createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+            createSurfaceProcessorNode()
             createInputEdge(mirroring = mirroring)
 
             // Act.
@@ -173,7 +148,7 @@
     @Test
     fun transformInput_applyCropRotateAndMirroring_initialTransformInfoIsPropagated() {
         // Arrange.
-        createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+        createSurfaceProcessorNode()
         createInputEdge(rotationDegrees = 90, cropRect = Rect(0, 0, 600, 400))
 
         // Act.
@@ -192,7 +167,7 @@
     @Test
     fun setRotationToInput_applyCropRotateAndMirroring_rotationIsPropagated() {
         // Arrange.
-        createSurfaceProcessorNode(APPLY_CROP_ROTATE_AND_MIRRORING)
+        createSurfaceProcessorNode()
         createInputEdge(rotationDegrees = 90)
         val inputSurface = inputEdge.surfaces[0]
         val outputEdge = node.transform(inputEdge)
@@ -269,12 +244,9 @@
         inputEdge = SurfaceEdge.create(listOf(surface))
     }
 
-    private fun createSurfaceProcessorNode(
-        glTransformOptions: GlTransformOptions = USE_SURFACE_TEXTURE_TRANSFORM
-    ) {
+    private fun createSurfaceProcessorNode() {
         node = SurfaceProcessorNode(
             FakeCamera(),
-            glTransformOptions,
             surfaceProcessorInternal
         )
     }
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
index f0be504..ea27418 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/ImageCaptureTest.kt
@@ -73,9 +73,12 @@
 
     private lateinit var baseCameraSelector: CameraSelector
 
+    private lateinit var extensionsCameraSelector: CameraSelector
+
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
     @Before
-    @Throws(Exception::class)
-    fun setUp() {
+    fun setUp(): Unit = runBlocking {
         assumeTrue(
             ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(
                 lensFacing,
@@ -91,6 +94,15 @@
         )[10000, TimeUnit.MILLISECONDS]
 
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
+
+        extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
+            baseCameraSelector,
+            extensionMode
+        )
+
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
+        }
     }
 
     @After
@@ -143,13 +155,6 @@
                     })
             )
 
-            val fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
-
-            val extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
-                baseCameraSelector,
-                extensionMode
-            )
-
             cameraProvider.bindToLifecycle(
                 fakeLifecycleOwner,
                 extensionsCameraSelector,
@@ -180,4 +185,18 @@
             )
         )
     }
+
+    @Test
+    fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
+        val imageCapture = ImageCapture.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                extensionsCameraSelector,
+                imageCapture)
+        }
+
+        assertThat(imageCapture.currentConfig.isHigResolutionDisabled(false)).isTrue()
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
index 241e9ab..f6f6235 100644
--- a/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
+++ b/camera/camera-extensions/src/androidTest/java/androidx/camera/extensions/PreviewTest.kt
@@ -70,6 +70,10 @@
 
     private lateinit var baseCameraSelector: CameraSelector
 
+    private lateinit var extensionsCameraSelector: CameraSelector
+
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
     private val surfaceTextureLatch = CountDownLatch(1)
     private val frameReceivedLatch = CountDownLatch(1)
     private var isSurfaceTextureReleased = false
@@ -106,8 +110,7 @@
     }
 
     @Before
-    @Throws(Exception::class)
-    fun setUp() {
+    fun setUp(): Unit = runBlocking {
         assumeTrue(
             ExtensionsTestUtil.isTargetDeviceAvailableForExtensions(
                 lensFacing,
@@ -123,6 +126,15 @@
         )[10000, TimeUnit.MILLISECONDS]
 
         assumeTrue(extensionsManager.isExtensionAvailable(baseCameraSelector, extensionMode))
+
+        extensionsCameraSelector = extensionsManager.getExtensionEnabledCameraSelector(
+            baseCameraSelector,
+            extensionMode
+        )
+
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
+        }
     }
 
     @After
@@ -156,17 +168,9 @@
                 SurfaceTextureProvider.createSurfaceTextureProvider(createSurfaceTextureCallback())
             )
 
-            val fakeLifecycleOwner = FakeLifecycleOwner().apply { startAndResume() }
-
-            val extensionEnabledCameraSelector =
-                extensionsManager.getExtensionEnabledCameraSelector(
-                    baseCameraSelector,
-                    extensionMode
-                )
-
             cameraProvider.bindToLifecycle(
                 fakeLifecycleOwner,
-                extensionEnabledCameraSelector,
+                extensionsCameraSelector,
                 preview
             )
         }
@@ -178,6 +182,20 @@
         assertThat(frameReceivedLatch.await(10000, TimeUnit.MILLISECONDS)).isTrue()
     }
 
+    @Test
+    fun highResolutionDisabled_whenExtensionsEnabled(): Unit = runBlocking {
+        val preview = Preview.Builder().build()
+
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(
+                fakeLifecycleOwner,
+                extensionsCameraSelector,
+                preview)
+        }
+
+        assertThat(preview.currentConfig.isHigResolutionDisabled(false)).isTrue()
+    }
+
     private fun createSurfaceTextureCallback(): SurfaceTextureProvider.SurfaceTextureCallback =
         object : SurfaceTextureProvider.SurfaceTextureCallback {
             override fun onSurfaceTextureReady(
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index d12a1ac3..fa38f9c 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -127,6 +127,7 @@
         List<Pair<Integer, Size[]>> supportedResolutions =
                 vendorExtender.getSupportedCaptureOutputResolutions();
         builder.setSupportedResolutions(supportedResolutions);
+        builder.setHighResolutionDisabled(true);
     }
 
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
index b97d8d0..2019e0b 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java
@@ -131,6 +131,7 @@
         List<Pair<Integer, Size[]>> supportedResolutions =
                 vendorExtender.getSupportedPreviewOutputResolutions();
         builder.setSupportedResolutions(supportedResolutions);
+        builder.setHighResolutionDisabled(true);
     }
 
     /**
diff --git a/camera/camera-testing/build.gradle b/camera/camera-testing/build.gradle
index 98bfd46..40a7f15 100644
--- a/camera/camera-testing/build.gradle
+++ b/camera/camera-testing/build.gradle
@@ -26,7 +26,10 @@
 }
 
 dependencies {
-    implementation("androidx.test:core:1.4.0")
+    implementation(libs.testCore)
+    // force runner 1.5.1 so implementation stays consistent with androidTestImplementation
+
+    implementation(libs.testRunner)
     implementation(libs.testRules)
     implementation(libs.testUiautomator)
     api("androidx.annotation:annotation:1.2.0")
@@ -35,8 +38,8 @@
     api(project(":camera:camera-core"))
     implementation("androidx.core:core:1.1.0")
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
-    implementation("androidx.test.espresso:espresso-core:3.3.0")
-    implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
+    implementation("androidx.test.espresso:espresso-core:3.5.0")
+    implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
     implementation(libs.junit)
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesAndroid)
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
index 71bc549..d4871bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeUseCaseConfig.java
@@ -22,6 +22,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.Config;
@@ -223,6 +224,13 @@
             return this;
         }
 
+        @NonNull
+        @Override
+        public Builder setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         /**
          * Sets specific image format to the fake use case.
          */
@@ -238,5 +246,12 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        @NonNull
+        @Override
+        public Builder setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
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 2df259f..5f8641f 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
@@ -17,9 +17,9 @@
 package androidx.camera.video;
 
 import static androidx.camera.core.CameraEffect.VIDEO_CAPTURE;
-import static androidx.camera.core.SurfaceOutput.GlTransformOptions.APPLY_CROP_ROTATE_AND_MIRRORING;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_DEFAULT_RESOLUTION;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_MAX_RESOLUTION;
+import static androidx.camera.core.impl.ImageOutputConfig.OPTION_RESOLUTION_SELECTOR;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_SUPPORTED_RESOLUTIONS;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ASPECT_RATIO;
 import static androidx.camera.core.impl.ImageOutputConfig.OPTION_TARGET_ROTATION;
@@ -27,6 +27,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_HIGH_RESOLUTION_DISABLED;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SURFACE_OCCUPANCY_PRIORITY;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_ZSL_DISABLED;
@@ -66,6 +67,7 @@
 import androidx.camera.core.CameraSelector;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.Logger;
+import androidx.camera.core.ResolutionSelector;
 import androidx.camera.core.SurfaceRequest;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.ViewPort;
@@ -455,7 +457,7 @@
             } else {
                 surfaceRequest.updateTransformationInfo(
                         SurfaceRequest.TransformationInfo.of(cropRect, relativeRotation,
-                                targetRotation));
+                                targetRotation, /*hasCameraTransform=*/true));
             }
         }
     }
@@ -732,7 +734,6 @@
         if (mSurfaceProcessor != null || ENABLE_SURFACE_PROCESSING_BY_QUIRK || isCropNeeded) {
             Logger.d(TAG, "Surface processing is enabled.");
             return new SurfaceProcessorNode(requireNonNull(getCamera()),
-                    APPLY_CROP_ROTATE_AND_MIRRORING,
                     mSurfaceProcessor != null ? mSurfaceProcessor : new DefaultSurfaceProcessor());
         }
         return null;
@@ -1410,6 +1411,15 @@
             return this;
         }
 
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @Override
+        @NonNull
+        public Builder<T> setResolutionSelector(@NonNull ResolutionSelector resolutionSelector) {
+            getMutableConfig().insertOption(OPTION_RESOLUTION_SELECTOR, resolutionSelector);
+            return this;
+        }
+
         // Implementations of ThreadConfig.Builder default methods
 
         /**
@@ -1506,5 +1516,14 @@
             getMutableConfig().insertOption(OPTION_ZSL_DISABLED, disabled);
             return this;
         }
+
+        /** @hide */
+        @RestrictTo(Scope.LIBRARY_GROUP)
+        @NonNull
+        @Override
+        public Builder<T> setHighResolutionDisabled(boolean disabled) {
+            getMutableConfig().insertOption(OPTION_HIGH_RESOLUTION_DISABLED, disabled);
+            return this;
+        }
     }
 }
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index e27e9b7..a371cb4 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -19,7 +19,7 @@
     method @MainThread public androidx.camera.view.CameraController.OutputSize? getPreviewTargetSize();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTapToFocusState();
     method @MainThread public androidx.lifecycle.LiveData<java.lang.Integer!> getTorchState();
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.view.CameraController.OutputSize? getVideoCaptureTargetSize();
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Quality? getVideoCaptureTargetQuality();
     method @MainThread public androidx.lifecycle.LiveData<androidx.camera.core.ZoomState!> getZoomState();
     method @MainThread public boolean hasCamera(androidx.camera.core.CameraSelector);
     method @MainThread public boolean isImageAnalysisEnabled();
@@ -43,10 +43,11 @@
     method @MainThread public void setPinchToZoomEnabled(boolean);
     method @MainThread public void setPreviewTargetSize(androidx.camera.view.CameraController.OutputSize?);
     method @MainThread public void setTapToFocusEnabled(boolean);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void setVideoCaptureTargetSize(androidx.camera.view.CameraController.OutputSize?);
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void setVideoCaptureTargetQuality(androidx.camera.video.Quality?);
     method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void startRecording(androidx.camera.view.video.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.view.video.OnVideoSavedCallback);
-    method @MainThread @androidx.camera.view.video.ExperimentalVideo public void stopRecording();
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+    method @MainThread @RequiresApi(26) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+    method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
     method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
     method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
     field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
@@ -160,45 +161,14 @@
 
 package androidx.camera.view.video {
 
+  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo public class AudioConfig {
+    method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+    method public boolean getAudioEnabled();
+    field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+  }
+
   @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
   }
 
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class Metadata {
-    method public static androidx.camera.view.video.Metadata.Builder builder();
-    method public abstract android.location.Location? getLocation();
-  }
-
-  @com.google.auto.value.AutoValue.Builder public abstract static class Metadata.Builder {
-    method public abstract androidx.camera.view.video.Metadata build();
-    method public abstract androidx.camera.view.video.Metadata.Builder setLocation(android.location.Location?);
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo public interface OnVideoSavedCallback {
-    method public void onError(int, String, Throwable?);
-    method public void onVideoSaved(androidx.camera.view.video.OutputFileResults);
-    field public static final int ERROR_ENCODER = 1; // 0x1
-    field public static final int ERROR_FILE_IO = 4; // 0x4
-    field public static final int ERROR_INVALID_CAMERA = 5; // 0x5
-    field public static final int ERROR_MUXER = 2; // 0x2
-    field public static final int ERROR_RECORDING_IN_PROGRESS = 3; // 0x3
-    field public static final int ERROR_UNKNOWN = 0; // 0x0
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class OutputFileOptions {
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(java.io.File);
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(android.os.ParcelFileDescriptor);
-    method public static androidx.camera.view.video.OutputFileOptions.Builder builder(android.content.ContentResolver, android.net.Uri, android.content.ContentValues);
-    method public abstract androidx.camera.view.video.Metadata getMetadata();
-  }
-
-  @com.google.auto.value.AutoValue.Builder public abstract static class OutputFileOptions.Builder {
-    method public abstract androidx.camera.view.video.OutputFileOptions build();
-    method public abstract androidx.camera.view.video.OutputFileOptions.Builder setMetadata(androidx.camera.view.video.Metadata);
-  }
-
-  @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo @com.google.auto.value.AutoValue public abstract class OutputFileResults {
-    method public abstract android.net.Uri? getSavedUri();
-  }
-
 }
 
diff --git a/camera/camera-view/build.gradle b/camera/camera-view/build.gradle
index fc7bb75..9c4856f 100644
--- a/camera/camera-view/build.gradle
+++ b/camera/camera-view/build.gradle
@@ -27,6 +27,7 @@
     api("androidx.lifecycle:lifecycle-common:2.0.0")
     api("androidx.annotation:annotation:1.2.0")
     api(project(":camera:camera-core"))
+    api(project(":camera:camera-video"))
     implementation(project(":camera:camera-lifecycle"))
     implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
     implementation(libs.guavaListenableFuture)
@@ -61,6 +62,7 @@
     androidTestImplementation(project(":camera:camera-camera2"))
     androidTestImplementation(project(":camera:camera-testing"))
     androidTestImplementation(project(":camera:camera-camera2-pipe-integration"))
+    androidTestImplementation(project(":internal-testutils-truth"))
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy) // DexMaker has it's own MockMaker
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy) // DexMaker has it's own MockMaker
     androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0")
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 c553bc3..5df9f75 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
@@ -1099,7 +1099,8 @@
     private fun updateCropRectAndWaitForIdle(cropRect: Rect) {
         for (surfaceRequest in surfaceRequestList) {
             surfaceRequest.updateTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(cropRect, 0, Surface.ROTATION_0)
+                SurfaceRequest.TransformationInfo.of(cropRect, 0, Surface.ROTATION_0,
+                    /*hasCameraTransform=*/true)
             )
         }
         instrumentation.waitForIdleSync()
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
index a66b68d..6e0c7bf 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewViewMeteringPointFactoryDeviceTest.kt
@@ -205,8 +205,9 @@
         val previewTransformation = PreviewTransformation()
         previewTransformation.scaleType = scaleType
         previewTransformation.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of
-            (cropRect, rotationDegrees, FAKE_TARGET_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                cropRect, rotationDegrees, FAKE_TARGET_ROTATION, /*hasCameraTransform=*/true
+            ),
             surfaceSize, isFrontCamera
         )
         val meteringPointFactory = PreviewViewMeteringPointFactory(previewTransformation)
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
new file mode 100644
index 0000000..db01315
--- /dev/null
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -0,0 +1,667 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view
+
+import android.Manifest
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.content.Context
+import android.media.MediaMetadataRetriever
+import android.net.Uri
+import android.os.Build
+import android.os.ParcelFileDescriptor
+import android.provider.MediaStore
+import android.util.Log
+import androidx.annotation.MainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.CoreAppTestUtil.ForegroundOccupiedError
+import androidx.camera.testing.fakes.FakeActivity
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.camera.video.FileDescriptorOutputOptions
+import androidx.camera.video.FileOutputOptions
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.OutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.camera.view.CameraController.VIDEO_CAPTURE
+import androidx.camera.view.video.AudioConfig
+import androidx.core.util.Consumer
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.FlakyTest
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.BeforeClass
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class VideoCaptureDeviceTest(
+    private val initialQuality: TargetQuality,
+    private val nextQuality: TargetQuality
+) {
+
+    /**
+     * The helper class to workaround the issue that "null" cannot be accepted as a parameter value
+     * in Parameterized tests, ref: b/37086576
+     */
+    enum class TargetQuality {
+        None, FHD, HD, HIGHEST, LOWEST, SD, UHD;
+
+        fun get(): Quality? {
+            return when (this) {
+                None -> null
+                FHD -> Quality.FHD
+                HD -> Quality.HD
+                HIGHEST -> Quality.HIGHEST
+                LOWEST -> Quality.LOWEST
+                SD -> Quality.SD
+                UHD -> Quality.UHD
+            }
+        }
+    }
+
+    companion object {
+        private const val VIDEO_TIMEOUT_SEC = 10L
+        private const val VIDEO_RECORDING_COUNT_DOWN = 5
+        private const val VIDEO_STARTED_COUNT_DOWN = 1
+        private const val VIDEO_SAVED_COUNT_DOWN = 1
+        private const val TAG = "VideoRecordingTest"
+
+        @JvmStatic
+        @BeforeClass
+        @Throws(ForegroundOccupiedError::class)
+        fun classSetUp() {
+            CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+        }
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "initialQuality={0}, nextQuality={1}")
+        fun data() = mutableListOf<Array<TargetQuality>>().apply {
+            add(arrayOf(TargetQuality.None, TargetQuality.FHD))
+            add(arrayOf(TargetQuality.FHD, TargetQuality.HD))
+            add(arrayOf(TargetQuality.HD, TargetQuality.HIGHEST))
+            add(arrayOf(TargetQuality.HIGHEST, TargetQuality.LOWEST))
+            add(arrayOf(TargetQuality.LOWEST, TargetQuality.SD))
+            add(arrayOf(TargetQuality.SD, TargetQuality.UHD))
+            add(arrayOf(TargetQuality.UHD, TargetQuality.None))
+        }
+    }
+
+    @get:Rule
+    val cameraRule: TestRule = CameraUtil.grantCameraPermissionAndPreTest()
+
+    @get:Rule
+    val activityRule: ActivityScenarioRule<FakeActivity> =
+        ActivityScenarioRule(FakeActivity::class.java)
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(
+        Manifest.permission.WRITE_EXTERNAL_STORAGE,
+        Manifest.permission.RECORD_AUDIO
+    )
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private val audioEnabled = AudioConfig.create(true)
+    private val audioDisabled = AudioConfig.AUDIO_DISABLED
+    private lateinit var previewView: PreviewView
+    private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private lateinit var cameraController: LifecycleCameraController
+    private lateinit var activeRecording: Recording
+    private lateinit var latchForVideoStarted: CountDownLatch
+    private lateinit var latchForVideoPaused: CountDownLatch
+    private lateinit var latchForVideoResumed: CountDownLatch
+    private lateinit var latchForVideoSaved: CountDownLatch
+    private lateinit var latchForVideoRecording: CountDownLatch
+    private lateinit var finalize: VideoRecordEvent.Finalize
+
+    private val videoRecordEventListener = Consumer<VideoRecordEvent> {
+        when (it) {
+            is VideoRecordEvent.Start -> {
+                Log.d(TAG, "Recording start")
+                latchForVideoStarted.countDown()
+            }
+            is VideoRecordEvent.Finalize -> {
+                Log.d(TAG, "Recording finalize")
+                finalize = it
+                latchForVideoSaved.countDown()
+            }
+            is VideoRecordEvent.Status -> {
+                // Make sure the recording proceed for a while.
+                Log.d(TAG, "Recording Status")
+                latchForVideoRecording.countDown()
+            }
+            is VideoRecordEvent.Pause -> {
+                Log.d(TAG, "Recording Pause")
+                latchForVideoPaused.countDown()
+            }
+            is VideoRecordEvent.Resume -> {
+                Log.d(TAG, "Recording Resume")
+                latchForVideoResumed.countDown()
+            }
+            else -> {
+                throw IllegalStateException()
+            }
+        }
+    }
+
+    @Before
+    fun setUp() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        initialLifecycleOwner()
+        initialPreviewView()
+        initialController()
+    }
+
+    @After
+    fun tearDown() {
+        if (this::cameraController.isInitialized) {
+            instrumentation.runOnMainSync {
+                cameraController.shutDownForTests()
+            }
+        }
+    }
+
+    @Test
+    fun canRecordToMediaStore() {
+        // Arrange.
+        val resolver: ContentResolver = context.contentResolver
+        val outputOptions = createMediaStoreOutputOptions(resolver)
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = finalize.outputResults.outputUri
+        assertThat(uri).isNotEqualTo(Uri.EMPTY)
+        checkFileHasAudioAndVideo(uri)
+
+        // Cleanup.
+        resolver.delete(uri, null, null)
+    }
+
+    @Test
+    @SdkSuppress(minSdkVersion = 26)
+    fun canRecordToFileDescriptor() {
+        // Arrange.
+        val file = createTempFile()
+        val fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
+        val outputOptions = FileDescriptorOutputOptions.Builder(fileDescriptor).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+
+        // Cleanup.
+        fileDescriptor.close()
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioEnabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_withoutAudio_whenAudioDisabled() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoCompletely(outputOptions, audioDisabled)
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileOnlyHasVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenLifecycleStops() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                lifecycleOwner.pauseAndStop()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenTargetQualityChanged() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                cameraController.videoCaptureTargetQuality = nextQuality.get()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.error).isEqualTo(ERROR_SOURCE_INACTIVE)
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenEnabledUseCasesChanged() {
+        // Arrange.
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                cameraController.setEnabledUseCases(IMAGE_ANALYSIS)
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @FlakyTest(bugId = 259294631)
+    @Test
+    fun canRecordToFile_rightAfterPreviousRecordingStopped() {
+        // Arrange.
+        val file1 = createTempFile()
+        val file2 = createTempFile()
+        val outputOptions1 = FileOutputOptions.Builder(file1).build()
+        val outputOptions2 = FileOutputOptions.Builder(file2).build()
+
+        // Pre Act.
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+        recordVideo(outputOptions1, audioEnabled)
+        instrumentation.runOnMainSync {
+            activeRecording.stop()
+            assertThat(cameraController.isRecording).isFalse()
+        }
+
+        // Act.
+        instrumentation.runOnMainSync {
+            startRecording(outputOptions2, audioEnabled)
+            assertThat(cameraController.isRecording).isTrue()
+        }
+
+        // Wait for the Finalize event of the previous recording.
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // reset latches and wait for Start and Status events
+        latchForVideoStarted = CountDownLatch(VIDEO_STARTED_COUNT_DOWN)
+        latchForVideoRecording = CountDownLatch(VIDEO_RECORDING_COUNT_DOWN)
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+        assertThat(latchForVideoStarted.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+        assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Stop the second recording and wait for the Finalize event
+        instrumentation.runOnMainSync {
+            activeRecording.stop()
+            assertThat(cameraController.isRecording).isFalse()
+        }
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+        val uri1 = Uri.fromFile(file1)
+        checkFileHasAudioAndVideo(uri1)
+        val uri2 = Uri.fromFile(file2)
+        checkFileHasAudioAndVideo(uri2)
+
+        // Cleanup.
+        file1.delete()
+        file2.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenPauseAndStop() {
+        val pauseTimes = 1
+
+        // Arrange.
+        latchForVideoPaused = CountDownLatch(pauseTimes)
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                activeRecording.pause()
+            }
+            assertThat(latchForVideoPaused.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun canRecordToFile_whenPauseAndResumeInTheMiddle() {
+        val pauseTimes = 1
+        val resumeTimes = 1
+
+        // Arrange.
+        latchForVideoPaused = CountDownLatch(pauseTimes)
+        latchForVideoResumed = CountDownLatch(resumeTimes)
+        val file = createTempFile()
+        val outputOptions = FileOutputOptions.Builder(file).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioEnabled) {
+            instrumentation.runOnMainSync {
+                activeRecording.pause()
+            }
+            assertThat(latchForVideoPaused.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.resume()
+            }
+            assertThat(latchForVideoResumed.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        val uri = Uri.fromFile(file)
+        checkFileHasAudioAndVideo(uri)
+        assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+        // Cleanup.
+        file.delete()
+    }
+
+    @Test
+    fun startRecording_throwsExceptionWhenAlreadyInRecording() {
+        // Arrange.
+        val file1 = createTempFile()
+        val file2 = createTempFile()
+        val outputOptions1 = FileOutputOptions.Builder(file1).build()
+        val outputOptions2 = FileOutputOptions.Builder(file2).build()
+
+        // Act.
+        recordVideoWithInterruptAction(outputOptions1, audioEnabled) {
+            instrumentation.runOnMainSync {
+                assertThrows(java.lang.IllegalStateException::class.java) {
+                    activeRecording = cameraController.startRecording(
+                        outputOptions2,
+                        audioEnabled,
+                        CameraXExecutors.directExecutor()
+                    ) {}
+                }
+                activeRecording.stop()
+            }
+        }
+
+        // Cleanup.
+        file1.delete()
+        file2.delete()
+    }
+
+    private fun initialLifecycleOwner() {
+        lifecycleOwner = FakeLifecycleOwner()
+        lifecycleOwner.startAndResume()
+    }
+
+    private fun initialPreviewView() {
+        activityRule.scenario.onActivity { activity ->
+            previewView = PreviewView(context)
+            previewView.implementationMode = PreviewView.ImplementationMode.PERFORMANCE
+            activity.setContentView(previewView)
+        }
+    }
+
+    private fun initialController() {
+        cameraController = LifecycleCameraController(context)
+        cameraController.initializationFuture.get()
+        instrumentation.runOnMainSync {
+            if (initialQuality != TargetQuality.None) {
+                cameraController.videoCaptureTargetQuality = initialQuality.get()
+            }
+
+            //  If the PreviewView is not attached, the enabled use cases will not be applied.
+            previewView.controller = cameraController
+
+            cameraController.bindToLifecycle(lifecycleOwner)
+            cameraController.setEnabledUseCases(VIDEO_CAPTURE)
+        }
+    }
+
+    private fun createTempFile(): File {
+        return File.createTempFile("CameraX", ".tmp").apply {
+            deleteOnExit()
+        }
+    }
+
+    private fun createMediaStoreOutputOptions(resolver: ContentResolver): MediaStoreOutputOptions {
+        val videoFileName = "video_" + System.currentTimeMillis()
+        val contentValues = ContentValues()
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName)
+        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
+        return MediaStoreOutputOptions
+            .Builder(resolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+            .setContentValues(contentValues)
+            .build()
+    }
+
+    private fun recordVideoCompletely(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        // Act.
+        recordVideoWithInterruptAction(outputOptions, audioConfig) {
+            instrumentation.runOnMainSync {
+                activeRecording.stop()
+            }
+        }
+
+        // Verify.
+        assertThat(finalize.hasError()).isFalse()
+    }
+
+    private fun recordVideoWithInterruptAction(
+        outputOptions: OutputOptions,
+        audioConfig: AudioConfig,
+        runInterruptAction: () -> Unit
+    ) {
+        // Arrange.
+        latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
+
+        // Act.
+        recordVideo(outputOptions, audioConfig)
+        runInterruptAction()
+
+        // Verify.
+        // Wait for finalize event to saved file.
+        assertThat(latchForVideoSaved.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        instrumentation.runOnMainSync {
+            assertThat(cameraController.isRecording).isFalse()
+        }
+    }
+
+    private fun recordVideo(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        // Arrange.
+        latchForVideoStarted = CountDownLatch(VIDEO_STARTED_COUNT_DOWN)
+        latchForVideoRecording = CountDownLatch(VIDEO_RECORDING_COUNT_DOWN)
+
+        // Act.
+        instrumentation.runOnMainSync {
+            startRecording(outputOptions, audioConfig)
+            assertThat(cameraController.isRecording).isTrue()
+        }
+
+        // Verify.
+        assertThat(latchForVideoStarted.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+
+        // Wait for status event to proceed recording for a while.
+        assertThat(latchForVideoRecording.await(VIDEO_TIMEOUT_SEC, TimeUnit.SECONDS)).isTrue()
+    }
+
+    @MainThread
+    private fun startRecording(outputOptions: OutputOptions, audioConfig: AudioConfig) {
+        if (outputOptions is FileOutputOptions) {
+            activeRecording = cameraController.startRecording(
+                outputOptions,
+                audioConfig,
+                CameraXExecutors.directExecutor(),
+                videoRecordEventListener
+            )
+        } else if (outputOptions is FileDescriptorOutputOptions) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                activeRecording = cameraController.startRecording(
+                    outputOptions,
+                    audioConfig,
+                    CameraXExecutors.directExecutor(),
+                    videoRecordEventListener
+                )
+            } else {
+                throw UnsupportedOperationException(
+                    "File descriptors are not supported on pre-Android O (API 26) devices."
+                )
+            }
+        } else if (outputOptions is MediaStoreOutputOptions) {
+            activeRecording = cameraController.startRecording(
+                outputOptions,
+                audioConfig,
+                CameraXExecutors.directExecutor(),
+                videoRecordEventListener
+            )
+        } else {
+            throw IllegalArgumentException("Unsupported OutputOptions type.")
+        }
+    }
+
+    private fun checkFileOnlyHasVideo(uri: Uri) {
+        checkFileHasVideo(uri)
+        checkFileHasAudio(uri, false)
+    }
+
+    private fun checkFileHasAudioAndVideo(uri: Uri) {
+        checkFileHasVideo(uri)
+        checkFileHasAudio(uri, true)
+    }
+
+    private fun checkFileHasVideo(uri: Uri) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(context, uri)
+            val hasVideo = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+            assertThat(hasVideo).isEqualTo("yes")
+        }
+    }
+
+    private fun checkFileHasAudio(uri: Uri, hasAudio: Boolean) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(context, uri)
+            val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
+
+            assertThat(value).isEqualTo(if (hasAudio) "yes" else null)
+        }
+    }
+
+    private fun skipVideoRecordingTestOnCuttlefishApi29() {
+        // Skip test for b/168175357
+        Assume.assumeFalse(
+            "Cuttlefish has MediaCodec dequeInput/Output buffer fails issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+        )
+    }
+
+    private fun skipTestWithSurfaceProcessingOnCuttlefishApi30() {
+        // Skip test for b/253211491
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 70afe86..f92738c 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -19,9 +19,11 @@
 import static androidx.camera.core.impl.utils.Threads.checkMainThread;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
 import static androidx.camera.view.CameraController.OutputSize.UNASSIGNED_ASPECT_RATIO;
+import static androidx.core.content.ContextCompat.getMainExecutor;
 
 import static java.util.Collections.emptyList;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Matrix;
@@ -38,6 +40,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.AspectRatio;
@@ -64,15 +67,28 @@
 import androidx.camera.core.ViewPort;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.ImageOutputConfig;
+import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.camera.video.FallbackStrategy;
+import androidx.camera.video.FileDescriptorOutputOptions;
+import androidx.camera.video.FileOutputOptions;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.OutputOptions;
+import androidx.camera.video.PendingRecording;
+import androidx.camera.video.Quality;
+import androidx.camera.video.QualitySelector;
+import androidx.camera.video.Recorder;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoCapture;
+import androidx.camera.video.VideoRecordEvent;
 import androidx.camera.view.transform.OutputTransform;
+import androidx.camera.view.video.AudioConfig;
 import androidx.camera.view.video.ExperimentalVideo;
-import androidx.camera.view.video.OnVideoSavedCallback;
-import androidx.camera.view.video.OutputFileOptions;
-import androidx.camera.view.video.OutputFileResults;
+import androidx.core.content.PermissionChecker;
+import androidx.core.util.Consumer;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
@@ -81,10 +97,11 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * The abstract base camera controller class.
@@ -108,7 +125,6 @@
  * {@link UseCase}s freezes the preview for a short period of time. To avoid the glitch, the
  * {@link UseCase}s need to be enabled/disabled before the controller is set on {@link PreviewView}.
  */
-@SuppressWarnings("deprecation")
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
 public abstract class CameraController {
 
@@ -121,6 +137,8 @@
     private static final String CAMERA_NOT_ATTACHED = "Use cases not attached to camera.";
     private static final String IMAGE_CAPTURE_DISABLED = "ImageCapture disabled.";
     private static final String VIDEO_CAPTURE_DISABLED = "VideoCapture disabled.";
+    private static final String VIDEO_RECORDING_UNFINISHED = "Recording video. Only one recording"
+            + " can be active at a time.";
 
     // Auto focus is 1/6 of the area.
     private static final float AF_SIZE = 1.0f / 6.0f;
@@ -254,18 +272,17 @@
     @Nullable
     OutputSize mImageAnalysisTargetSize;
 
-    // Synthetic access
-    @SuppressWarnings("WeakerAccess")
     @NonNull
-    androidx.camera.core.VideoCapture mVideoCapture;
-
-    // Synthetic access
-    @SuppressWarnings("WeakerAccess")
-    @NonNull
-    final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
+    VideoCapture<Recorder> mVideoCapture;
 
     @Nullable
-    OutputSize mVideoCaptureOutputSize;
+    Recording mActiveRecording = null;
+
+    @NonNull
+    Map<Consumer<VideoRecordEvent>, Recording> mRecordingMap = new HashMap<>();
+
+    @Nullable
+    Quality mVideoCaptureQuality;
 
     // The latest bound camera.
     // Synthetic access
@@ -322,7 +339,7 @@
         mPreview = new Preview.Builder().build();
         mImageCapture = new ImageCapture.Builder().build();
         mImageAnalysis = new ImageAnalysis.Builder().build();
-        mVideoCapture = new androidx.camera.core.VideoCapture.Builder().build();
+        mVideoCapture = createNewVideoCapture();
 
         // Wait for camera to be initialized before binding use cases.
         mInitializationFuture = Futures.transform(
@@ -344,6 +361,18 @@
         };
     }
 
+    private static Recorder generateVideoCaptureRecorder(Quality videoQuality) {
+        Recorder.Builder builder = new Recorder.Builder();
+        if (videoQuality != null) {
+            builder.setQualitySelector(QualitySelector.from(
+                    videoQuality,
+                    FallbackStrategy.lowerQualityOrHigherThan(videoQuality)
+            ));
+        }
+
+        return builder.build();
+    }
+
     /**
      * Gets the application context and preserves the attribution tag.
      *
@@ -427,7 +456,6 @@
      * // Switch to video capture to shoot video.
      * controller.setEnabledUseCases(VIDEO_CAPTURE);
      * controller.startRecording(...);
-     * controller.stopRecording(...);
      *
      * // Switch back to image capture and image analysis before taking another picture.
      * controller.setEnabledUseCases(IMAGE_CAPTURE|IMAGE_ANALYSIS);
@@ -453,7 +481,7 @@
         }
         int oldEnabledUseCases = mEnabledUseCases;
         mEnabledUseCases = enabledUseCases;
-        if (!isVideoCaptureEnabled()) {
+        if (!isVideoCaptureEnabled() && isRecording()) {
             stopRecording();
         }
         startCameraAndTrackStates(() -> mEnabledUseCases = oldEnabledUseCases);
@@ -650,7 +678,7 @@
      * <p> By default, the saved image is mirrored to match the output of the preview if front
      * camera is used. To override this behavior, the app needs to explicitly set the flag to
      * {@code false} using {@link ImageCapture.Metadata#setReversedHorizontal} and
-     * {@link OutputFileOptions.Builder#setMetadata}.
+     * {@link ImageCapture.OutputFileOptions.Builder#setMetadata}.
      *
      * @param outputFileOptions  Options to store the newly captured image.
      * @param executor           The executor in which the callback methods will be run.
@@ -1120,112 +1148,308 @@
     }
 
     /**
-     * Takes a video and calls the OnVideoSavedCallback when done.
+     * Takes a video to a given file.
      *
-     * @param outputFileOptions Options to store the newly captured video.
-     * @param executor          The executor in which the callback methods will be run.
-     * @param callback          Callback which will receive success or failure.
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
      */
     @SuppressLint("MissingPermission")
     @ExperimentalVideo
     @MainThread
-    public void startRecording(@NonNull OutputFileOptions outputFileOptions,
-            @NonNull Executor executor, final @NonNull OnVideoSavedCallback callback) {
-        checkMainThread();
-        Preconditions.checkState(isCameraInitialized(), CAMERA_NOT_INITIALIZED);
-        Preconditions.checkState(isVideoCaptureEnabled(), VIDEO_CAPTURE_DISABLED);
-
-        mVideoCapture.startRecording(outputFileOptions.toVideoCaptureOutputFileOptions(), executor,
-                new androidx.camera.core.VideoCapture.OnVideoSavedCallback() {
-                    @Override
-                    public void onVideoSaved(
-                            @NonNull androidx.camera.core.VideoCapture.OutputFileResults
-                                    outputFileResults) {
-                        mVideoIsRecording.set(false);
-                        callback.onVideoSaved(
-                                OutputFileResults.create(outputFileResults.getSavedUri()));
-                    }
-
-                    @Override
-                    public void onError(int videoCaptureError, @NonNull String message,
-                            @Nullable Throwable cause) {
-                        mVideoIsRecording.set(false);
-                        callback.onError(videoCaptureError, message, cause);
-                    }
-                });
-        mVideoIsRecording.set(true);
+    @NonNull
+    public Recording startRecording(
+            @NonNull FileOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
     }
 
     /**
-     * Stops a in progress video recording.
+     * Takes a video to a given file descriptor.
+     *
+     * <p> Currently, file descriptors as output destinations are not supported on pre-Android O
+     * (API 26) devices.
+     *
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
      */
+    @SuppressLint("MissingPermission")
+    @ExperimentalVideo
+    @RequiresApi(26)
+    @MainThread
+    @NonNull
+    public Recording startRecording(
+            @NonNull FileDescriptorOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
+    }
+
+    /**
+     * Takes a video to MediaStore.
+     *
+     * <p> Only a single recording can be active at a time, so if {@link #isRecording()} is true,
+     * this will throw an {@link IllegalStateException}.
+     *
+     * <p> Upon successfully starting the recording, a {@link VideoRecordEvent.Start} event will
+     * be the first event sent to the provided event listener.
+     *
+     * <p> If errors occur while starting the recording, a {@link VideoRecordEvent.Finalize} event
+     * will be the first event sent to the provided listener, and information about the error can
+     * be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
+     *
+     * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+     * permission; without it, starting a recording will fail with a {@link SecurityException}.
+     *
+     * @param outputOptions the options to store the newly captured video.
+     * @param audioConfig the configuration of audio.
+     * @param executor the executor that the event listener will be run on.
+     * @param listener the event listener to handle video record events.
+     * @return a {@link Recording} that provides controls for new active recordings.
+     * @throws IllegalStateException if there is an unfinished active recording.
+     * @throws SecurityException if the audio config specifies audio should be enabled but the
+     * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
+     */
+    @SuppressLint("MissingPermission")
     @ExperimentalVideo
     @MainThread
-    public void stopRecording() {
+    @NonNull
+    public Recording startRecording(
+            @NonNull MediaStoreOutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        return startRecordingInternal(outputOptions, audioConfig, executor, listener);
+    }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @ExperimentalVideo
+    @MainThread
+    private Recording startRecordingInternal(
+            @NonNull OutputOptions outputOptions,
+            @NonNull AudioConfig audioConfig,
+            @NonNull Executor executor,
+            @NonNull Consumer<VideoRecordEvent> listener) {
         checkMainThread();
-        if (mVideoIsRecording.get()) {
-            mVideoCapture.stopRecording();
+        Preconditions.checkState(isCameraInitialized(), CAMERA_NOT_INITIALIZED);
+        Preconditions.checkState(isVideoCaptureEnabled(), VIDEO_CAPTURE_DISABLED);
+        Preconditions.checkState(!isRecording(), VIDEO_RECORDING_UNFINISHED);
+
+        Consumer<VideoRecordEvent> wrappedListener =
+                wrapListenerToDeactivateRecordingOnFinalized(listener);
+        PendingRecording pendingRecording = prepareRecording(outputOptions);
+        boolean isAudioEnabled = audioConfig.getAudioEnabled();
+        if (isAudioEnabled) {
+            checkAudioPermissionGranted();
+            pendingRecording.withAudioEnabled();
+        }
+        Recording recording = pendingRecording.start(executor, wrappedListener);
+        setActiveRecording(recording, wrappedListener);
+
+        return recording;
+    }
+
+    private void checkAudioPermissionGranted() {
+        int permissionState = PermissionChecker.checkSelfPermission(mAppContext,
+                Manifest.permission.RECORD_AUDIO);
+        if (permissionState == PermissionChecker.PERMISSION_DENIED) {
+            throw new SecurityException("Attempted to start recording with audio, but "
+                    + "application does not have RECORD_AUDIO permission granted.");
         }
     }
 
     /**
-     * Returns whether there is a in progress video recording.
+     * Generates a {@link PendingRecording} instance for starting a recording.
+     *
+     * <p> This method handles {@code prepareRecording()} methods for different output formats,
+     * and makes {@link #startRecordingInternal(OutputOptions, AudioConfig, Executor, Consumer)}
+     * only handle the general flow.
+     *
+     * <p> This method uses the parent class {@link OutputOptions} as the parameter. On the other
+     * hand, the public {@code startRecording()} is overloaded with subclasses. The reason is to
+     * enforce compile-time check for API levels.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private PendingRecording prepareRecording(@NonNull OutputOptions options) {
+        Recorder recorder = mVideoCapture.getOutput();
+        if (options instanceof FileOutputOptions) {
+            return recorder.prepareRecording(mAppContext, (FileOutputOptions) options);
+        } else if (options instanceof FileDescriptorOutputOptions) {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+                throw new UnsupportedOperationException(
+                        "File descriptors are not supported on pre-Android O (API 26) devices."
+                );
+            }
+            return recorder.prepareRecording(mAppContext, (FileDescriptorOutputOptions) options);
+        } else if (options instanceof MediaStoreOutputOptions) {
+            return recorder.prepareRecording(mAppContext, (MediaStoreOutputOptions) options);
+        } else {
+            throw new IllegalArgumentException("Unsupported OutputOptions type.");
+        }
+    }
+
+    @ExperimentalVideo
+    private Consumer<VideoRecordEvent> wrapListenerToDeactivateRecordingOnFinalized(
+            @NonNull final Consumer<VideoRecordEvent> listener) {
+        final Executor mainExecutor = getMainExecutor(mAppContext);
+
+        return new Consumer<VideoRecordEvent>() {
+            @Override
+            public void accept(VideoRecordEvent videoRecordEvent) {
+                if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
+                    if (!Threads.isMainThread()) {
+                        // Post on main thread to ensure thread safety.
+                        mainExecutor.execute(() -> deactivateRecordingByListener(this));
+                    } else {
+                        deactivateRecordingByListener(this);
+                    }
+                }
+                listener.accept(videoRecordEvent);
+            }
+        };
+    }
+
+    @ExperimentalVideo
+    @MainThread
+    void deactivateRecordingByListener(@NonNull Consumer<VideoRecordEvent> listener) {
+        Recording recording = mRecordingMap.remove(listener);
+        if (recording != null) {
+            deactivateRecording(recording);
+        }
+    }
+
+    /**
+     * Clears the active video recording reference if the recording to be deactivated matches.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private void deactivateRecording(@NonNull Recording recording) {
+        if (mActiveRecording == recording) {
+            mActiveRecording = null;
+        }
+    }
+
+    @ExperimentalVideo
+    @MainThread
+    private void setActiveRecording(
+            @NonNull Recording recording,
+            @NonNull Consumer<VideoRecordEvent> listener) {
+        mRecordingMap.put(listener, recording);
+        mActiveRecording = recording;
+    }
+
+    /**
+     * Stops an in-progress video recording.
+     *
+     * <p> Once the current recording has been stopped, the next recording can be started.
+     *
+     * <p> If the recording completes successfully, a {@link VideoRecordEvent.Finalize} event with
+     * {@link VideoRecordEvent.Finalize#ERROR_NONE} will be sent to the provided listener.
+     */
+    @ExperimentalVideo
+    @MainThread
+    private void stopRecording() {
+        checkMainThread();
+
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+            deactivateRecording(mActiveRecording);
+        }
+    }
+
+    /**
+     * Returns whether there is an in-progress video recording.
      */
     @ExperimentalVideo
     @MainThread
     public boolean isRecording() {
         checkMainThread();
-        return mVideoIsRecording.get();
+        return mActiveRecording != null && !mActiveRecording.isClosed();
     }
 
     /**
-     * Sets the intended video size for {@code VideoCapture}.
+     * Sets the intended video quality for {@code VideoCapture}.
      *
-     * <p> The value is used as a hint when determining the resolution and aspect ratio of
-     * the video. The actual output may differ from the requested value due to device constraints.
+     * <p> The value is used as a hint when determining the resolution of the video.
+     * The actual output may differ from the requested value due to device constraints.
+     * The {@link FallbackStrategy#lowerQualityOrHigherThan(Quality)} fallback strategy
+     * will be applied when the quality is not supported.
      *
-     * <p> When set to null, the output will be based on the default config of {@code VideoCapture}.
+     * <p> When set to null, the output will be based on the default config of {@link
+     * Recorder#DEFAULT_QUALITY_SELECTOR}.
      *
-     * <p> Changing the value will reconfigure the camera which will cause video capture to stop.
-     * To avoid this, set the value before controller is bound to lifecycle.
+     * <p> Changing the value will reconfigure the camera which will cause video
+     * capture to stop. To avoid this, set the value before controller is bound to
+     * lifecycle.
      *
-     * @param targetSize the intended video size for {@code VideoCapture}.
+     * @param targetQuality the intended video quality for {@code VideoCapture}.
      */
     @ExperimentalVideo
     @MainThread
-    public void setVideoCaptureTargetSize(@Nullable OutputSize targetSize) {
+    public void setVideoCaptureTargetQuality(@Nullable Quality targetQuality) {
         checkMainThread();
-        if (isOutputSizeEqual(mVideoCaptureOutputSize, targetSize)) {
+        if (targetQuality == mVideoCaptureQuality) {
             return;
         }
-        mVideoCaptureOutputSize = targetSize;
-        unbindVideoAndRecreate();
+        mVideoCaptureQuality = targetQuality;
         startCameraAndTrackStates();
     }
 
     /**
-     * Returns the intended output size for {@code VideoCapture} set by
-     * {@link #setVideoCaptureTargetSize(OutputSize)}, or null if not set.
+     * Returns the intended quality for {@code VideoCapture} set by
+     * {@link #setVideoCaptureTargetQuality(Quality)}, or null if not set.
      */
     @ExperimentalVideo
     @MainThread
     @Nullable
-    public OutputSize getVideoCaptureTargetSize() {
+    public Quality getVideoCaptureTargetQuality() {
         checkMainThread();
-        return mVideoCaptureOutputSize;
+        return mVideoCaptureQuality;
     }
 
-    /**
-     * Unbinds VideoCapture and recreate with the latest parameters.
-     */
-    private void unbindVideoAndRecreate() {
-        if (isCameraInitialized()) {
-            mCameraProvider.unbind(mVideoCapture);
-        }
-        androidx.camera.core.VideoCapture.Builder builder =
-                new androidx.camera.core.VideoCapture.Builder();
-        setTargetOutputSize(builder, mVideoCaptureOutputSize);
-        mVideoCapture = builder.build();
+    private VideoCapture<Recorder> createNewVideoCapture() {
+        return VideoCapture.withOutput(generateVideoCaptureRecorder(mVideoCaptureQuality));
     }
 
     // -----------------
@@ -1761,10 +1985,11 @@
             mCameraProvider.unbind(mImageAnalysis);
         }
 
+        // TODO: revert aosp/2280599 to reuse VideoCapture when VideoCapture supports reuse.
+        mCameraProvider.unbind(mVideoCapture);
         if (isVideoCaptureEnabled()) {
+            mVideoCapture = createNewVideoCapture();
             builder.addUseCase(mVideoCapture);
-        } else {
-            mCameraProvider.unbind(mVideoCapture);
         }
 
         builder.setViewPort(mViewPort);
@@ -1806,7 +2031,6 @@
      * @see #setImageAnalysisTargetSize(OutputSize)
      * @see #setPreviewTargetSize(OutputSize)
      * @see #setImageCaptureTargetSize(OutputSize)
-     * @see #setVideoCaptureTargetSize(OutputSize)
      */
     @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
     public static final class OutputSize {
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
index 4049e5c..a51658b 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/PreviewTransformation.java
@@ -109,6 +109,8 @@
     private int mTargetRotation;
     // Whether the preview is using front camera.
     private boolean mIsFrontCamera;
+    // Whether the Surface contains camera transform.
+    private boolean mHasCameraTransform;
 
     private PreviewView.ScaleType mScaleType = DEFAULT_SCALE_TYPE;
 
@@ -129,12 +131,21 @@
         mTargetRotation = transformationInfo.getTargetRotation();
         mResolution = resolution;
         mIsFrontCamera = isFrontCamera;
+        mHasCameraTransform = transformationInfo.hasCameraTransform();
     }
 
     /**
      * Override with display rotation when Preview does not have a target rotation set.
+     *
+     * TODO: move the PreviewView#updateDisplayRotationIfNeeded logic into PreviewTransformation
+     *  so all the transformation logic will be in one place.
      */
     void overrideWithDisplayRotation(int rotationDegrees, int displayRotation) {
+        if (!mHasCameraTransform) {
+            // When the Surface doesn't have the camera transform, we use mPreviewRotationDegrees
+            // from the core directly. There is no need to override the values.
+            return;
+        }
         mPreviewRotationDegrees = rotationDegrees;
         mTargetRotation = displayRotation;
     }
@@ -154,10 +165,35 @@
     Matrix getTextureViewCorrectionMatrix() {
         Preconditions.checkState(isTransformationInfoReady());
         RectF surfaceRect = new RectF(0, 0, mResolution.getWidth(), mResolution.getHeight());
-        int rotationDegrees = -surfaceRotationToDegrees(mTargetRotation);
+        int rotationDegrees = getRemainingRotationDegrees();
         return getRectToRect(surfaceRect, surfaceRect, rotationDegrees);
     }
 
+
+    /**
+     * Gets the remaining rotation degrees after the preview is transformed by Android Views.
+     *
+     * <p>Both {@link TextureView} or {@link SurfaceView} uses the camera transform encoded in
+     * the {@link Surface} to correct the output. The remaining rotation degrees depends on
+     * whether the camera transform is present.
+     */
+    private int getRemainingRotationDegrees() {
+        if (mTargetRotation == ROTATION_NOT_SPECIFIED && !mHasCameraTransform) {
+            // If the Surface is not connected to the camera, then the SurfaceView/TextureView will
+            // not apply any transformation. In that case, we need to apply the rotation
+            // calculated by CameraX.
+            return mPreviewRotationDegrees;
+        } else if (mHasCameraTransform && mTargetRotation != ROTATION_NOT_SPECIFIED) {
+            // If the Surface is connected to the camera, then the SurfaceView/TextureView
+            // will be the one to apply the camera orientation. In that case, only the Surface
+            // rotation needs to be applied by PreviewView.
+            return -surfaceRotationToDegrees(mTargetRotation);
+        } else {
+            throw new IllegalStateException("Target rotation must be specified. Target rotation: "
+                    + mTargetRotation + " hasCameraTransform " + mHasCameraTransform);
+        }
+    }
+
     /**
      * Calculates the transformation and applies it to the inner view of {@link PreviewView}.
      *
@@ -180,9 +216,12 @@
         } else {
             // Logs an error if non-display rotation is used with SurfaceView.
             Display display = preview.getDisplay();
-            if (display != null && display.getRotation() != mTargetRotation) {
-                Logger.e(TAG, "Non-display rotation not supported with SurfaceView / PERFORMANCE "
-                        + "mode.");
+            boolean mismatchedDisplayRotation = mHasCameraTransform && display != null
+                    && display.getRotation() != mTargetRotation;
+            boolean hasRemainingRotation =
+                    !mHasCameraTransform && getRemainingRotationDegrees() != 0;
+            if (mismatchedDisplayRotation || hasRemainingRotation) {
+                Logger.e(TAG, "Custom rotation not supported with SurfaceView/PERFORMANCE mode.");
             }
         }
 
@@ -441,7 +480,10 @@
     }
 
     private boolean isTransformationInfoReady() {
+        // Ignore target rotation if Surface doesn't have camera transform.
+        boolean isTargetRotationSpecified =
+                !mHasCameraTransform || (mTargetRotation != ROTATION_NOT_SPECIFIED);
         return mSurfaceCropRect != null && mResolution != null
-                && mTargetRotation != ROTATION_NOT_SPECIFIED;
+                && isTargetRotationSpecified;
     }
 }
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
new file mode 100644
index 0000000..67977b7
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.view.video;
+
+import android.Manifest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+
+/**
+ * A class providing configuration for audio settings in the video recording.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@ExperimentalVideo
+public class AudioConfig {
+
+    /**
+     * The audio configuration with audio disabled.
+     */
+    @NonNull
+    public static final AudioConfig AUDIO_DISABLED = new AudioConfig(false);
+
+    private final boolean mIsAudioEnabled;
+
+    AudioConfig(boolean audioEnabled) {
+        mIsAudioEnabled = audioEnabled;
+    }
+
+    /**
+     * Creates a default {@link AudioConfig} with the given audio enabled state.
+     *
+     * <p> The {@link android.Manifest.permission#RECORD_AUDIO} permission is required to
+     * enable audio in video recording; for the use cases where audio is always disabled, please
+     * use {@link AudioConfig#AUDIO_DISABLED} instead, which has no permission requirements.
+     */
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @NonNull
+    public static AudioConfig create(boolean enableAudio) {
+        return new AudioConfig(enableAudio);
+    }
+
+    /**
+     * Get the audio enabled state.
+     */
+    public boolean getAudioEnabled() {
+        return mIsAudioEnabled;
+    }
+}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java b/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java
deleted file mode 100644
index 121a020..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/Metadata.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import android.location.Location;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-import com.google.auto.value.AutoValue;
-
-/** Holder class for metadata that should be saved alongside captured video. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class Metadata {
-    /**
-     * Returns a {@link Location} object representing the geographic location where the video was
-     * taken.
-     *
-     * @return The location object or {@code null} if no location was set.
-     */
-    @Nullable
-    public abstract Location getLocation();
-
-    /** Creates a {@link Builder}. */
-    @NonNull
-    public static Builder builder() {
-        return new AutoValue_Metadata.Builder();
-    }
-
-    // Don't allow inheritance outside of package
-    Metadata() {
-    }
-
-    /** The builder for {@link Metadata}. */
-    @SuppressWarnings("StaticFinalBuilder")
-    @AutoValue.Builder
-    public abstract static class Builder {
-        /**
-         * Sets a {@link Location} object representing a geographic location where the video was
-         * taken.
-         *
-         * <p>If {@code null}, no location information will be saved with the video. Default
-         * value is {@code null}.
-         */
-        @NonNull
-        public abstract Builder setLocation(@Nullable Location location);
-
-        /** Build the {@link Metadata} from this builder. */
-        @NonNull
-        public abstract Metadata build();
-
-        Builder() {
-        }
-    }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java
deleted file mode 100644
index 5783d89..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OnVideoSavedCallback.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import androidx.annotation.IntDef;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/** Listener containing callbacks for video file I/O events. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@SuppressWarnings("deprecation")
-@ExperimentalVideo
-public interface OnVideoSavedCallback {
-    /**
-     * An unknown error occurred.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     */
-    int ERROR_UNKNOWN = androidx.camera.core.VideoCapture.ERROR_UNKNOWN;
-    /**
-     * An error occurred with encoder state, either when trying to change state or when an
-     * unexpected state change occurred.
-     */
-    int ERROR_ENCODER = androidx.camera.core.VideoCapture.ERROR_ENCODER;
-    /** An error with muxer state such as during creation or when stopping. */
-    int ERROR_MUXER = androidx.camera.core.VideoCapture.ERROR_MUXER;
-    /**
-     * An error indicating start recording was called when video recording is still in progress.
-     */
-    int ERROR_RECORDING_IN_PROGRESS = androidx.camera.core.VideoCapture.ERROR_RECORDING_IN_PROGRESS;
-    /**
-     * An error indicating the file saving operations.
-     */
-    int ERROR_FILE_IO = androidx.camera.core.VideoCapture.ERROR_FILE_IO;
-    /**
-     * An error indicating this VideoCapture is not bound to a camera.
-     */
-    int ERROR_INVALID_CAMERA = androidx.camera.core.VideoCapture.ERROR_INVALID_CAMERA;
-
-    /**
-     * Describes the error that occurred during video capture operations.
-     *
-     * <p>This is a parameter sent to the error callback functions set in listeners such as {@link
-     * OnVideoSavedCallback#onError(int, String, Throwable)}.
-     *
-     * <p>See message parameter in onError callback or log for more details.
-     *
-     * @hide
-     */
-    @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS,
-            ERROR_FILE_IO, ERROR_INVALID_CAMERA})
-    @Retention(RetentionPolicy.SOURCE)
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @interface VideoCaptureError {
-    }
-
-    /** Called when the video has been successfully saved. */
-    void onVideoSaved(@NonNull OutputFileResults outputFileResults);
-
-    /** Called when an error occurs while attempting to save the video. */
-    void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
-            @Nullable Throwable cause);
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java
deleted file mode 100644
index 48360e0..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileOptions.java
+++ /dev/null
@@ -1,247 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import static androidx.annotation.RestrictTo.Scope.LIBRARY;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.os.Build;
-import android.os.ParcelFileDescriptor;
-import android.provider.MediaStore;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.core.util.Preconditions;
-
-import com.google.auto.value.AutoValue;
-
-import java.io.File;
-
-/**
- * Options for saving newly captured video.
- *
- * <p> this class is used to configure save location and metadata. Save location can be
- * either a {@link File}, {@link MediaStore}. The metadata will be
- * stored with the saved video.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class OutputFileOptions {
-
-    // Empty metadata object used as a placeholder for no user-supplied metadata.
-    // Should be initialized to all default values.
-    private static final Metadata EMPTY_METADATA = Metadata.builder().build();
-
-    // Restrict constructor to same package
-    OutputFileOptions() {
-    }
-
-    /**
-     * Creates options to write captured video to a {@link File}.
-     *
-     * @param file save location of the video.
-     */
-    @NonNull
-    public static Builder builder(@NonNull File file) {
-        return new AutoValue_OutputFileOptions.Builder().setMetadata(EMPTY_METADATA).setFile(file);
-    }
-
-    /**
-     * Creates options to write captured video to a {@link ParcelFileDescriptor}.
-     *
-     * <p>Using a ParcelFileDescriptor to record a video is only supported for Android 8.0 or
-     * above.
-     *
-     * @param fileDescriptor to save the video.
-     * @throws IllegalArgumentException when the device is not running Android 8.0 or above.
-     */
-    @NonNull
-    public static Builder builder(@NonNull ParcelFileDescriptor fileDescriptor) {
-        Preconditions.checkArgument(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O,
-                "Using a ParcelFileDescriptor to record a video is only supported for Android 8"
-                        + ".0 or above.");
-
-        return new AutoValue_OutputFileOptions.Builder().setMetadata(
-                EMPTY_METADATA).setFileDescriptor(fileDescriptor);
-    }
-
-    /**
-     * Creates options to write captured video to {@link MediaStore}.
-     *
-     * Example:
-     *
-     * <pre>{@code
-     *
-     * ContentValues contentValues = new ContentValues();
-     * contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, "NEW_VIDEO");
-     * contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-     *
-     * OutputFileOptions options = OutputFileOptions.builder(
-     *         getContentResolver(),
-     *         MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-     *         contentValues).build();
-     *
-     * }</pre>
-     *
-     * @param contentResolver to access {@link MediaStore}
-     * @param saveCollection  The URI of the table to insert into.
-     * @param contentValues   to be included in the created video file.
-     */
-    @NonNull
-    public static Builder builder(@NonNull ContentResolver contentResolver,
-            @NonNull Uri saveCollection,
-            @NonNull ContentValues contentValues) {
-        return new AutoValue_OutputFileOptions.Builder()
-                .setMetadata(EMPTY_METADATA)
-                .setContentResolver(contentResolver)
-                .setSaveCollection(saveCollection).setContentValues(contentValues);
-    }
-
-    /**
-     * Returns the File object which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract File getFile();
-
-    /**
-     * Returns the ParcelFileDescriptor object which is set by the
-     * {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ParcelFileDescriptor getFileDescriptor();
-
-    /**
-     * Returns the content resolver which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ContentResolver getContentResolver();
-
-    /**
-     * Returns the URI which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract Uri getSaveCollection();
-
-    /**
-     * Returns the content values which is set by the {@link OutputFileOptions.Builder}.
-     */
-    @Nullable
-    abstract ContentValues getContentValues();
-
-    /** Returns the metadata which is set by the {@link OutputFileOptions.Builder}. */
-    @NonNull
-    public abstract Metadata getMetadata();
-
-    /**
-     * Checking the caller wants to save video to MediaStore.
-     */
-    private boolean isSavingToMediaStore() {
-        return getSaveCollection() != null && getContentResolver() != null
-                && getContentValues() != null;
-    }
-
-    /**
-     * Checking the caller wants to save video to a File.
-     */
-    private boolean isSavingToFile() {
-        return getFile() != null;
-    }
-
-    /**
-     * Checking the caller wants to save video to a ParcelFileDescriptor.
-     */
-    private boolean isSavingToFileDescriptor() {
-        return getFileDescriptor() != null;
-    }
-
-    /**
-     * Converts to a {@link androidx.camera.core.VideoCapture.OutputFileOptions}.
-     *
-     * @hide
-     */
-    @RestrictTo(LIBRARY)
-    @SuppressWarnings("deprecation")
-    @NonNull
-    public androidx.camera.core.VideoCapture.OutputFileOptions toVideoCaptureOutputFileOptions() {
-        androidx.camera.core.VideoCapture.OutputFileOptions.Builder
-                internalOutputFileOptionsBuilder;
-        if (isSavingToFile()) {
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getFile()));
-        } else if (isSavingToFileDescriptor()) {
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getFileDescriptor()).getFileDescriptor());
-        } else {
-            Preconditions.checkState(isSavingToMediaStore());
-            internalOutputFileOptionsBuilder =
-                    new androidx.camera.core.VideoCapture.OutputFileOptions.Builder(
-                            Preconditions.checkNotNull(getContentResolver()),
-                            Preconditions.checkNotNull(getSaveCollection()),
-                            Preconditions.checkNotNull(getContentValues()));
-        }
-
-        androidx.camera.core.VideoCapture.Metadata internalMetadata =
-                new androidx.camera.core.VideoCapture.Metadata();
-        internalMetadata.location = getMetadata().getLocation();
-        internalOutputFileOptionsBuilder.setMetadata(internalMetadata);
-
-        return internalOutputFileOptionsBuilder.build();
-    }
-
-    /**
-     * Builder class for {@link OutputFileOptions}.
-     */
-    @AutoValue.Builder
-    @SuppressWarnings("StaticFinalBuilder")
-    public abstract static class Builder {
-
-        // Restrict construction to same package
-        Builder() {
-        }
-
-        abstract Builder setFile(@Nullable File file);
-
-        abstract Builder setFileDescriptor(@Nullable ParcelFileDescriptor fileDescriptor);
-
-        abstract Builder setContentResolver(@Nullable ContentResolver contentResolver);
-
-        abstract Builder setSaveCollection(@Nullable Uri uri);
-
-        abstract Builder setContentValues(@Nullable ContentValues contentValues);
-
-        /**
-         * Sets the metadata to be stored with the saved video.
-         *
-         * @param metadata Metadata to be stored with the saved video.
-         */
-        @NonNull
-        public abstract Builder setMetadata(@NonNull Metadata metadata);
-
-        /**
-         * Builds {@link OutputFileOptions}.
-         */
-        @NonNull
-        public abstract OutputFileOptions build();
-    }
-}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java b/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java
deleted file mode 100644
index d9a64a0..0000000
--- a/camera/camera-view/src/main/java/androidx/camera/view/video/OutputFileResults.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.view.video;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.net.Uri;
-import android.provider.MediaStore;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-
-import com.google.auto.value.AutoValue;
-
-/**
- * Info about the saved video file.
- */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-@ExperimentalVideo
-@AutoValue
-public abstract class OutputFileResults {
-
-    // Restrict constructor to package
-    OutputFileResults() {
-    }
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    @NonNull
-    public static OutputFileResults create(@Nullable Uri savedUri) {
-        return new AutoValue_OutputFileResults(savedUri);
-    }
-
-    /**
-     * Returns the {@link Uri} of the saved video file.
-     *
-     * @return URI of saved video file if the {@link OutputFileOptions} is backed by
-     * {@link MediaStore} using
-     * {@link OutputFileOptions#builder(ContentResolver, Uri, ContentValues)}, {@code null}
-     * otherwise.
-     */
-    @Nullable
-    public abstract Uri getSavedUri();
-}
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
index d69c153..b85fa8a 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/CameraControllerTest.kt
@@ -36,6 +36,7 @@
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.view.CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED
 import androidx.camera.view.transform.OutputTransform
+import androidx.camera.video.Quality
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -64,6 +65,7 @@
         CameraController.OutputSize(AspectRatio.RATIO_16_9)
     private val targetSizeWithResolution =
         CameraController.OutputSize(Size(1080, 1960))
+    private val targetVideoQuality = Quality.HIGHEST
 
     @Before
     public fun setUp() {
@@ -287,22 +289,9 @@
 
     @UiThreadTest
     @Test
-    public fun setVideoCaptureResolution() {
-        controller.videoCaptureTargetSize = targetSizeWithResolution
-        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithResolution)
-
-        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
-        assertThat(config.targetResolution).isEqualTo(targetSizeWithResolution.resolution)
-    }
-
-    @UiThreadTest
-    @Test
-    public fun setVideoCaptureAspectRatio() {
-        controller.videoCaptureTargetSize = targetSizeWithAspectRatio
-        assertThat(controller.videoCaptureTargetSize).isEqualTo(targetSizeWithAspectRatio)
-
-        val config = controller.mVideoCapture.currentConfig as ImageOutputConfig
-        assertThat(config.targetAspectRatio).isEqualTo(targetSizeWithAspectRatio.aspectRatio)
+    fun setVideoCaptureQuality() {
+        controller.videoCaptureTargetQuality = targetVideoQuality
+        assertThat(controller.videoCaptureTargetQuality).isEqualTo(targetVideoQuality)
     }
 
     @UiThreadTest
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
index ddf2b9e..bacd6d4 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewTransformationTest.kt
@@ -23,6 +23,7 @@
 import android.view.Surface
 import android.view.View
 import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.ImageOutputConfig.ROTATION_NOT_SPECIFIED
 import androidx.camera.core.impl.ImageOutputConfig.RotationValue
 import androidx.camera.core.impl.utils.TransformUtils.sizeToVertices
 import androidx.test.core.app.ApplicationProvider
@@ -111,7 +112,10 @@
     private fun isCropRectAspectRatioMatchPreviewView(cropRect: Rect): Boolean {
         mPreviewTransform.setTransformationInfo(
             // Height and width is swapped because rotation is 90°.
-            SurfaceRequest.TransformationInfo.of(cropRect, 90, ARBITRARY_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                cropRect, 90, ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
+            ),
             SURFACE_SIZE,
             BACK_CAMERA
         )
@@ -119,6 +123,36 @@
     }
 
     @Test
+    fun withoutCameraTransform_isScalingOnly() {
+        // Arrange: set up a stream that is already corrected, crop rect is full rect, no
+        // rotation and no camera transform.
+        val croppedSize = Size(40, 20)
+        mPreviewTransform.setTransformationInfo(
+            SurfaceRequest.TransformationInfo.of(
+                Rect(0, 0, croppedSize.width, croppedSize.height),
+                /*rotationDegrees*/0,
+                ROTATION_NOT_SPECIFIED,
+                /*hasCameraTransform=*/false
+            ),
+            croppedSize,
+            /*isFrontCamera=*/false
+        )
+
+        // Act.
+        mPreviewTransform.transformView(PREVIEW_VIEW_SIZE, LayoutDirection.LTR, mView)
+
+        // Assert: PreviewView simply scales the output.
+        assertThat(mView.scaleX).isWithin(FLOAT_ERROR)
+            .of(PREVIEW_VIEW_SIZE.width / croppedSize.width.toFloat())
+        assertThat(mView.scaleY).isWithin(FLOAT_ERROR)
+            .of(PREVIEW_VIEW_SIZE.height / croppedSize.height.toFloat())
+        assertThat(mView.translationX).isWithin(FLOAT_ERROR).of(0f)
+        assertThat(mView.translationY).isWithin(FLOAT_ERROR).of(0f)
+        // Assert: no correction needed because the stream is already correct.
+        assertThat(mPreviewTransform.textureViewCorrectionMatrix.isIdentity).isTrue()
+    }
+
+    @Test
     fun correctTextureViewWith0Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_0)).isEqualTo(
             intArrayOf(
@@ -195,7 +229,12 @@
     ): IntArray {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of(CROP_RECT, 90, rotation),
+            SurfaceRequest.TransformationInfo.of(
+                CROP_RECT,
+                90,
+                rotation,
+                /*hasCameraTransform=*/true
+            ),
             SURFACE_SIZE,
             isFrontCamera
         )
@@ -223,7 +262,8 @@
             SurfaceRequest.TransformationInfo.of(
                 CROP_RECT,
                 90,
-                ARBITRARY_ROTATION
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
             ),
             SURFACE_SIZE, BACK_CAMERA
         )
@@ -351,7 +391,12 @@
     ) {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
-            SurfaceRequest.TransformationInfo.of(MISMATCHED_CROP_RECT, 90, ARBITRARY_ROTATION),
+            SurfaceRequest.TransformationInfo.of(
+                MISMATCHED_CROP_RECT,
+                90,
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
+            ),
             FIT_SURFACE_SIZE,
             isFrontCamera
         )
@@ -436,7 +481,8 @@
             SurfaceRequest.TransformationInfo.of(
                 cropRect,
                 rotationDegrees,
-                ARBITRARY_ROTATION
+                ARBITRARY_ROTATION,
+                /*hasCameraTransform=*/true
             ),
             SURFACE_SIZE,
             isFrontCamera
diff --git a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
index 3269265..e763fdb 100644
--- a/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
+++ b/camera/camera-view/src/test/java/androidx/camera/view/PreviewViewMeteringPointFactoryTest.java
@@ -66,8 +66,13 @@
         // Arrange.
         PreviewTransformation previewTransformation = new PreviewTransformation();
         previewTransformation.setTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(new Rect(0, 0, WIDTH, HEIGHT), 0,
-                        Surface.ROTATION_0), new Size(WIDTH, HEIGHT), false);
+                SurfaceRequest.TransformationInfo.of(
+                        new Rect(0, 0, WIDTH, HEIGHT),
+                        0,
+                        Surface.ROTATION_0,
+                        /*hasCameraTransform=*/true),
+                new Size(WIDTH, HEIGHT),
+                /*isFrontCamera=*/false);
         PreviewViewMeteringPointFactory previewViewMeteringPointFactory =
                 new PreviewViewMeteringPointFactory(previewTransformation);
 
@@ -85,8 +90,13 @@
         // Arrange.
         PreviewTransformation previewTransformation = new PreviewTransformation();
         previewTransformation.setTransformationInfo(
-                SurfaceRequest.TransformationInfo.of(new Rect(0, 0, WIDTH, HEIGHT), 0,
-                        Surface.ROTATION_0), new Size(WIDTH, HEIGHT), false);
+                SurfaceRequest.TransformationInfo.of(
+                        new Rect(0, 0, WIDTH, HEIGHT),
+                        /*rotationDegrees=*/0,
+                        Surface.ROTATION_0,
+                        /*hasCameraTransform=*/true),
+                new Size(WIDTH, HEIGHT),
+                /*isFrontCamera=*/false);
         PreviewViewMeteringPointFactory previewViewMeteringPointFactory =
                 new PreviewViewMeteringPointFactory(previewTransformation);
 
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
index ff72ca2..2acc74e 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorScreen.kt
@@ -22,7 +22,6 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
@@ -63,10 +62,13 @@
         isSignalActive = viewModel.isActivePeriod,
         isSignalStarted = viewModel.isSignalGenerating,
         isRecording = viewModel.isRecording,
+        isPaused = viewModel.isPaused,
         onSignalStartClick = { viewModel.startSignalGeneration() },
         onSignalStopClick = { viewModel.stopSignalGeneration() },
         onRecordingStartClick = { viewModel.startRecording(context) },
         onRecordingStopClick = { viewModel.stopRecording() },
+        onRecordingPauseClick = { viewModel.pauseRecording() },
+        onRecordingResumeClick = { viewModel.resumeRecording() },
     )
 }
 
@@ -77,10 +79,13 @@
     isSignalActive: Boolean = false,
     isSignalStarted: Boolean = false,
     isRecording: Boolean = false,
+    isPaused: Boolean = false,
     onSignalStartClick: () -> Unit = {},
     onSignalStopClick: () -> Unit = {},
     onRecordingStartClick: () -> Unit = {},
     onRecordingStopClick: () -> Unit = {},
+    onRecordingPauseClick: () -> Unit = {},
+    onRecordingResumeClick: () -> Unit = {},
 ) {
     Box(modifier = Modifier.fillMaxSize()) {
         LightingScreen(isOn = isSignalActive)
@@ -93,8 +98,11 @@
         RecordingControl(
             enabled = isRecorderReady,
             isStarted = isRecording,
+            isPaused = isPaused,
             onStartClick = onRecordingStartClick,
             onStopClick = onRecordingStopClick,
+            onPauseClick = onRecordingPauseClick,
+            onResumeClick = onRecordingResumeClick,
         )
     }
 }
@@ -133,10 +141,14 @@
     modifier: Modifier = Modifier,
     enabled: Boolean,
     isStarted: Boolean,
+    isPaused: Boolean = false,
     onStartClick: () -> Unit = {},
     onStopClick: () -> Unit = {},
+    onPauseClick: () -> Unit = {},
+    onResumeClick: () -> Unit = {},
 ) {
-    val icon = painterResource(if (isStarted) R.drawable.ic_stop else R.drawable.ic_record)
+    val startStopIconRes = if (isStarted) R.drawable.ic_stop else R.drawable.ic_record
+    val pauseResumeIconRes = if (isPaused) R.drawable.ic_record else R.drawable.ic_pause
 
     Row(modifier = modifier.fillMaxSize()) {
         Box(
@@ -148,10 +160,31 @@
                 onClick = if (isStarted) onStopClick else onStartClick,
                 backgroundColor = Color.Cyan
             ) {
-                Icon(icon, stringResource(R.string.desc_recording_control), modifier.size(16.dp))
+                Icon(
+                    painterResource(startStopIconRes),
+                    stringResource(R.string.desc_recording_control),
+                    modifier.size(16.dp)
+                )
             }
         }
-        Spacer(modifier = modifier.weight(1f).fillMaxHeight())
+        Box(
+            modifier = modifier.weight(1f).fillMaxHeight(),
+            contentAlignment = Alignment.Center,
+        ) {
+            if (enabled && isStarted) {
+                AdvancedFloatingActionButton(
+                    enabled = true,
+                    onClick = if (isPaused) onResumeClick else onPauseClick,
+                    backgroundColor = Color.Cyan
+                ) {
+                    Icon(
+                        painterResource(pauseResumeIconRes),
+                        stringResource(R.string.desc_pause_control),
+                        modifier.size(16.dp)
+                    )
+                }
+            }
+        }
     }
 }
 
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
index a8cad0d..9931e68 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -66,6 +66,8 @@
         private set
     var isRecording: Boolean by mutableStateOf(false)
         private set
+    var isPaused: Boolean by mutableStateOf(false)
+        private set
 
     suspend fun initialRecorder(context: Context, lifecycleOwner: LifecycleOwner) {
         withContext(Dispatchers.Main) {
@@ -145,6 +147,23 @@
 
         cameraHelper.stopRecording()
         isRecording = false
+        isPaused = false
+    }
+
+    fun pauseRecording() {
+        Logger.d(TAG, "Pause recording.")
+        Preconditions.checkState(isRecorderReady)
+
+        cameraHelper.pauseRecording()
+        isPaused = true
+    }
+
+    fun resumeRecording() {
+        Logger.d(TAG, "Resume recording.")
+        Preconditions.checkState(isRecorderReady)
+
+        cameraHelper.resumeRecording()
+        isPaused = false
     }
 
     private fun saveOriginalVolume() {
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt
index daca14e..161095e 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/CameraHelper.kt
@@ -104,6 +104,14 @@
         activeRecording = null
     }
 
+    fun pauseRecording() {
+        activeRecording!!.pause()
+    }
+
+    fun resumeRecording() {
+        activeRecording!!.resume()
+    }
+
     private fun generateVideoFileOutputOptions(): FileOutputOptions {
         val videoFileName = "${generateVideoFileName()}.mp4"
         val videoFolder = Environment.getExternalStoragePublicDirectory(
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_pause.xml b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_pause.xml
new file mode 100644
index 0000000..f701d6f
--- /dev/null
+++ b/camera/integration-tests/avsynctestapp/src/main/res/drawable/ic_pause.xml
@@ -0,0 +1,5 @@
+<vector android:height="24dp" android:tint="#FFFFFF"
+    android:viewportHeight="24" android:viewportWidth="24"
+    android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="@android:color/white" android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
+</vector>
diff --git a/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml b/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
index dca7f0e..def9e0f 100644
--- a/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/avsynctestapp/src/main/res/values/strings.xml
@@ -18,4 +18,5 @@
     <string name="app_name">AV sync test app</string>
     <string name="desc_signal_control">Signal Control</string>
     <string name="desc_recording_control">Recording Control</string>
+    <string name="desc_pause_control">Pause Control</string>
 </resources>
\ No newline at end of file
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index eb2773a..453701b 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -87,6 +87,8 @@
     // Testing resource dependency for manifest
     debugImplementation(project(":camera:camera-testing"))
     debugImplementation(libs.testCore)
+    // explicitly add runner here to force consistency with androidTestImplementation
+    debugImplementation(libs.testRunner)
 
     // Testing framework
     androidTestImplementation(libs.testCore)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
index ff9cca4..2a55cbd 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/BindUnbindUseCasesStressTest.kt
@@ -22,14 +22,16 @@
 import android.os.HandlerThread
 import android.util.Log
 import android.util.Size
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageProxy
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
@@ -38,6 +40,7 @@
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.GLUtil
 import androidx.camera.testing.LabTestRule
@@ -81,11 +84,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class BindUnbindUseCasesStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -135,6 +145,8 @@
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -164,9 +176,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @LabTestRule.LabTestOnly
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 45f998f..1781ca6 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -64,6 +64,7 @@
  */
 internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
     val idlingResource = withActivity {
+        cleanTakePictureErrorMessage()
         imageSavedIdlingResource
     }
     try {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
index 52c6853..0fa1f88 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCameraStressTest.kt
@@ -17,17 +17,22 @@
 package androidx.camera.integration.core
 
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import android.hardware.camera2.CameraDevice
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
-import androidx.camera.core.CameraState
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.LabTestRule
@@ -36,7 +41,6 @@
 import androidx.camera.testing.fakes.FakeLifecycleOwner
 import androidx.camera.video.Recorder
 import androidx.camera.video.VideoCapture
-import androidx.lifecycle.Observer
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
@@ -60,11 +64,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class OpenCloseCameraStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
+        PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -81,9 +92,14 @@
     private lateinit var preview: Preview
     private lateinit var imageCapture: ImageCapture
     private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private val cameraDeviceStateMonitor = CameraDeviceStateMonitor()
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Skips CameraPipe part now and will open this when camera-pipe-integration can support
+        assumeTrue(implName != CameraPipeConfig::class.simpleName)
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -94,7 +110,7 @@
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraIdCameraSelector)
         }
 
-        preview = Preview.Builder().build()
+        preview = createPreviewWithDeviceStateMonitor(implName, cameraDeviceStateMonitor)
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
         }
@@ -116,16 +132,19 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @LabTestRule.LabTestOnly
     @Test
     @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
     fun openCloseCameraStressTest_withPreviewImageCapture(): Unit = runBlocking {
-        bindUseCase_unbindAll_toCheckCameraState_repeatedly(preview, imageCapture)
+        bindUseCase_unbindAll_toCheckCameraState_repeatedly(
+            preview,
+            imageCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
+        )
     }
 
     @LabTestRule.LabTestOnly
@@ -137,7 +156,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             imageCapture,
-            imageAnalysis = imageAnalysis
+            imageAnalysis = imageAnalysis,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -146,7 +166,11 @@
     @RepeatRule.Repeat(times = STRESS_TEST_REPEAT_COUNT)
     fun openCloseCameraStressTest_withPreviewVideoCapture(): Unit = runBlocking {
         val videoCapture = VideoCapture.withOutput(Recorder.Builder().build())
-        bindUseCase_unbindAll_toCheckCameraState_repeatedly(preview, videoCapture = videoCapture)
+        bindUseCase_unbindAll_toCheckCameraState_repeatedly(
+            preview,
+            videoCapture = videoCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
+        )
     }
 
     @LabTestRule.LabTestOnly
@@ -158,7 +182,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
-            imageCapture = imageCapture
+            imageCapture = imageCapture,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -172,7 +197,8 @@
         bindUseCase_unbindAll_toCheckCameraState_repeatedly(
             preview,
             videoCapture = videoCapture,
-            imageAnalysis = imageAnalysis
+            imageAnalysis = imageAnalysis,
+            cameraDeviceStateMonitor = cameraDeviceStateMonitor
         )
     }
 
@@ -188,23 +214,13 @@
         imageCapture: ImageCapture? = null,
         videoCapture: VideoCapture<Recorder>? = null,
         imageAnalysis: ImageAnalysis? = null,
+        cameraDeviceStateMonitor: CameraDeviceStateMonitor,
         repeatCount: Int = STRESS_TEST_OPERATION_REPEAT_COUNT
     ): Unit = runBlocking {
         for (i in 1..repeatCount) {
-            val openCameraLatch = CountDownLatch(1)
-            val closeCameraLatch = CountDownLatch(1)
-            val observer = Observer<CameraState> { state ->
-                if (state.type == CameraState.Type.OPEN) {
-                    openCameraLatch.countDown()
-                } else if (state.type == CameraState.Type.CLOSED) {
-                    closeCameraLatch.countDown()
-                }
-            }
+            cameraDeviceStateMonitor.reset()
 
             withContext(Dispatchers.Main) {
-                // Arrange: sets up CameraState observer
-                camera.cameraInfo.cameraState.observe(lifecycleOwner, observer)
-
                 // VideoCapture needs to be recreated everytime until b/212654991 is fixed
                 var newVideoCapture: VideoCapture<Recorder>? = null
                 videoCapture?.let {
@@ -224,21 +240,68 @@
                 )
             }
 
-            // Assert: checks the CameraState.Type.OPEN can be received
-            assertThat(openCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+            // Assert: checks the CameraDevice opened event can be received
+            cameraDeviceStateMonitor.awaitCameraOpenedAndAssert()
 
             // Act: unbinds all use cases
             withContext(Dispatchers.Main) {
                 cameraProvider.unbindAll()
             }
 
-            // Assert: checks the CameraState.Type.CLOSED can be received
-            assertThat(closeCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+            // Assert: checks the CameraDevice closed event can be received
+            cameraDeviceStateMonitor.awaitCameraClosedAndAssert()
+        }
+    }
 
-            // Clean it up.
-            withContext(Dispatchers.Main) {
-                camera.cameraInfo.cameraState.removeObserver(observer)
+    @OptIn(ExperimentalCamera2Interop::class)
+    private fun createPreviewWithDeviceStateMonitor(
+        implementationName: String,
+        cameraDeviceStateMonitor: CameraDeviceStateMonitor
+    ): Preview {
+        val builder = Preview.Builder()
+
+        when (implementationName) {
+            CameraPipeConfig::class.simpleName -> {
+                androidx.camera.camera2.pipe.integration.interop.Camera2Interop.Extender(builder)
+                    .setDeviceStateCallback(cameraDeviceStateMonitor)
             }
+            else -> Camera2Interop.Extender(builder)
+                .setDeviceStateCallback(cameraDeviceStateMonitor)
+        }
+
+        return builder.build()
+    }
+
+    private class CameraDeviceStateMonitor : CameraDevice.StateCallback() {
+        private var openCameraLatch = CountDownLatch(1)
+        private var closeCameraLatch = CountDownLatch(1)
+        override fun onOpened(p0: CameraDevice) {
+            openCameraLatch.countDown()
+        }
+
+        override fun onClosed(camera: CameraDevice) {
+            closeCameraLatch.countDown()
+        }
+
+        override fun onDisconnected(p0: CameraDevice) {
+            // No op.
+        }
+
+        override fun onError(p0: CameraDevice, p1: Int) {
+            // No op.
+        }
+
+        fun reset() {
+            openCameraLatch = CountDownLatch(1)
+            closeCameraLatch = CountDownLatch(1)
+        }
+
+        fun awaitCameraOpenedAndAssert() {
+            assertThat(openCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
+        }
+
+        fun awaitCameraClosedAndAssert() {
+            assertThat(closeCameraLatch.await(3000, TimeUnit.MILLISECONDS)).isTrue()
         }
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
index a6dfbca..4a33164 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/OpenCloseCaptureSessionStressTest.kt
@@ -17,20 +17,23 @@
 package androidx.camera.integration.core
 
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.impl.Camera2ImplConfig
-import androidx.camera.camera2.impl.CameraEventCallback
-import androidx.camera.camera2.impl.CameraEventCallbacks
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCaptureSession.StateCallback
+import androidx.camera.camera2.interop.Camera2Interop
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
-import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.LabTestRule
 import androidx.camera.testing.StressTestRule
@@ -61,11 +64,18 @@
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
 class OpenCloseCaptureSessionStressTest(
-    private val cameraId: String
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
 ) {
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -82,10 +92,14 @@
     private lateinit var preview: Preview
     private lateinit var imageCapture: ImageCapture
     private lateinit var lifecycleOwner: FakeLifecycleOwner
-    private val cameraEventMonitor = CameraEventMonitor()
+    private val sessionStateMonitor = CameraCaptureSessionStateMonitor()
 
     @Before
     fun setUp(): Unit = runBlocking {
+        // Skips CameraPipe part now and will open this when camera-pipe-integration can support
+        assumeTrue(implName != CameraPipeConfig::class.simpleName)
+        // Configures the test target config
+        ProcessCameraProvider.configureInstance(cameraConfig)
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -96,9 +110,10 @@
             cameraProvider.bindToLifecycle(lifecycleOwner, cameraIdCameraSelector)
         }
 
-        // Creates the Preview with the CameraEventMonitor to monitor whether the event callbacks
-        // are called.
-        preview = createPreviewWithCameraEventMonitor(cameraEventMonitor)
+        // Creates the Preview with the CameraCaptureSessionStateMonitor to monitor whether the
+        // event callbacks are called.
+        preview = createPreviewWithSessionStateMonitor(implName, sessionStateMonitor)
+
         withContext(Dispatchers.Main) {
             preview.setSurfaceProvider(SurfaceTextureProvider.createSurfaceTextureProvider())
         }
@@ -193,7 +208,7 @@
     ): Unit = runBlocking {
         for (i in 1..repeatCount) {
             // Arrange: resets the camera event monitor
-            cameraEventMonitor.reset()
+            sessionStateMonitor.reset()
 
             withContext(Dispatchers.Main) {
                 // VideoCapture needs to be recreated everytime until b/212654991 is fixed
@@ -215,16 +230,13 @@
                 )
             }
 
-            // Assert: checks the CameraEvent#onEnableSession callback function is called
-            cameraEventMonitor.awaitSessionEnabledAndAssert()
+            // Assert: checks the capture session opened callback function is called
+            sessionStateMonitor.awaitSessionConfiguredAndAssert()
 
             // Act: unbinds all use cases
             withContext(Dispatchers.Main) {
                 cameraProvider.unbindAll()
             }
-
-            // Assert: checks the CameraEvent#onSessionDisabled callback function is called
-            cameraEventMonitor.awaitSessionDisabledAndAssert()
         }
     }
 
@@ -233,51 +245,49 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
-    private fun createPreviewWithCameraEventMonitor(
-        cameraEventMonitor: CameraEventMonitor
+    @OptIn(ExperimentalCamera2Interop::class)
+    private fun createPreviewWithSessionStateMonitor(
+        implementationName: String,
+        sessionStateMonitor: CameraCaptureSessionStateMonitor
     ): Preview {
         val builder = Preview.Builder()
 
-        Camera2ImplConfig.Extender(builder)
-            .setCameraEventCallback(CameraEventCallbacks(cameraEventMonitor))
+        when (implementationName) {
+            CameraPipeConfig::class.simpleName -> {
+                androidx.camera.camera2.pipe.integration.interop.Camera2Interop.Extender(
+                    builder
+                ).setSessionStateCallback(sessionStateMonitor)
+            }
+            else -> Camera2Interop.Extender(builder).setSessionStateCallback(sessionStateMonitor)
+        }
 
         return builder.build()
     }
 
     /**
-     * An implementation of CameraEventCallback to monitor whether the camera event callbacks are
-     * called properly or not.
+     * An implementation of CameraCaptureSession.StateCallback to monitor whether the event
+     * callbacks are called properly or not.
      */
-    private class CameraEventMonitor : CameraEventCallback() {
-        private var sessionEnabledLatch = CountDownLatch(1)
-        private var sessionDisabledLatch = CountDownLatch(1)
-
-        override fun onEnableSession(): CaptureConfig? {
-            sessionEnabledLatch.countDown()
-            return null
+    private class CameraCaptureSessionStateMonitor : StateCallback() {
+        private var sessionConfiguredLatch = CountDownLatch(1)
+        override fun onConfigured(session: CameraCaptureSession) {
+            sessionConfiguredLatch.countDown()
         }
 
-        override fun onDisableSession(): CaptureConfig? {
-            sessionDisabledLatch.countDown()
-            return null
+        override fun onConfigureFailed(session: CameraCaptureSession) {
+            throw RuntimeException("Capture session configures failed!")
         }
 
         fun reset() {
-            sessionEnabledLatch = CountDownLatch(1)
-            sessionDisabledLatch = CountDownLatch(1)
+            sessionConfiguredLatch = CountDownLatch(1)
         }
 
-        fun awaitSessionEnabledAndAssert() {
-            assertThat(sessionEnabledLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
-        }
-
-        fun awaitSessionDisabledAndAssert() {
-            assertThat(sessionDisabledLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
+        fun awaitSessionConfiguredAndAssert() {
+            assertThat(sessionConfiguredLatch.await(15000, TimeUnit.MILLISECONDS)).isTrue()
         }
     }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
index 70e5598..1ceec71 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/TakePictureTest.kt
@@ -25,6 +25,7 @@
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.testing.waitForIdle
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.Espresso.onView
@@ -33,6 +34,8 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertWithMessage
 import java.util.concurrent.TimeUnit
 import org.junit.After
 import org.junit.Assume.assumeTrue
@@ -137,4 +140,35 @@
             }
         }
     }
+
+    @Test
+    fun testTakePictureQuickly() {
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) {
+            use { // Ensure ActivityScenario is cleaned up properly.
+
+                // Arrange, wait for camera starts processing output.
+                waitForViewfinderIdle()
+
+                // Act. continuously take 5 photos.
+                withActivity {
+                    cleanTakePictureErrorMessage()
+                    imageSavedIdlingResource
+                }.apply {
+                    for (i in 5 downTo 1) {
+                        onView(withId(R.id.Picture)).perform(click())
+                    }
+                    waitForIdle()
+                }
+
+                // Assert, there's no error message.
+                withActivity {
+                    deleteSessionImages()
+                    lastTakePictureErrorMessage ?: ""
+                }.let { errorMessage ->
+                    assertWithMessage("Fail to take picture: $errorMessage").that(errorMessage)
+                        .isEmpty()
+                }
+            }
+        }
+    }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
index e37c5f1..c03d388 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageAnalysisLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class ImageAnalysisLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
index f0996e9..6d2ab58 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageAnalysisSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageAnalysisSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class ImageAnalysisSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
index 8e35e34..2682fa5 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class ImageCaptureLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
index befb664..a447c7f 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureStressTest.kt
@@ -18,7 +18,8 @@
 
 import android.Manifest
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
@@ -28,6 +29,7 @@
 import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -58,12 +60,21 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureStressTest(val cameraId: String) {
+class ImageCaptureStressTest(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
+    val cameraId: String
+) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -78,15 +89,17 @@
     @get:Rule
     val repeatRule = RepeatRule()
 
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private lateinit var cameraProvider: ProcessCameraProvider
+
     companion object {
         @ClassRule
         @JvmField
         val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -94,6 +107,17 @@
         Assume.assumeTrue(CameraUtil.deviceHasCamera())
         CoreAppTestUtil.assumeCompatibleDevice()
         CoreAppTestUtil.assumeNotUntestableFrontCamera(cameraId)
+
+        // For running the ImageCaptureStressTest, we need to get the target test camera to check
+        // whether the testing use case combination can be supported to skip unsupported cases. For
+        // the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
+
         // Clear the device UI and check if there is no dialog or lock screen on the top of the
         // window before start the test.
         CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
@@ -106,17 +130,17 @@
 
     @After
     fun tearDown(): Unit = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.shutdown()[10000, TimeUnit.MILLISECONDS]
+            }
+        }
+
         // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
         // to other tests :)
         device.unfreezeRotation()
         device.pressHome()
         device.waitForIdle(StressTestUtil.HOME_TIMEOUT_MS)
-
-        withContext(Dispatchers.Main) {
-            val context = ApplicationProvider.getApplicationContext<Context>()
-            val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
-            cameraProvider.shutdown()[10, TimeUnit.SECONDS]
-        }
     }
 
     @LabTestRule.LabTestOnly
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
index c446e77..0837894 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/ImageCaptureSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,9 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class ImageCaptureSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
-
+class ImageCaptureSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
     @LabTestRule.LabTestOnly
     @Test
     @RepeatRule.Repeat(times = LARGE_STRESS_TEST_REPEAT_COUNT)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
index cfc9c26..3c59a34 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/LifecycleStatusChangeStressTestBase.kt
@@ -18,9 +18,10 @@
 
 import android.Manifest
 import android.content.Context
-import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.recordVideoAndWaitForVideoSavedIdle
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
 import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
@@ -30,10 +31,12 @@
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_PREVIEW
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_VIDEO_CAPTURE
 import androidx.camera.integration.core.util.StressTestUtil.createCameraSelectorById
+import androidx.camera.integration.core.util.StressTestUtil.getAllCameraXConfigCameraIdCombinations
 import androidx.camera.integration.core.util.StressTestUtil.launchCameraXActivityAndWaitForPreviewReady
 import androidx.camera.integration.core.waitForImageAnalysisIdle
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -57,13 +60,20 @@
 import org.junit.runners.Parameterized
 
 abstract class LifecycleStatusChangeStressTestBase(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
     val cameraId: String
 ) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -90,9 +100,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -109,6 +118,15 @@
         // explicitly initiated from within the test.
         device.setOrientationNatural()
 
+        // For running the LifecycleStatusChangeStressTest, we need to get the target test camera
+        // to check whether the testing use case combination can be supported to skip unsupported
+        // cases. For the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
index 04e4fe4..464e84d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class PreviewLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
index ee61902..30fc762 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/PreviewSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -33,8 +34,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class PreviewSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
index bbb9455..f710cab8 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/SwitchCameraStressTestBase.kt
@@ -19,13 +19,15 @@
 import android.Manifest
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
-import androidx.camera.camera2.Camera2Config
-import androidx.camera.camera2.interop.Camera2CameraInfo
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.integration.core.recordVideoAndWaitForVideoSavedIdle
 import androidx.camera.integration.core.switchCameraAndWaitForViewfinderIdle
 import androidx.camera.integration.core.takePictureAndWaitForImageSavedIdle
+import androidx.camera.integration.core.util.StressTestUtil
 import androidx.camera.integration.core.util.StressTestUtil.HOME_TIMEOUT_MS
 import androidx.camera.integration.core.util.StressTestUtil.STRESS_TEST_OPERATION_REPEAT_COUNT
 import androidx.camera.integration.core.util.StressTestUtil.VERIFICATION_TARGET_IMAGE_ANALYSIS
@@ -38,6 +40,7 @@
 import androidx.camera.integration.core.waitForImageAnalysisIdle
 import androidx.camera.integration.core.waitForViewfinderIdle
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.LabTestRule
@@ -60,13 +63,20 @@
 import org.junit.runners.Parameterized
 
 abstract class SwitchCameraStressTestBase(
+    val implName: String,
+    val cameraConfig: CameraXConfig,
     val cameraId: String
 ) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
     val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
-        CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
+        CameraUtil.PreTestCameraIdList(cameraConfig)
     )
 
     @get:Rule
@@ -93,9 +103,8 @@
         @JvmField val stressTest = StressTestRule()
 
         @JvmStatic
-        @get:Parameterized.Parameters(name = "cameraId = {0}")
-        val parameters: Collection<String>
-            get() = CameraUtil.getBackwardCompatibleCameraIdListOrThrow()
+        @Parameterized.Parameters(name = "config = {0}, cameraId = {2}")
+        fun data() = StressTestUtil.getAllCameraXConfigCameraIdCombinations()
     }
 
     @Before
@@ -112,6 +121,15 @@
         // explicitly initiated from within the test.
         device.setOrientationNatural()
 
+        // For running the LifecycleStatusChangeStressTest, we need to get the target test camera
+        // to check whether the testing use case combination can be supported to skip unsupported
+        // cases. For the purpose, we force configure the target testing config first
+        // (Camera2Config/CameraPipeConfig) and gets the CameraProvider instance in the setup()
+        // function. Then, the activity launched afterward will also run on the same config
+        // environment. The setup config environment will be cleared after
+        // CameraProvider#shutdown() is called in the tearDown() function.
+        ProcessCameraProvider.configureInstance(cameraConfig)
+
         cameraProvider = ProcessCameraProvider.getInstance(context)[10000, TimeUnit.MILLISECONDS]
 
         cameraIdCameraSelector = createCameraSelectorById(cameraId)
@@ -233,9 +251,7 @@
         // Checks whether the input camera can support the use case combination
         assumeCameraSupportUseCaseCombination(camera, useCaseCombination)
 
-        val camera2CameraInfo = Camera2CameraInfo.from(camera.cameraInfo)
-        val lensFacing =
-            camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.LENS_FACING)
+        val lensFacing = (camera.cameraInfo as CameraInfoInternal).lensFacing
 
         val otherLensFacingCameraSelector =
             if (lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
index cd215e3..13defec 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureLifecycleStatusChangeStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -34,8 +35,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class VideoCaptureLifecycleStatusChangeStressTest constructor(cameraId: String) :
-    LifecycleStatusChangeStressTestBase(cameraId) {
+class VideoCaptureLifecycleStatusChangeStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : LifecycleStatusChangeStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
index 39e7011..7d8630d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/stresstest/VideoCaptureSwitchCameraStressTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.core.stresstest
 
+import androidx.camera.core.CameraXConfig
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
 import androidx.camera.integration.core.CameraXActivity.BIND_PREVIEW
@@ -33,8 +34,11 @@
 @LargeTest
 @RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class VideoCaptureSwitchCameraStressTest constructor(cameraId: String) :
-    SwitchCameraStressTestBase(cameraId) {
+class VideoCaptureSwitchCameraStressTest constructor(
+    implName: String,
+    cameraConfig: CameraXConfig,
+    cameraId: String
+) : SwitchCameraStressTestBase(implName, cameraConfig, cameraId) {
 
     @LabTestRule.LabTestOnly
     @Test
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
index da6f867..857db58 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/util/StressTestUtil.kt
@@ -18,9 +18,8 @@
 
 import android.content.Context
 import android.content.Intent
-import androidx.annotation.OptIn
-import androidx.camera.camera2.interop.Camera2CameraInfo
-import androidx.camera.camera2.interop.ExperimentalCamera2Interop
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraFilter
 import androidx.camera.core.CameraInfo
@@ -28,6 +27,7 @@
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
+import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.integration.core.CameraXActivity
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_ANALYSIS
 import androidx.camera.integration.core.CameraXActivity.BIND_IMAGE_CAPTURE
@@ -36,6 +36,7 @@
 import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_CAMERA_ID
 import androidx.camera.integration.core.CameraXActivity.INTENT_EXTRA_USE_CASE_COMBINATION
 import androidx.camera.integration.core.waitForViewfinderIdle
+import androidx.camera.testing.CameraUtil
 import androidx.camera.video.Recorder
 import androidx.camera.video.VideoCapture
 import androidx.test.core.app.ActivityScenario
@@ -80,9 +81,7 @@
 
         activityScenario.onActivity {
             // Checks that the camera id is correct
-            val camera2CameraInfo = Camera2CameraInfo.from(it.camera!!.cameraInfo)
-
-            if (camera2CameraInfo.cameraId != cameraId) {
+            if ((it.camera!!.cameraInfo as CameraInfoInternal).cameraId != cameraId) {
                 it.finish()
                 throw IllegalArgumentException("The activity is not launched with the correct" +
                     " camera of expected id.")
@@ -131,11 +130,10 @@
     }
 
     @JvmStatic
-    @OptIn(ExperimentalCamera2Interop::class)
     fun createCameraSelectorById(cameraId: String) =
         CameraSelector.Builder().addCameraFilter(CameraFilter { cameraInfos ->
             cameraInfos.forEach {
-                if (Camera2CameraInfo.from(it).cameraId.equals(cameraId)) {
+                if ((it as CameraInfoInternal).cameraId == cameraId) {
                     return@CameraFilter listOf<CameraInfo>(it)
                 }
             }
@@ -143,6 +141,30 @@
             throw IllegalArgumentException("No camera can be find for id: $cameraId")
         }).build()
 
+    @JvmStatic
+    fun getAllCameraXConfigCameraIdCombinations() = mutableListOf<Array<Any?>>().apply {
+        val cameraxConfigs =
+            listOf(Camera2Config::class.simpleName, CameraPipeConfig::class.simpleName)
+
+        cameraxConfigs.forEach { configImplName ->
+            CameraUtil.getBackwardCompatibleCameraIdListOrThrow().forEach { cameraId ->
+                add(
+                    arrayOf(
+                        configImplName,
+                        when (configImplName) {
+                            CameraPipeConfig::class.simpleName ->
+                                CameraPipeConfig.defaultConfig()
+                            Camera2Config::class.simpleName ->
+                                Camera2Config.defaultConfig()
+                            else -> Camera2Config.defaultConfig()
+                        },
+                        cameraId
+                    )
+                )
+            }
+        }
+    }
+
     /**
      * Large stress test repeat count to run the test
      */
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index 24257c5..d78d107 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -737,8 +737,6 @@
                     @Override
                     public void onClick(View view) {
                         mImageSavedIdlingResource.increment();
-                        mLastTakePictureErrorMessage = null;
-
                         mStartCaptureTime = SystemClock.elapsedRealtime();
                         createDefaultPictureFolderIfNotExist();
                         ContentValues contentValues = new ContentValues();
@@ -1276,7 +1274,6 @@
      *
      * @param calledBySelf flag indicates if this is a recursive call.
      */
-    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     void tryBindUseCases(boolean calledBySelf) {
         boolean isViewFinderReady = mViewFinder.getWidth() != 0 && mViewFinder.getHeight() != 0;
         boolean isCameraReady = mCameraProvider != null;
@@ -1312,10 +1309,7 @@
             // camera id.
             if (mCurrentCameraSelector == mLaunchingCameraIdSelector
                     && mLaunchingCameraLensFacing == UNKNOWN_LENS_FACING) {
-                Camera2CameraInfo camera2CameraInfo =
-                        Camera2CameraInfo.from(mCamera.getCameraInfo());
-                mLaunchingCameraLensFacing = camera2CameraInfo.getCameraCharacteristic(
-                        CameraCharacteristics.LENS_FACING);
+                mLaunchingCameraLensFacing = getLensFacing(mCamera.getCameraInfo());
             }
             List<UseCase> useCases = buildUseCases();
             mCamera = bindToLifecycleSafely(useCases);
@@ -1743,7 +1737,11 @@
             synchronized (mSessionMediaUris) {
                 Iterator<Uri> it = mSessionMediaUris.iterator();
                 while (it.hasNext()) {
-                    getContentResolver().delete(it.next(), null, null);
+                    try {
+                        getContentResolver().delete(it.next(), null, null);
+                    } catch (SecurityException e) {
+                        Log.w(TAG, "Cannot delete the content.", e);
+                    }
                     it.remove();
                 }
             }
@@ -1876,6 +1874,11 @@
         return mLastTakePictureErrorMessage;
     }
 
+    @VisibleForTesting
+    void cleanTakePictureErrorMessage() {
+        mLastTakePictureErrorMessage = null;
+    }
+
     @SuppressWarnings("unchecked")
     VideoCapture<Recorder> getVideoCapture() {
         return findUseCase(VideoCapture.class);
@@ -1989,10 +1992,9 @@
         return new CameraSelector.Builder().addCameraFilter(new CameraFilter() {
             @NonNull
             @Override
-            @OptIn(markerClass = ExperimentalCamera2Interop.class)
             public List<CameraInfo> filter(@NonNull List<CameraInfo> cameraInfos) {
                 for (CameraInfo cameraInfo : cameraInfos) {
-                    if (cameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
+                    if (Objects.equals(cameraId, getCameraId(cameraInfo))) {
                         return Collections.singletonList(cameraInfo);
                     }
                 }
@@ -2001,4 +2003,53 @@
             }
         }).build();
     }
+
+    private static int getLensFacing(@NonNull CameraInfo cameraInfo) {
+        try {
+            return getCamera2LensFacing(cameraInfo);
+        } catch (IllegalArgumentException e) {
+            return getCamera2PipeLensFacing(cameraInfo);
+        }
+    }
+
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
+    private static int getCamera2LensFacing(@NonNull CameraInfo cameraInfo) {
+        Integer lensFacing = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
+                    CameraCharacteristics.LENS_FACING);
+
+        return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
+    }
+
+    @OptIn(markerClass =
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
+    private static int getCamera2PipeLensFacing(@NonNull CameraInfo cameraInfo) {
+        Integer lensFacing =
+                androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
+                        cameraInfo).getCameraCharacteristic(CameraCharacteristics.LENS_FACING);
+
+        return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
+    }
+
+    @NonNull
+    private static String getCameraId(@NonNull CameraInfo cameraInfo) {
+        try {
+            return getCamera2CameraId(cameraInfo);
+        } catch (IllegalArgumentException e) {
+            return getCameraPipeCameraId(cameraInfo);
+        }
+    }
+
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
+    @NonNull
+    private static String getCamera2CameraId(@NonNull CameraInfo cameraInfo) {
+        return Camera2CameraInfo.from(cameraInfo).getCameraId();
+    }
+
+    @OptIn(markerClass =
+            androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
+    @NonNull
+    private static String getCameraPipeCameraId(@NonNull CameraInfo cameraInfo) {
+        return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
+                cameraInfo).getCameraId();
+    }
 }
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index cab06b9..6eec96a 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,15 +32,16 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoRecordEvent
 import androidx.camera.view.CameraController
 import androidx.camera.view.CameraController.IMAGE_CAPTURE
 import androidx.camera.view.CameraController.VIDEO_CAPTURE
 import androidx.camera.view.LifecycleCameraController
 import androidx.camera.view.PreviewView
+import androidx.camera.view.video.AudioConfig
 import androidx.camera.view.video.ExperimentalVideo
-import androidx.camera.view.video.OnVideoSavedCallback
-import androidx.camera.view.video.OutputFileOptions
-import androidx.camera.view.video.OutputFileResults
 import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import java.text.SimpleDateFormat
@@ -62,10 +63,11 @@
 import kotlinx.coroutines.withContext
 
 @OptIn(ExperimentalVideo::class)
-@SuppressLint("NullAnnotationGroup")
+@SuppressLint("NullAnnotationGroup", "MissingPermission")
 class MainActivity : AppCompatActivity() {
 
     private lateinit var cameraController: LifecycleCameraController
+    private lateinit var activeRecording: Recording
     private lateinit var previewView: PreviewView
     private lateinit var overlayView: OverlayView
     private lateinit var executor: Executor
@@ -185,7 +187,7 @@
         videoCaptureBtn.setOnClickListener {
             // determine whether the onclick is to start recording or stop recording
             if (cameraController.isRecording) {
-                cameraController.stopRecording()
+                activeRecording.stop()
                 videoCaptureBtn.setText(R.string.start_video_capture)
                 val msg = "video stopped recording"
                 showToast(msg)
@@ -200,35 +202,29 @@
                         put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
                     }
                 }
-                val outputFileOptions = OutputFileOptions
-                    .builder(
-                        contentResolver,
-                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
-                        contentValues
-                    )
+                val outputOptions = MediaStoreOutputOptions
+                    .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+                    .setContentValues(contentValues)
                     .build()
                 Log.d(TAG, "finished composing video name")
 
+                val audioConfig = AudioConfig.create(true)
+
                 // start recording
                 try {
-                    cameraController.startRecording(
-                        outputFileOptions,
-                        executor,
-                        object : OnVideoSavedCallback {
-                            override fun onVideoSaved(outputFileResults: OutputFileResults) {
-                                val msg = "Video record succeeded: " + outputFileResults.savedUri
+                    activeRecording = cameraController.startRecording(
+                        outputOptions, audioConfig, executor
+                    ) { event ->
+                        if (event is VideoRecordEvent.Finalize) {
+                            val uri = event.outputResults.outputUri
+                            if (event.error == VideoRecordEvent.Finalize.ERROR_NONE) {
+                                val msg = "Video record succeeded: $uri"
                                 showToast(msg)
-                            }
-
-                            override fun onError(
-                                videoCaptureError: Int,
-                                message: String,
-                                cause: Throwable?
-                            ) {
-                                Log.e(TAG, "Video saving failed: $message")
+                            } else {
+                                Log.e(TAG, "Video saving failed: ${event.cause}")
                             }
                         }
-                    )
+                    }
                     videoCaptureBtn.setText(R.string.stop_video_capture)
                     val msg = "video recording"
                     showToast(msg)
diff --git a/camera/integration-tests/viewtestapp/build.gradle b/camera/integration-tests/viewtestapp/build.gradle
index 75139ec..794f5d8 100644
--- a/camera/integration-tests/viewtestapp/build.gradle
+++ b/camera/integration-tests/viewtestapp/build.gradle
@@ -63,6 +63,7 @@
     implementation(project(":camera:camera-mlkit-vision"))
     implementation("androidx.lifecycle:lifecycle-runtime:2.3.1")
     implementation(project(":camera:camera-view"))
+    implementation(project(":camera:camera-video"))
     implementation(libs.guavaAndroid)
     implementation('com.google.mlkit:barcode-scanning:17.0.2')
     implementation("androidx.exifinterface:exifinterface:1.3.2")
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 0f90843..28e1fde 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -21,6 +21,7 @@
 import android.graphics.BitmapFactory
 import android.graphics.Matrix
 import android.graphics.PointF
+import android.media.MediaMetadataRetriever
 import android.net.Uri
 import android.os.Build
 import android.view.Surface
@@ -39,6 +40,7 @@
 import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CoreAppTestUtil
+import androidx.camera.video.VideoRecordEvent
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_FAILED
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_FOCUSED
 import androidx.camera.view.CameraController.TAP_TO_FOCUS_NOT_FOCUSED
@@ -131,7 +133,7 @@
     }
 
     @Test
-    fun enableEffect_effectIsEnabled() {
+    fun enableEffect_previewEffectIsEnabled() {
         // Arrange: launch app and verify effect is inactive.
         fragment.assertPreviewIsStreaming()
         val processor =
@@ -148,6 +150,23 @@
     }
 
     @Test
+    fun enableEffect_imageCaptureEffectIsEnabled() {
+        // Arrange: launch app and verify effect is inactive.
+        fragment.assertPreviewIsStreaming()
+        val effect = fragment.mToneMappingImageEffect as ToneMappingImageEffect
+        assertThat(effect.isInvoked()).isFalse()
+
+        // Act: turn on effect.
+        val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+        uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+        instrumentation.waitForIdleSync()
+        fragment.assertCanTakePicture()
+
+        // Assert: verify that effect is active.
+        assertThat(effect.isInvoked()).isTrue()
+    }
+
+    @Test
     fun controllerBound_canGetCameraControl() {
         fragment.assertPreviewIsStreaming()
         instrumentation.runOnMainSync {
@@ -443,6 +462,98 @@
         fragment.assertCanTakePicture()
     }
 
+    @Test
+    fun fragmentLaunched_cannotRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        val exception = Assert.assertThrows(IllegalStateException::class.java) {
+            fragment.assertCanRecordVideo()
+        }
+        assertThat(exception).hasMessageThat().isEqualTo("VideoCapture disabled.")
+    }
+
+    @Test
+    fun recordEnabled_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    @Test
+    fun cameraToggled_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        fragment.assertPreviewIsStreaming()
+        onView(withId(R.id.camera_toggle)).perform(click())
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    @Test
+    fun recordDisabledAndEnabledMultipleTimes_canRecordVideo() {
+        skipVideoRecordingTestOnCuttlefishApi29()
+        skipTestWithSurfaceProcessingOnCuttlefishApi30()
+
+        // Arrange.
+        val times = 10
+        fragment.assertPreviewIsStreaming()
+
+        // Act.
+        invertAllUseCaseEnableStatusExceptPreview()
+        repeat(times) {
+            onView(withId(R.id.video_enabled)).perform(click())
+            onView(withId(R.id.video_enabled)).perform(click())
+        }
+        fragment.assertPreviewIsStreaming()
+
+        // Assert.
+        fragment.assertCanRecordVideo()
+    }
+
+    private fun invertAllUseCaseEnableStatusExceptPreview() {
+        onView(withId(R.id.capture_enabled)).perform(click())
+        onView(withId(R.id.analysis_enabled)).perform(click())
+        onView(withId(R.id.video_enabled)).perform(click())
+    }
+
+    private fun skipVideoRecordingTestOnCuttlefishApi29() {
+        // Skip test for b/168175357
+        Assume.assumeFalse(
+            "Cuttlefish has MediaCodec dequeInput/Output buffer fails issue. Unable to test.",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 29
+        )
+    }
+
+    private fun skipTestWithSurfaceProcessingOnCuttlefishApi30() {
+        // Skip test for b/253211491
+        Assume.assumeFalse(
+            "Skip tests for Cuttlefish API 30 eglCreateWindowSurface issue",
+            Build.MODEL.contains("Cuttlefish") && Build.VERSION.SDK_INT == 30
+        )
+    }
+
     /**
      * Calculates the 1st order moment (center of mass) of the R, G and B of the bitmap.
      */
@@ -565,6 +676,73 @@
         )
     }
 
+    /**
+     * Records a video and assert the URI exists.
+     *
+     * <p> Also cleans up the saved video afterwards.
+     */
+    private fun CameraControllerFragment.assertCanRecordVideo() {
+        // Arrange.
+        val videoSavedSemaphore = Semaphore(0)
+        val videoRecordingSemaphore = Semaphore(0)
+        var finalize: VideoRecordEvent.Finalize? = null
+
+        // Act.
+        instrumentation.runOnMainSync {
+            this.startRecording {
+                when (it) {
+                    is VideoRecordEvent.Finalize -> {
+                        finalize = it
+                        videoSavedSemaphore.release()
+                    }
+                    is VideoRecordEvent.Status -> {
+                        videoRecordingSemaphore.release()
+                    }
+                    is VideoRecordEvent.Start,
+                    is VideoRecordEvent.Pause,
+                    is VideoRecordEvent.Resume -> {
+                        // no op for this test, skip these event now.
+                    }
+                    else -> {
+                        throw IllegalStateException()
+                    }
+                }
+            }
+        }
+
+        // Wait for status event to proceed recording for a while.
+        assertThat(
+            videoRecordingSemaphore.tryAcquire(RECORDING_COUNT, TIMEOUT_SECONDS, TimeUnit.SECONDS)
+        ).isTrue()
+
+        instrumentation.runOnMainSync {
+            this.stopRecording()
+        }
+
+        // Wait for finalize event to saved file.
+        assertThat(videoSavedSemaphore.tryAcquire(TIMEOUT_SECONDS, TimeUnit.SECONDS)).isTrue()
+        assertThat(finalize).isNotEqualTo(null)
+        assertThat(finalize!!.hasError()).isFalse()
+
+        // Verify.
+        val uri = finalize!!.outputResults.outputUri
+        assertThat(uri).isNotEqualTo(Uri.EMPTY)
+        checkFileVideo(uri)
+
+        // Cleanup.
+        val contentResolver: ContentResolver = this.activity!!.contentResolver
+        contentResolver.delete(uri, null, null)
+    }
+
+    private fun checkFileVideo(uri: Uri) {
+        val mediaRetriever = MediaMetadataRetriever()
+        mediaRetriever.apply {
+            setDataSource(ApplicationProvider.getApplicationContext(), uri)
+            val hasVideo = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO)
+            assertThat(hasVideo).isEqualTo("yes")
+        }
+    }
+
     private fun createFragmentScenario(): FragmentScenario<CameraControllerFragment> {
         return FragmentScenario.launchInContainer(
             CameraControllerFragment::class.java, null, R.style.AppTheme,
@@ -639,6 +817,7 @@
         val testCameraRule = CameraUtil.PreTestCamera()
 
         const val TIMEOUT_SECONDS = 10L
+        const val RECORDING_COUNT = 5
 
         @JvmStatic
         @Parameterized.Parameters(name = "{0}")
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index b7c539c..79c1b1c 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -18,10 +18,12 @@
 
 import static androidx.camera.core.impl.utils.TransformUtils.getRectToRect;
 import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
 
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
-import static java.util.Collections.singletonList;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.app.Dialog;
 import android.content.ContentResolver;
@@ -33,6 +35,7 @@
 import android.graphics.Paint;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.provider.MediaStore;
@@ -51,9 +54,11 @@
 import android.widget.Toast;
 import android.widget.ToggleButton;
 
+import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.OptIn;
+import androidx.annotation.RequiresPermission;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.CameraSelector;
@@ -65,14 +70,16 @@
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.camera.video.MediaStoreOutputOptions;
+import androidx.camera.video.Recording;
+import androidx.camera.video.VideoRecordEvent;
 import androidx.camera.view.CameraController;
 import androidx.camera.view.LifecycleCameraController;
 import androidx.camera.view.PreviewView;
 import androidx.camera.view.RotationProvider;
+import androidx.camera.view.video.AudioConfig;
 import androidx.camera.view.video.ExperimentalVideo;
-import androidx.camera.view.video.OnVideoSavedCallback;
-import androidx.camera.view.video.OutputFileOptions;
-import androidx.camera.view.video.OutputFileResults;
+import androidx.core.util.Consumer;
 import androidx.fragment.app.Fragment;
 import androidx.lifecycle.LiveData;
 
@@ -121,6 +128,21 @@
     private RotationProvider mRotationProvider;
     private int mRotation;
     private final RotationProvider.Listener mRotationListener = rotation -> mRotation = rotation;
+    @Nullable private Recording mActiveRecording = null;
+    private final Consumer<VideoRecordEvent> mVideoRecordEventListener = videoRecordEvent -> {
+        if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
+            VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) videoRecordEvent;
+            Uri uri = finalize.getOutputResults().getOutputUri();
+
+            if (finalize.getError() == ERROR_NONE) {
+                toast("Video saved to: " + uri);
+            } else {
+                String msg = "Saved uri " + uri;
+                msg += " with code (" + finalize.getError() + ")";
+                toast("Failed to save video: " + msg);
+            }
+        }
+    };
 
     // Wrapped analyzer for tests to receive callbacks.
     @Nullable
@@ -128,6 +150,7 @@
 
     @VisibleForTesting
     ToneMappingPreviewEffect mToneMappingPreviewEffect;
+    ToneMappingImageEffect mToneMappingImageEffect;
 
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
@@ -148,6 +171,21 @@
     };
 
     @NonNull
+    private MediaStoreOutputOptions getNewVideoOutputMediaStoreOptions() {
+        String videoFileName = "video_" + System.currentTimeMillis();
+        ContentResolver resolver = requireContext().getContentResolver();
+        ContentValues contentValues = new ContentValues();
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
+        contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
+        contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
+        return new MediaStoreOutputOptions
+                .Builder(resolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+                .setContentValues(contentValues)
+                .build();
+    }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @NonNull
     @Override
     @OptIn(markerClass = ExperimentalVideo.class)
     public View onCreateView(
@@ -184,6 +222,7 @@
 
         // Set up post-processing effects.
         mToneMappingPreviewEffect = new ToneMappingPreviewEffect();
+        mToneMappingImageEffect = new ToneMappingImageEffect();
         mEffectToggle = view.findViewById(R.id.effect_toggle);
         mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
         onEffectsToggled();
@@ -259,30 +298,7 @@
 
         view.findViewById(R.id.video_record).setOnClickListener(v -> {
             try {
-                String videoFileName = "video_" + System.currentTimeMillis();
-                ContentResolver resolver = requireContext().getContentResolver();
-                ContentValues contentValues = new ContentValues();
-                contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4");
-                contentValues.put(MediaStore.Video.Media.TITLE, videoFileName);
-                contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName);
-                OutputFileOptions outputFileOptions = OutputFileOptions.builder(resolver,
-                        MediaStore.Video.Media.EXTERNAL_CONTENT_URI, contentValues).build();
-                mCameraController.startRecording(outputFileOptions, mExecutorService,
-                        new OnVideoSavedCallback() {
-                            @Override
-                            public void onVideoSaved(
-                                    @NonNull OutputFileResults outputFileResults) {
-                                toast("Video saved to: "
-                                        + outputFileResults.getSavedUri());
-                            }
-
-                            @Override
-                            public void onError(int videoCaptureError,
-                                    @NonNull String message,
-                                    @Nullable Throwable cause) {
-                                toast("Failed to save video: " + message);
-                            }
-                        });
+                startRecording(mVideoRecordEventListener);
             } catch (RuntimeException exception) {
                 toast("Failed to record video: " + exception.getMessage());
             }
@@ -290,7 +306,7 @@
         });
         view.findViewById(R.id.video_stop_recording).setOnClickListener(
                 v -> {
-                    mCameraController.stopRecording();
+                    stopRecording();
                     updateUiText();
                 });
 
@@ -358,7 +374,8 @@
 
     private void onEffectsToggled() {
         if (mEffectToggle.isChecked()) {
-            mCameraController.setEffects(singletonList(mToneMappingPreviewEffect));
+            mCameraController.setEffects(
+                    asList(mToneMappingPreviewEffect, mToneMappingImageEffect));
         } else {
             mCameraController.setEffects(emptyList());
         }
@@ -636,4 +653,25 @@
                         contentValues).build();
         mCameraController.takePicture(outputFileOptions, mExecutorService, callback);
     }
+
+    @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+    @VisibleForTesting
+    @MainThread
+    @OptIn(markerClass = ExperimentalVideo.class)
+    void startRecording(Consumer<VideoRecordEvent> listener) {
+        MediaStoreOutputOptions outputOptions = getNewVideoOutputMediaStoreOptions();
+        AudioConfig audioConfig = AudioConfig.create(true);
+        mActiveRecording = mCameraController.startRecording(outputOptions, audioConfig,
+                mExecutorService, listener);
+    }
+
+    @VisibleForTesting
+    @MainThread
+    @OptIn(markerClass = ExperimentalVideo.class)
+    void stopRecording() {
+        if (mActiveRecording != null) {
+            mActiveRecording.stop();
+        }
+    }
+
 }
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
new file mode 100644
index 0000000..846fa0c
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingImageEffect.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.integration.view
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.Paint
+import androidx.camera.core.CameraEffect
+import androidx.camera.core.ImageProcessor
+import androidx.camera.core.ImageProcessor.Response
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.imagecapture.RgbaImageProxy
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+
+/**
+ * A image effect that applies the same tone mapping as [ToneMappingSurfaceProcessor].
+ */
+class ToneMappingImageEffect : CameraEffect(
+    IMAGE_CAPTURE, mainThreadExecutor(), ToneMappingImageProcessor()
+) {
+
+    fun isInvoked(): Boolean {
+        return (imageProcessor as ToneMappingImageProcessor).processoed
+    }
+
+    private class ToneMappingImageProcessor : ImageProcessor {
+
+        var processoed = false
+
+        override fun process(request: ImageProcessor.Request): Response {
+            processoed = true
+            val inputImage = request.inputImages.single() as RgbaImageProxy
+            val bitmap = inputImage.createBitmap()
+            applyToneMapping(bitmap)
+            val outputImage = createOutputImage(bitmap, inputImage)
+            inputImage.close()
+            return Response { outputImage }
+        }
+
+        /**
+         * Creates output image
+         */
+        private fun createOutputImage(newBitmap: Bitmap, imageIn: ImageProxy): ImageProxy {
+            return RgbaImageProxy(
+                newBitmap,
+                imageIn.cropRect,
+                imageIn.imageInfo.rotationDegrees,
+                imageIn.imageInfo.sensorToBufferTransformMatrix,
+                imageIn.imageInfo.timestamp
+            )
+        }
+
+        /**
+         * Applies the same color matrix as [ToneMappingSurfaceProcessor].
+         */
+        private fun applyToneMapping(bitmap: Bitmap) {
+            val paint = Paint()
+            paint.colorFilter = ColorMatrixColorFilter(
+                floatArrayOf(
+                    0.5F, 0.8F, 0.3F, 0F, 0F,
+                    0.4F, 0.7F, 0.2F, 0F, 0F,
+                    0.3F, 0.5F, 0.1F, 0F, 0F,
+                    0F, 0F, 0F, 1F, 0F,
+                )
+            )
+            val canvas = Canvas(bitmap)
+            canvas.drawBitmap(bitmap, 0F, 0F, paint)
+        }
+    }
+}
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 8157779..5bc3d2c 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -870,7 +870,18 @@
     method public androidx.car.app.messaging.model.CarMessage.Builder setSender(androidx.core.app.Person);
   }
 
+  @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi public interface ConversationCallback {
+    method public void onMarkAsRead();
+    method public void onTextReply(String);
+  }
+
+  @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public interface ConversationCallbackDelegate {
+    method public void sendMarkAsRead(androidx.car.app.OnDoneCallback);
+    method public void sendTextReply(String, androidx.car.app.OnDoneCallback);
+  }
+
   @androidx.car.app.annotations.CarProtocol @androidx.car.app.annotations.ExperimentalCarApi @androidx.car.app.annotations.RequiresCarApi(6) public class ConversationItem implements androidx.car.app.model.Item {
+    method public androidx.car.app.messaging.model.ConversationCallbackDelegate getConversationCallbackDelegate();
     method public androidx.car.app.model.CarIcon? getIcon();
     method public String getId();
     method public java.util.List<androidx.car.app.messaging.model.CarMessage!> getMessages();
@@ -881,6 +892,7 @@
   public static final class ConversationItem.Builder {
     ctor public ConversationItem.Builder();
     method public androidx.car.app.messaging.model.ConversationItem build();
+    method public androidx.car.app.messaging.model.ConversationItem.Builder setConversationCallback(androidx.car.app.messaging.model.ConversationCallback);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setGroupConversation(boolean);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setIcon(androidx.car.app.model.CarIcon);
     method public androidx.car.app.messaging.model.ConversationItem.Builder setId(String);
diff --git a/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl b/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl
new file mode 100644
index 0000000..44042e2
--- /dev/null
+++ b/car/app/app/src/main/aidl/androidx/car/app/messaging/model/IConversationCallback.aidl
@@ -0,0 +1,34 @@
+/*
+ * 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.car.app.messaging.model;
+
+import androidx.car.app.IOnDoneCallback;
+
+/**
+ * Handles Host -> Client IPC calls for a conversation.
+ *
+ * @hide
+ */
+oneway interface IConversationCallback {
+  /**
+   * Notifies the app that it should mark all messages in the current conversation as read
+   */
+  void onMarkAsRead(IOnDoneCallback callback) = 1;
+  /**
+   * Notifies the app that it should send a reply to a given conversation
+   */
+  void onTextReply(IOnDoneCallback callback, String replyText) = 2;
+}
\ No newline at end of file
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.java
new file mode 100644
index 0000000..e369a72
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallback.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.model;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+
+/** Host -> Client callbacks for a {@link ConversationItem} */
+@ExperimentalCarApi
+@CarProtocol
+public interface ConversationCallback {
+    /**
+     * Notifies the app that it should mark all messages in the current conversation as read
+     */
+    void onMarkAsRead();
+
+    /**
+     * Notifies the app that it should send a reply to a given conversation
+     */
+    void onTextReply(@NonNull String replyText);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java
new file mode 100644
index 0000000..924e566
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegate.java
@@ -0,0 +1,45 @@
+/*
+ * 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.model;
+
+import android.annotation.SuppressLint;
+
+import androidx.annotation.NonNull;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+
+/** Used by the host to invoke {@link ConversationCallback} methods on the client */
+@ExperimentalCarApi
+@CarProtocol
+@RequiresCarApi(6)
+public interface ConversationCallbackDelegate {
+
+    /** Called from the host to invoke {@link ConversationCallback#onMarkAsRead()} on the client. */
+    // This mirrors the AIDL class and is not supported to support an executor as an input.
+    @SuppressLint("ExecutorRegistration")
+    void sendMarkAsRead(@NonNull OnDoneCallback onDoneCallback);
+
+    /**
+     * Called from the host to invoke {@link ConversationCallback#onTextReply(String)} on the
+     * client.
+     */
+    // This mirrors the AIDL class and is not supported to support an executor as an input.
+    @SuppressLint("ExecutorRegistration")
+    void sendTextReply(@NonNull String replyText, @NonNull OnDoneCallback onDoneCallback);
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java
new file mode 100644
index 0000000..a31fd66
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationCallbackDelegateImpl.java
@@ -0,0 +1,118 @@
+/*
+ * 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.model;
+
+import static androidx.annotation.RestrictTo.Scope.LIBRARY;
+
+import static java.util.Objects.requireNonNull;
+
+import android.os.RemoteException;
+
+import androidx.annotation.Keep;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+import androidx.car.app.IOnDoneCallback;
+import androidx.car.app.OnDoneCallback;
+import androidx.car.app.annotations.CarProtocol;
+import androidx.car.app.annotations.ExperimentalCarApi;
+import androidx.car.app.annotations.RequiresCarApi;
+import androidx.car.app.utils.RemoteUtils;
+
+/**
+ * Handles binder transactions related to {@link ConversationCallback}
+ *
+ * <p> This class exists because we don't want to expose {@link IConversationCallback} to the A4C
+ * client.
+ *
+ * @hide
+ */
+@ExperimentalCarApi
+@RestrictTo(LIBRARY)
+@CarProtocol
+@RequiresCarApi(6)
+class ConversationCallbackDelegateImpl implements ConversationCallbackDelegate {
+    @Keep
+    @Nullable
+    private final IConversationCallback mConversationCallbackBinder;
+
+    ConversationCallbackDelegateImpl(@NonNull ConversationCallback conversationCallback) {
+        mConversationCallbackBinder = new ConversationCallbackStub(conversationCallback);
+    }
+
+    /** Default constructor for serialization. */
+    private ConversationCallbackDelegateImpl() {
+        mConversationCallbackBinder = null;
+    }
+
+    @Override
+    public void sendMarkAsRead(@NonNull OnDoneCallback onDoneCallback) {
+        try {
+            requireNonNull(mConversationCallbackBinder)
+                    .onMarkAsRead(RemoteUtils.createOnDoneCallbackStub(onDoneCallback));
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void sendTextReply(@NonNull String replyText, @NonNull OnDoneCallback onDoneCallback) {
+        try {
+            requireNonNull(mConversationCallbackBinder).onTextReply(
+                    RemoteUtils.createOnDoneCallbackStub(onDoneCallback),
+                    replyText
+            );
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static class ConversationCallbackStub extends IConversationCallback.Stub {
+        @Keep
+        @NonNull
+        private final ConversationCallback mConversationCallback;
+
+        ConversationCallbackStub(@NonNull ConversationCallback conversationCallback) {
+            mConversationCallback = conversationCallback;
+        }
+
+        @Override
+        public void onMarkAsRead(@NonNull IOnDoneCallback onDoneCallback) {
+            RemoteUtils.dispatchCallFromHost(
+                    onDoneCallback,
+                    "onMarkAsRead", () -> {
+                        mConversationCallback.onMarkAsRead();
+                        return null;
+                    }
+            );
+        }
+
+        @Override
+        public void onTextReply(
+                @NonNull IOnDoneCallback onDoneCallback,
+                @NonNull String replyText
+        ) {
+            RemoteUtils.dispatchCallFromHost(
+                    onDoneCallback,
+                    "onReply", () -> {
+                        mConversationCallback.onTextReply(replyText);
+                        return null;
+                    }
+            );
+        }
+    }
+}
diff --git a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
index 42eeeba..2b70b2c 100644
--- a/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/messaging/model/ConversationItem.java
@@ -18,6 +18,8 @@
 
 import static java.util.Objects.requireNonNull;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.car.app.annotations.CarProtocol;
@@ -35,12 +37,17 @@
 @CarProtocol
 @RequiresCarApi(6)
 public class ConversationItem implements Item {
-    @NonNull private final String mId;
-    @NonNull private final CarText mTitle;
+    @NonNull
+    private final String mId;
+    @NonNull
+    private final CarText mTitle;
     @Nullable
     private final CarIcon mIcon;
     private final boolean mIsGroupConversation;
-    @NonNull private final List<CarMessage> mMessages;
+    @NonNull
+    private final List<CarMessage> mMessages;
+    @NonNull
+    private final ConversationCallbackDelegate mConversationCallbackDelegate;
 
     ConversationItem(@NonNull Builder builder) {
         this.mId = requireNonNull(builder.mId);
@@ -48,6 +55,8 @@
         this.mIcon = builder.mIcon;
         this.mIsGroupConversation = builder.mIsGroupConversation;
         this.mMessages = requireNonNull(builder.mMessages);
+        this.mConversationCallbackDelegate = new ConversationCallbackDelegateImpl(
+                requireNonNull(builder.mConversationCallback));
     }
 
     /** Default constructor for serialization. */
@@ -57,6 +66,18 @@
         mIcon = null;
         mIsGroupConversation = false;
         mMessages = new ArrayList<>();
+        mConversationCallbackDelegate = new ConversationCallbackDelegateImpl(
+                new ConversationCallback() {
+                    @Override
+                    public void onMarkAsRead() {
+                        // Do nothing
+                    }
+
+                    @Override
+                    public void onTextReply(@NonNull String replyText) {
+                        // Do nothing
+                    }
+                });
     }
 
     /**
@@ -64,17 +85,20 @@
      *
      * @see Builder#setId
      */
-    public @NonNull String getId() {
+    @NonNull
+    public String getId() {
         return mId;
     }
 
     /** Returns the title of the conversation */
-    public @NonNull CarText getTitle() {
+    @NonNull
+    public CarText getTitle() {
         return mTitle;
     }
 
     /** Returns a {@link CarIcon} for the conversation, or {@code null} if not set */
-    public @Nullable CarIcon getIcon() {
+    @Nullable
+    public CarIcon getIcon() {
         return mIcon;
     }
 
@@ -88,10 +112,17 @@
     }
 
     /** Returns a list of messages for this {@link ConversationItem} */
-    public @NonNull List<CarMessage> getMessages() {
+    @NonNull
+    public List<CarMessage> getMessages() {
         return mMessages;
     }
 
+    /** Returns host->client callbacks for this conversation */
+    @NonNull
+    public ConversationCallbackDelegate getConversationCallbackDelegate() {
+        return mConversationCallbackDelegate;
+    }
+
     /** A builder for {@link ConversationItem} */
     public static final class Builder {
         @Nullable
@@ -103,6 +134,8 @@
         boolean mIsGroupConversation;
         @Nullable
         List<CarMessage> mMessages;
+        @Nullable
+        ConversationCallback mConversationCallback;
 
         /**
          * Specifies a unique identifier for the conversation
@@ -114,19 +147,22 @@
          *     <li> Identifying {@link ConversationItem}s in "mark as read" / "reply" callbacks
          * </ul>
          */
-        public @NonNull Builder setId(@NonNull String id) {
+        @NonNull
+        public Builder setId(@NonNull String id) {
             mId = id;
             return this;
         }
 
         /** Sets the title of the conversation */
-        public @NonNull Builder setTitle(@NonNull CarText title) {
+        @NonNull
+        public Builder setTitle(@NonNull CarText title) {
             mTitle = title;
             return this;
         }
 
         /** Sets a {@link CarIcon} for the conversation */
-        public @NonNull Builder setIcon(@NonNull CarIcon icon) {
+        @NonNull
+        public Builder setIcon(@NonNull CarIcon icon) {
             mIcon = icon;
             return this;
         }
@@ -141,19 +177,31 @@
          * historical example, message readout may include sender names for group conversations, but
          * omit them for 1:1 conversations.
          */
-        public @NonNull Builder setGroupConversation(boolean isGroupConversation) {
+        @NonNull
+        public Builder setGroupConversation(boolean isGroupConversation) {
             mIsGroupConversation = isGroupConversation;
             return this;
         }
 
         /** Specifies a list of messages for the conversation */
-        public @NonNull Builder setMessages(@NonNull List<CarMessage> messages) {
+        @NonNull
+        public Builder setMessages(@NonNull List<CarMessage> messages) {
             mMessages = messages;
             return this;
         }
 
+        /** Sets a {@link ConversationCallback} for the conversation */
+        @SuppressLint({"MissingGetterMatchingBuilder", "ExecutorRegistration"})
+        @NonNull
+        public Builder setConversationCallback(
+                @NonNull ConversationCallback conversationCallback) {
+            mConversationCallback = conversationCallback;
+            return this;
+        }
+
         /** Returns a new {@link ConversationItem} instance defined by this builder */
-        public @NonNull ConversationItem build() {
+        @NonNull
+        public ConversationItem build() {
             return new ConversationItem(this);
         }
     }
diff --git a/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java b/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
index cfa432d..3791896 100644
--- a/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
+++ b/car/app/app/src/main/java/androidx/car/app/suggestion/model/Suggestion.java
@@ -88,7 +88,7 @@
     }
 
     /**
-     * Returns an image to display with the suggestion or {@code null} if not set.
+     * Returns a {@code CarIcon} to display with the suggestion or {@code null} if not set.
      *
      * @see Builder#setIcon(CarIcon)
      */
@@ -226,7 +226,7 @@
         }
 
         /**
-         * Sets su suggestion image to display.
+         * Sets a suggestion image to display.
          *
          * <h4>Image Sizing Guidance</h4>
          *
@@ -235,6 +235,8 @@
          * either one of the dimensions, it will be scaled down to be centered inside the
          * bounding box while preserving the aspect ratio.
          *
+         * Icon images are expected to be tintable.
+         *
          * <p>See {@link CarIcon} for more details related to providing icon and image resources
          * that work with different car screen pixel densities.
          *
diff --git a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
index fa94459..8bb5fca 100644
--- a/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
+++ b/car/app/app/src/test/java/androidx/car/app/model/constraints/RowListConstraintsTest.java
@@ -18,7 +18,9 @@
 
 import static org.junit.Assert.assertThrows;
 
+import androidx.annotation.NonNull;
 import androidx.car.app.TestUtils;
+import androidx.car.app.messaging.model.ConversationCallback;
 import androidx.car.app.messaging.model.ConversationItem;
 import androidx.car.app.model.CarText;
 import androidx.car.app.model.ItemList;
@@ -97,6 +99,17 @@
                         .setId("id")
                         .setTitle(CarText.create("title"))
                         .setMessages(new ArrayList<>())
+                        .setConversationCallback(new ConversationCallback() {
+                            @Override
+                            public void onMarkAsRead() {
+                                // do nothing
+                            }
+
+                            @Override
+                            public void onTextReply(@NonNull String replyText) {
+                                // do nothing
+                            }
+                        })
                         .build()
                 )
                 .build();
diff --git a/collection/collection/api/current.ignore b/collection/collection/api/current.ignore
index decf837..6397290 100644
--- a/collection/collection/api/current.ignore
+++ b/collection/collection/api/current.ignore
@@ -9,28 +9,12 @@
     Method androidx.collection.SparseArrayCompat.clone has changed return type from androidx.collection.SparseArrayCompat<E!> to androidx.collection.SparseArrayCompat<E>
 
 
-RemovedInterface: androidx.collection.ArraySet:
-    Class androidx.collection.ArraySet no longer implements java.util.Collection<E>
-
-
 RemovedMethod: androidx.collection.ArraySet#ArraySet(androidx.collection.ArraySet<E>):
     Removed constructor androidx.collection.ArraySet(androidx.collection.ArraySet<E>)
 RemovedMethod: androidx.collection.ArraySet#ArraySet(java.util.Collection<E>):
     Removed constructor androidx.collection.ArraySet(java.util.Collection<E>)
-RemovedMethod: androidx.collection.ArraySet#add(E):
-    Removed method androidx.collection.ArraySet.add(E)
-RemovedMethod: androidx.collection.ArraySet#addAll(java.util.Collection<? extends E>):
-    Removed method androidx.collection.ArraySet.addAll(java.util.Collection<? extends E>)
-RemovedMethod: androidx.collection.ArraySet#clear():
-    Removed method androidx.collection.ArraySet.clear()
-RemovedMethod: androidx.collection.ArraySet#isEmpty():
-    Removed method androidx.collection.ArraySet.isEmpty()
 RemovedMethod: androidx.collection.ArraySet#size():
     Removed method androidx.collection.ArraySet.size()
-RemovedMethod: androidx.collection.ArraySet#toArray():
-    Removed method androidx.collection.ArraySet.toArray()
-RemovedMethod: androidx.collection.ArraySet#toArray(T[]):
-    Removed method androidx.collection.ArraySet.toArray(T[])
 RemovedMethod: androidx.collection.LruCache#toString():
     Removed method androidx.collection.LruCache.toString()
 RemovedMethod: androidx.collection.SimpleArrayMap#SimpleArrayMap(androidx.collection.SimpleArrayMap<K,V>):
diff --git a/collection/collection/api/restricted_current.ignore b/collection/collection/api/restricted_current.ignore
index decf837..6397290 100644
--- a/collection/collection/api/restricted_current.ignore
+++ b/collection/collection/api/restricted_current.ignore
@@ -9,28 +9,12 @@
     Method androidx.collection.SparseArrayCompat.clone has changed return type from androidx.collection.SparseArrayCompat<E!> to androidx.collection.SparseArrayCompat<E>
 
 
-RemovedInterface: androidx.collection.ArraySet:
-    Class androidx.collection.ArraySet no longer implements java.util.Collection<E>
-
-
 RemovedMethod: androidx.collection.ArraySet#ArraySet(androidx.collection.ArraySet<E>):
     Removed constructor androidx.collection.ArraySet(androidx.collection.ArraySet<E>)
 RemovedMethod: androidx.collection.ArraySet#ArraySet(java.util.Collection<E>):
     Removed constructor androidx.collection.ArraySet(java.util.Collection<E>)
-RemovedMethod: androidx.collection.ArraySet#add(E):
-    Removed method androidx.collection.ArraySet.add(E)
-RemovedMethod: androidx.collection.ArraySet#addAll(java.util.Collection<? extends E>):
-    Removed method androidx.collection.ArraySet.addAll(java.util.Collection<? extends E>)
-RemovedMethod: androidx.collection.ArraySet#clear():
-    Removed method androidx.collection.ArraySet.clear()
-RemovedMethod: androidx.collection.ArraySet#isEmpty():
-    Removed method androidx.collection.ArraySet.isEmpty()
 RemovedMethod: androidx.collection.ArraySet#size():
     Removed method androidx.collection.ArraySet.size()
-RemovedMethod: androidx.collection.ArraySet#toArray():
-    Removed method androidx.collection.ArraySet.toArray()
-RemovedMethod: androidx.collection.ArraySet#toArray(T[]):
-    Removed method androidx.collection.ArraySet.toArray(T[])
 RemovedMethod: androidx.collection.LruCache#toString():
     Removed method androidx.collection.LruCache.toString()
 RemovedMethod: androidx.collection.SimpleArrayMap#SimpleArrayMap(androidx.collection.SimpleArrayMap<K,V>):
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
index 6d7a336..0c7ac8e 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
@@ -59,132 +59,37 @@
  * @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
  * will grow once items are added to it.
  */
-public class ArraySet<E> @JvmOverloads constructor(capacity: Int = 0) :
-    MutableCollection<E>, MutableSet<E> {
+public expect class ArraySet<E> @JvmOverloads constructor(
+    capacity: Int = 0
+) : MutableCollection<E>, MutableSet<E> {
 
-    private var hashes: IntArray = EMPTY_INTS
-    private var array: Array<Any?> = EMPTY_OBJECTS
+    internal var hashes: IntArray
+    internal var array: Array<Any?>
 
-    private var _size = 0
-
+    internal var _size: Int
     override val size: Int
-        get() = _size
 
     /**
      * Create a new ArraySet with the mappings from the given ArraySet.
      */
-    public constructor(set: ArraySet<out E>?) : this(capacity = 0) {
-        if (set != null) {
-            addAll(set)
-        }
-    }
+    public constructor(set: ArraySet<out E>?)
 
     /**
      * Create a new ArraySet with the mappings from the given [Collection].
      */
-    public constructor(set: Collection<E>?) : this(capacity = 0) {
-        if (set != null) {
-            addAll(set)
-        }
-    }
+    public constructor(set: Collection<E>?)
 
     /**
      * Create a new ArraySet with items from the given array.
      */
-    public constructor(array: Array<out E>?) : this(capacity = 0) {
-        if (array != null) {
-            for (value in array) {
-                add(value)
-            }
-        }
-    }
-
-    init {
-        if (capacity > 0) {
-            allocArrays(capacity)
-        }
-    }
-
-    private fun binarySearchInternal(hash: Int): Int =
-        try {
-            binarySearch(hashes, _size, hash)
-        } catch (e: IndexOutOfBoundsException) {
-            throw ConcurrentModificationException()
-        }
-
-    private fun indexOf(key: Any?, hash: Int): Int {
-        val n = _size
-
-        // Important fast case: if nothing is in here, nothing to look for.
-        if (n == 0) {
-            return -1
-        }
-        val index = binarySearchInternal(hash)
-
-        // If the hash code wasn't found, then we have no entry for this key.
-        if (index < 0) {
-            return index
-        }
-
-        // If the key at the returned index matches, that's what we want.
-        if (key == array[index]) {
-            return index
-        }
-
-        // Search for a matching key after the index.
-        var end = index + 1
-        while (end < n && hashes[end] == hash) {
-            if (key == array[end]) {
-                return end
-            }
-            end++
-        }
-
-        // Search for a matching key before the index.
-        var i = index - 1
-        while (i >= 0 && hashes[i] == hash) {
-            if (key == array[i]) {
-                return i
-            }
-            i--
-        }
-
-        // Key not found -- return negative value indicating where a
-        // new entry for this key should go.  We use the end of the
-        // hash chain to reduce the number of array entries that will
-        // need to be copied when inserting.
-        return end.inv()
-    }
-
-    private fun indexOfNull(): Int = indexOf(key = null, hash = 0)
-
-    private fun allocArrays(size: Int) {
-        hashes = IntArray(size)
-        array = arrayOfNulls(size)
-    }
-
-    private inline fun printlnIfDebug(message: () -> String) {
-        if (DEBUG) {
-            println(message())
-        }
-    }
+    public constructor(array: Array<out E>?)
 
     /**
      * Make the array map empty.  All storage is released.
      *
      * @throws ConcurrentModificationException if concurrent modifications detected.
      */
-    override fun clear() {
-        if (_size != 0) {
-            hashes = EMPTY_INTS
-            array = EMPTY_OBJECTS
-            _size = 0
-        }
-        @Suppress("KotlinConstantConditions")
-        if (_size != 0) {
-            throw ConcurrentModificationException()
-        }
-    }
+    override fun clear()
 
     /**
      * Ensure the array map can hold at least [minimumCapacity]
@@ -192,21 +97,7 @@
      *
      * @throws ConcurrentModificationException if concurrent modifications detected.
      */
-    public fun ensureCapacity(minimumCapacity: Int) {
-        val oSize: Int = _size
-        if (hashes.size < minimumCapacity) {
-            val ohashes = hashes
-            val oarray = array
-            allocArrays(minimumCapacity)
-            if (_size > 0) {
-                ohashes.copyInto(destination = hashes, endIndex = _size)
-                oarray.copyInto(destination = array, endIndex = _size)
-            }
-        }
-        if (_size != oSize) {
-            throw ConcurrentModificationException()
-        }
-    }
+    public fun ensureCapacity(minimumCapacity: Int)
 
     /**
      * Check whether a value exists in the set.
@@ -214,8 +105,7 @@
      * @param element The value to search for.
      * @return Returns true if the value exists, else false.
      */
-    override operator fun contains(element: E): Boolean =
-        indexOf(element) >= 0
+    override operator fun contains(element: E): Boolean
 
     /**
      * Returns the index of a value in the set.
@@ -223,23 +113,20 @@
      * @param key The value to search for.
      * @return Returns the index of the value if it exists, else a negative integer.
      */
-    public fun indexOf(key: Any?): Int =
-        if (key == null) indexOfNull() else indexOf(key = key, hash = key.hashCode())
+    public fun indexOf(key: Any?): Int
 
     /**
      * Return the value at the given index in the array.
+     *
      * @param index The desired index, must be between 0 and [size]-1.
      * @return Returns the value stored at the given index.
      */
-    @Suppress("UNCHECKED_CAST")
-    public fun valueAt(index: Int): E =
-        array[index] as E
+    public fun valueAt(index: Int): E
 
     /**
-     * Return true if the array map contains no items.
+     * Return `true` if the array map contains no items.
      */
-    override fun isEmpty(): Boolean =
-        _size <= 0
+    override fun isEmpty(): Boolean
 
     /**
      * Adds the specified object to this set. The set is not modified if it
@@ -249,98 +136,15 @@
      * @return `true` if this set is modified, `false` otherwise.
      * @throws ConcurrentModificationException if concurrent modifications detected.
      */
-    override fun add(element: E): Boolean {
-        val oSize = _size
-        val hash: Int
-        var index: Int
-        if (element == null) {
-            hash = 0
-            index = indexOfNull()
-        } else {
-            hash = element.hashCode()
-            index = indexOf(element, hash)
-        }
-
-        if (index >= 0) {
-            return false
-        }
-
-        index = index.inv()
-        if (oSize >= hashes.size) {
-            val n =
-                when {
-                    oSize >= BASE_SIZE * 2 -> oSize + (oSize shr 1)
-                    oSize >= BASE_SIZE -> BASE_SIZE * 2
-                    else -> BASE_SIZE
-                }
-
-            printlnIfDebug { "$TAG add: grow from ${hashes.size} to $n" }
-
-            val ohashes = hashes
-            val oarray = array
-            allocArrays(n)
-
-            if (oSize != _size) {
-                throw ConcurrentModificationException()
-            }
-
-            if (hashes.isNotEmpty()) {
-                printlnIfDebug { "$TAG add: copy 0-$oSize to 0" }
-                ohashes.copyInto(destination = hashes, endIndex = ohashes.size)
-                oarray.copyInto(destination = array, endIndex = oarray.size)
-            }
-        }
-
-        if (index < oSize) {
-            printlnIfDebug { "$TAG add: move $index-${oSize - index} to ${index + 1}" }
-
-            hashes.copyInto(
-                destination = hashes,
-                destinationOffset = index + 1,
-                startIndex = index,
-                endIndex = oSize
-            )
-            array.copyInto(
-                destination = array,
-                destinationOffset = index + 1,
-                startIndex = index,
-                endIndex = oSize
-            )
-        }
-
-        if (oSize != _size || index >= hashes.size) {
-            throw ConcurrentModificationException()
-        }
-
-        hashes[index] = hash
-        array[index] = element
-        _size++
-        return true
-    }
+    override fun add(element: E): Boolean
 
     /**
      * Perform a [add] of all values in [array]
+     *
      * @param array The array whose contents are to be retrieved.
      * @throws ConcurrentModificationException if concurrent modifications detected.
      */
-    public fun addAll(array: ArraySet<out E>) {
-        val n = array._size
-        ensureCapacity(_size + n)
-        if (_size == 0) {
-            if (n > 0) {
-                array.hashes.copyInto(destination = hashes, endIndex = n)
-                array.array.copyInto(destination = this.array, endIndex = n)
-                if (0 != _size) {
-                    throw ConcurrentModificationException()
-                }
-                _size = n
-            }
-        } else {
-            for (i in 0 until n) {
-                add(array.valueAt(i))
-            }
-        }
-    }
+    public fun addAll(array: ArraySet<out E>)
 
     /**
      * Removes the specified object from this set.
@@ -348,125 +152,23 @@
      * @param element the object to remove.
      * @return `true` if this set was modified, `false` otherwise.
      */
-    override fun remove(element: E): Boolean {
-        val index = indexOf(element)
-        if (index >= 0) {
-            removeAt(index)
-            return true
-        }
-        return false
-    }
+    override fun remove(element: E): Boolean
 
     /**
      * Remove the key/value mapping at the given index.
+     *
      * @param index The desired index, must be between 0 and [size]-1.
      * @return Returns the value that was stored at this index.
      * @throws ConcurrentModificationException if concurrent modifications detected.
      */
-    public fun removeAt(index: Int): E {
-        val oSize = _size
-        val old = array[index]
-        if (oSize <= 1) {
-            // Now empty.
-            printlnIfDebug { "$TAG remove: shrink from ${hashes.size} to 0" }
-            clear()
-        } else {
-            val nSize = oSize - 1
-            if (hashes.size > (BASE_SIZE * 2) && (_size < hashes.size / 3)) {
-                // Shrunk enough to reduce size of arrays.  We don't allow it to
-                // shrink smaller than (BASE_SIZE*2) to avoid flapping between
-                // that and BASE_SIZE.
-                val n = if (_size > BASE_SIZE * 2) _size + (_size shr 1) else BASE_SIZE * 2
-                printlnIfDebug { "$TAG remove: shrink from ${hashes.size} to $n" }
-                val ohashes = hashes
-                val oarray = array
-                allocArrays(n)
-                if (index > 0) {
-                    printlnIfDebug { "$TAG remove: copy from 0-$index to 0" }
-                    ohashes.copyInto(destination = hashes, endIndex = index)
-                    oarray.copyInto(destination = array, endIndex = index)
-                }
-                if (index < nSize) {
-                    printlnIfDebug { "$TAG remove: copy from ${index + 1}-$nSize to $index" }
-                    ohashes.copyInto(
-                        destination = hashes,
-                        destinationOffset = index,
-                        startIndex = index + 1,
-                        endIndex = nSize + 1
-                    )
-                    oarray.copyInto(
-                        destination = array,
-                        destinationOffset = index,
-                        startIndex = index + 1,
-                        endIndex = nSize + 1
-                    )
-                }
-            } else {
-                if (index < nSize) {
-                    printlnIfDebug { "$TAG remove: move ${index + 1}-$nSize to $index" }
-                    hashes.copyInto(
-                        destination = hashes,
-                        destinationOffset = index,
-                        startIndex = index + 1,
-                        endIndex = nSize + 1
-                    )
-                    array.copyInto(
-                        destination = array,
-                        destinationOffset = index,
-                        startIndex = index + 1,
-                        endIndex = nSize + 1
-                    )
-                }
-                array[nSize] = null
-            }
-            if (oSize != _size) {
-                throw ConcurrentModificationException()
-            }
-            _size = nSize
-        }
-        @Suppress("UNCHECKED_CAST")
-        return old as E
-    }
+    public fun removeAt(index: Int): E
 
     /**
      * Perform a [remove] of all values in [array]
+     *
      * @param array The array whose contents are to be removed.
      */
-    public fun removeAll(array: ArraySet<out E>): Boolean {
-        // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
-        //       pass, use the property that the sets are sorted by hash to make this linear passes
-        //       (except for hash collisions, which means worst case still n*m), then do one
-        //       collection pass into a new array. This avoids binary searches and excessive memcpy.
-        val n = array._size
-
-        // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
-        //       the single results, compare size before and after.
-        val originalSize = _size
-        for (i in 0 until n) {
-            remove(array.valueAt(i))
-        }
-        return originalSize != _size
-    }
-
-    @Suppress("ArrayReturn")
-    public fun toArray(): Array<Any?> {
-        return array.copyOfRange(fromIndex = 0, toIndex = _size)
-    }
-
-    @Suppress("ArrayReturn")
-    public fun <T> toArray(array: Array<T>): Array<T> {
-        return if (array.size < _size) {
-            @Suppress("UNCHECKED_CAST")
-            this.array.copyOfRange(fromIndex = 0, toIndex = _size) as Array<T>
-        } else {
-            @Suppress("UNCHECKED_CAST")
-            this.array.copyInto(array as Array<Any?>, 0, 0, _size)
-            if (array.size > _size) {
-                array[_size] = null
-            }
-            array
-        }
-    }
+    public fun removeAll(array: ArraySet<out E>): Boolean
 
     /**
      * This implementation returns false if the object is not a set, or
@@ -477,70 +179,19 @@
      *
      * @see Any.equals
      */
-    override fun equals(other: Any?): Boolean {
-        if (this === other) {
-            return true
-        }
-        if (other is Set<*>) {
-            if (size != other.size) {
-                return false
-            }
-            try {
-                for (i in 0 until _size) {
-                    val mine = valueAt(i)
-                    if (!other.contains(mine)) {
-                        return false
-                    }
-                }
-            } catch (ignored: NullPointerException) {
-                return false
-            } catch (ignored: ClassCastException) {
-                return false
-            }
-            return true
-        }
-        return false
-    }
+    override fun equals(other: Any?): Boolean
 
     /**
      * @see Any.hashCode
      */
-    override fun hashCode(): Int {
-        val hashes = hashes
-        val s = _size
-        var result = 0
-        for (i in 0 until s) {
-            result += hashes[i]
-        }
-        return result
-    }
+    override fun hashCode(): Int
 
     /**
      * This implementation composes a string by iterating over its values. If
      * this set contains itself as a value, the string "(this Set)"
      * will appear in its place.
      */
-    override fun toString(): String {
-        if (isEmpty()) {
-            return "{}"
-        }
-
-        return buildString(capacity = _size * 14) {
-            append('{')
-            for (i in 0 until _size) {
-                if (i > 0) {
-                    append(", ")
-                }
-                val value = valueAt(i)
-                if (value !== this@ArraySet) {
-                    append(value)
-                } else {
-                    append("(this Set)")
-                }
-            }
-            append('}')
-        }
-    }
+    override fun toString(): String
 
     /**
      * Return a [MutableIterator] over all values in the set.
@@ -548,84 +199,430 @@
      * **Note:** this is a less efficient way to access the array contents compared to
      * looping from 0 until [size] and calling [valueAt].
      */
-    override fun iterator(): MutableIterator<E> =
-        ElementIterator()
-
-    private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
-        override fun elementAt(index: Int): E =
-            valueAt(index)
-
-        override fun removeAt(index: Int) {
-            this@ArraySet.removeAt(index)
-        }
-    }
+    override fun iterator(): MutableIterator<E>
 
     /**
      * Determine if the array set contains all of the values in the given collection.
+     *
      * @param elements The collection whose contents are to be checked against.
      * @return Returns true if this array set contains a value for every entry
      * in [elements] else returns false.
      */
-    override fun containsAll(elements: Collection<E>): Boolean {
-        for (item in elements) {
-            if (!contains(item)) {
-                return false
-            }
-        }
-        return true
-    }
+    override fun containsAll(elements: Collection<E>): Boolean
 
     /**
      * Perform an [add] of all values in [elements]
+     *
      * @param elements The collection whose contents are to be retrieved.
      */
-    override fun addAll(elements: Collection<E>): Boolean {
-        ensureCapacity(_size + elements.size)
-        var added = false
-        for (value in elements) {
-            added = add(value) or added
-        }
-        return added
-    }
+    override fun addAll(elements: Collection<E>): Boolean
 
     /**
      * Remove all values in the array set that exist in the given collection.
+     *
      * @param elements The collection whose contents are to be used to remove values.
      * @return Returns true if any values were removed from the array set, else false.
      */
-    override fun removeAll(elements: Collection<E>): Boolean {
-        var removed = false
-        for (value in elements) {
-            removed = removed or remove(value)
-        }
-        return removed
-    }
+    override fun removeAll(elements: Collection<E>): Boolean
 
     /**
      * Remove all values in the array set that do **not** exist in the given collection.
+     *
      * @param elements The collection whose contents are to be used to determine which
      * values to keep.
      * @return Returns true if any values were removed from the array set, else false.
      */
-    override fun retainAll(elements: Collection<E>): Boolean {
-        var removed = false
-        for (i in _size - 1 downTo 0) {
-            if (array[i] !in elements) {
-                removeAt(i)
-                removed = true
+    override fun retainAll(elements: Collection<E>): Boolean
+}
+
+/**
+ * The minimum amount by which the capacity of a ArraySet will increase.
+ * This is tuned to be relatively space-efficient.
+ */
+internal const val ARRAY_SET_BASE_SIZE = 4
+
+internal fun <E> ArraySet<E>.binarySearchInternal(hash: Int): Int =
+    try {
+        binarySearch(hashes, _size, hash)
+    } catch (e: IndexOutOfBoundsException) {
+        throw ConcurrentModificationException()
+    }
+
+internal fun <E> ArraySet<E>.indexOf(key: Any?, hash: Int): Int {
+    val n = _size
+
+    // Important fast case: if nothing is in here, nothing to look for.
+    if (n == 0) {
+        return -1
+    }
+    val index = binarySearchInternal(hash)
+
+    // If the hash code wasn't found, then we have no entry for this key.
+    if (index < 0) {
+        return index
+    }
+
+    // If the key at the returned index matches, that's what we want.
+    if (key == array[index]) {
+        return index
+    }
+
+    // Search for a matching key after the index.
+    var end = index + 1
+    while (end < n && hashes[end] == hash) {
+        if (key == array[end]) {
+            return end
+        }
+        end++
+    }
+
+    // Search for a matching key before the index.
+    var i = index - 1
+    while (i >= 0 && hashes[i] == hash) {
+        if (key == array[i]) {
+            return i
+        }
+        i--
+    }
+
+    // Key not found -- return negative value indicating where a
+    // new entry for this key should go.  We use the end of the
+    // hash chain to reduce the number of array entries that will
+    // need to be copied when inserting.
+    return end.inv()
+}
+
+internal fun <E> ArraySet<E>.indexOfNull(): Int = indexOf(key = null, hash = 0)
+
+internal fun <E> ArraySet<E>.allocArrays(size: Int) {
+    hashes = IntArray(size)
+    array = arrayOfNulls(size)
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.clearInternal() {
+    if (_size != 0) {
+        hashes = EMPTY_INTS
+        array = EMPTY_OBJECTS
+        _size = 0
+    }
+    @Suppress("KotlinConstantConditions")
+    if (_size != 0) {
+        throw ConcurrentModificationException()
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.ensureCapacityInternal(minimumCapacity: Int) {
+    val oSize: Int = _size
+    if (hashes.size < minimumCapacity) {
+        val ohashes = hashes
+        val oarray = array
+        allocArrays(minimumCapacity)
+        if (_size > 0) {
+            ohashes.copyInto(destination = hashes, endIndex = _size)
+            oarray.copyInto(destination = array, endIndex = _size)
+        }
+    }
+    if (_size != oSize) {
+        throw ConcurrentModificationException()
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.containsInternal(element: E): Boolean {
+    return indexOf(element) >= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.indexOfInternal(key: Any?): Int {
+    return if (key == null) indexOfNull() else indexOf(key = key, hash = key.hashCode())
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.valueAtInternal(index: Int): E {
+    @Suppress("UNCHECKED_CAST")
+    return array[index] as E
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.isEmptyInternal(): Boolean {
+    return _size <= 0
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addInternal(element: E): Boolean {
+    val oSize = _size
+    val hash: Int
+    var index: Int
+    if (element == null) {
+        hash = 0
+        index = indexOfNull()
+    } else {
+        hash = element.hashCode()
+        index = indexOf(element, hash)
+    }
+
+    if (index >= 0) {
+        return false
+    }
+
+    index = index.inv()
+    if (oSize >= hashes.size) {
+        val n =
+            when {
+                oSize >= ARRAY_SET_BASE_SIZE * 2 -> oSize + (oSize shr 1)
+                oSize >= ARRAY_SET_BASE_SIZE -> ARRAY_SET_BASE_SIZE * 2
+                else -> ARRAY_SET_BASE_SIZE
+            }
+
+        val ohashes = hashes
+        val oarray = array
+        allocArrays(n)
+
+        if (oSize != _size) {
+            throw ConcurrentModificationException()
+        }
+
+        if (hashes.isNotEmpty()) {
+            ohashes.copyInto(destination = hashes, endIndex = ohashes.size)
+            oarray.copyInto(destination = array, endIndex = oarray.size)
+        }
+    }
+
+    if (index < oSize) {
+        hashes.copyInto(
+            destination = hashes,
+            destinationOffset = index + 1,
+            startIndex = index,
+            endIndex = oSize
+        )
+        array.copyInto(
+            destination = array,
+            destinationOffset = index + 1,
+            startIndex = index,
+            endIndex = oSize
+        )
+    }
+
+    if (oSize != _size || index >= hashes.size) {
+        throw ConcurrentModificationException()
+    }
+
+    hashes[index] = hash
+    array[index] = element
+    _size++
+    return true
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addAllInternal(array: ArraySet<out E>) {
+    val n = array._size
+    ensureCapacity(_size + n)
+    if (_size == 0) {
+        if (n > 0) {
+            array.hashes.copyInto(destination = hashes, endIndex = n)
+            array.array.copyInto(destination = this.array, endIndex = n)
+            if (0 != _size) {
+                throw ConcurrentModificationException()
+            }
+            _size = n
+        }
+    } else {
+        for (i in 0 until n) {
+            add(array.valueAt(i))
+        }
+    }
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeInternal(element: E): Boolean {
+    val index = indexOf(element)
+    if (index >= 0) {
+        removeAt(index)
+        return true
+    }
+    return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAtInternal(index: Int): E {
+    val oSize = _size
+    val old = array[index]
+    if (oSize <= 1) {
+        // Now empty.
+        clear()
+    } else {
+        val nSize = oSize - 1
+        if (hashes.size > (ARRAY_SET_BASE_SIZE * 2) && (_size < hashes.size / 3)) {
+            // Shrunk enough to reduce size of arrays.  We don't allow it to
+            // shrink smaller than (ARRAY_SET_BASE_SIZE*2) to avoid flapping between
+            // that and ARRAY_SET_BASE_SIZE.
+            val n = when {
+                _size > ARRAY_SET_BASE_SIZE * 2 -> _size + (_size shr 1)
+                else -> ARRAY_SET_BASE_SIZE * 2
+            }
+            val ohashes = hashes
+            val oarray = array
+            allocArrays(n)
+            if (index > 0) {
+                ohashes.copyInto(destination = hashes, endIndex = index)
+                oarray.copyInto(destination = array, endIndex = index)
+            }
+            if (index < nSize) {
+                ohashes.copyInto(
+                    destination = hashes,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = nSize + 1
+                )
+                oarray.copyInto(
+                    destination = array,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = nSize + 1
+                )
+            }
+        } else {
+            if (index < nSize) {
+                hashes.copyInto(
+                    destination = hashes,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = nSize + 1
+                )
+                array.copyInto(
+                    destination = array,
+                    destinationOffset = index,
+                    startIndex = index + 1,
+                    endIndex = nSize + 1
+                )
+            }
+            array[nSize] = null
+        }
+        if (oSize != _size) {
+            throw ConcurrentModificationException()
+        }
+        _size = nSize
+    }
+    @Suppress("UNCHECKED_CAST")
+    return old as E
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAllInternal(array: ArraySet<out E>): Boolean {
+    // TODO: If array is sufficiently large, a marking approach might be beneficial. In a first
+    //       pass, use the property that the sets are sorted by hash to make this linear passes
+    //       (except for hash collisions, which means worst case still n*m), then do one
+    //       collection pass into a new array. This avoids binary searches and excessive memcpy.
+    val n = array._size
+
+    // Note: ArraySet does not make thread-safety guarantees. So instead of OR-ing together all
+    //       the single results, compare size before and after.
+    val originalSize = _size
+    for (i in 0 until n) {
+        remove(array.valueAt(i))
+    }
+    return originalSize != _size
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.equalsInternal(other: Any?): Boolean {
+    if (this === other) {
+        return true
+    }
+    if (other is Set<*>) {
+        if (size != other.size) {
+            return false
+        }
+        try {
+            for (i in 0 until _size) {
+                val mine = valueAt(i)
+                if (!other.contains(mine)) {
+                    return false
+                }
+            }
+        } catch (ignored: NullPointerException) {
+            return false
+        } catch (ignored: ClassCastException) {
+            return false
+        }
+        return true
+    }
+    return false
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.hashCodeInternal(): Int {
+    val hashes = hashes
+    val s = _size
+    var result = 0
+    for (i in 0 until s) {
+        result += hashes[i]
+    }
+    return result
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.toStringInternal(): String {
+    if (isEmpty()) {
+        return "{}"
+    }
+
+    return buildString(capacity = _size * 14) {
+        append('{')
+        for (i in 0 until _size) {
+            if (i > 0) {
+                append(", ")
+            }
+            val value = valueAt(i)
+            if (value !== this@toStringInternal) {
+                append(value)
+            } else {
+                append("(this Set)")
             }
         }
-        return removed
+        append('}')
     }
+}
 
-    private companion object {
-        private const val DEBUG = false
-        private const val TAG = "ArraySet"
-
-        /**
-         * The minimum amount by which the capacity of a ArraySet will increase.
-         * This is tuned to be relatively space-efficient.
-         */
-        private const val BASE_SIZE = 4
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.containsAllInternal(elements: Collection<E>): Boolean {
+    for (item in elements) {
+        if (!contains(item)) {
+            return false
+        }
     }
+    return true
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.addAllInternal(elements: Collection<E>): Boolean {
+    ensureCapacity(_size + elements.size)
+    var added = false
+    for (value in elements) {
+        added = add(value) or added
+    }
+    return added
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.removeAllInternal(elements: Collection<E>): Boolean {
+    var removed = false
+    for (value in elements) {
+        removed = removed or remove(value)
+    }
+    return removed
+}
+
+@Suppress("NOTHING_TO_INLINE")
+internal inline fun <E> ArraySet<E>.retainAllInternal(elements: Collection<E>): Boolean {
+    var removed = false
+    for (i in _size - 1 downTo 0) {
+        if (array[i] !in elements) {
+            removeAt(i)
+            removed = true
+        }
+    }
+    return removed
 }
diff --git a/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt b/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
index 365694f..257a876 100644
--- a/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
+++ b/collection/collection/src/commonTest/kotlin/androidx/collection/ArraySetTest.kt
@@ -35,7 +35,6 @@
 import kotlinx.coroutines.runBlocking
 
 internal class ArraySetTest {
-
     private val set = ArraySet<String>()
 
     /**
diff --git a/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.java b/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.java
new file mode 100644
index 0000000..13642b6
--- /dev/null
+++ b/collection/collection/src/jvmMain/java/androidx/collection/ArraySetJvmUtil.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 androidx.collection;
+
+import java.lang.reflect.Array;
+
+class ArraySetJvmUtil {
+    private ArraySetJvmUtil() {
+    }
+
+    // Necessary to implement in Java to allow allocating a typed array without a callback for
+    // initialization. We also need to ignore the nullity of the type in order to null out the
+    // (n+1)'th item for behavior compatibility.
+    @SuppressWarnings("unchecked")
+    static <T> T[] resizeForToArray(T[] destination, int size) {
+        if (destination.length < size) {
+            return (T[]) Array.newInstance(destination.getClass().getComponentType(), size);
+        } else {
+            if (destination.length > size) {
+                destination[size] = null;
+            }
+            return destination;
+        }
+    }
+}
diff --git a/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt b/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt
new file mode 100644
index 0000000..e804ff1
--- /dev/null
+++ b/collection/collection/src/jvmMain/kotlin/androidx/collection/ArraySet.jvm.kt
@@ -0,0 +1,302 @@
+/*
+ * 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.collection
+
+import androidx.collection.internal.EMPTY_INTS
+import androidx.collection.internal.EMPTY_OBJECTS
+
+/**
+ * ArraySet is a generic set data structure that is designed to be more memory efficient than a
+ * traditional [HashSet].  The design is very similar to
+ * [ArrayMap], with all of the caveats described there.  This implementation is
+ * separate from ArrayMap, however, so the Object array contains only one item for each
+ * entry in the set (instead of a pair for a mapping).
+ *
+ * Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items.  It is generally slower than a traditional
+ * HashSet, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array.  For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ * Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it.  Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size.  In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.
+ *
+ * This structure is **NOT** thread-safe.
+ *
+ * @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+public actual class ArraySet<E>
+// TODO(b/237405792): Default value for optional argument is required here to workaround Metalava's
+//  lack of support for expect / actual.
+@Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
+// TODO(b/237405286): @JvmOverloads is redundant in this actual, but is necessary here to workaround
+//  Metalava's lack of support for expect / actual.
+@JvmOverloads actual constructor(capacity: Int = 0) : MutableCollection<E>, MutableSet<E> {
+    internal actual var hashes: IntArray = EMPTY_INTS
+    internal actual var array: Array<Any?> = EMPTY_OBJECTS
+
+    internal actual var _size = 0
+    actual override val size: Int
+        get() = _size
+
+    /**
+     * Create a new ArraySet with the mappings from the given ArraySet.
+     */
+    public actual constructor(set: ArraySet<out E>?) : this(capacity = 0) {
+        if (set != null) {
+            addAll(set)
+        }
+    }
+
+    /**
+     * Create a new ArraySet with the mappings from the given [Collection].
+     */
+    public actual constructor(set: Collection<E>?) : this(capacity = 0) {
+        if (set != null) {
+            addAll(set)
+        }
+    }
+
+    /**
+     * Create a new ArraySet with items from the given array.
+     */
+    public actual constructor(array: Array<out E>?) : this(capacity = 0) {
+        if (array != null) {
+            for (value in array) {
+                add(value)
+            }
+        }
+    }
+
+    init {
+        if (capacity > 0) {
+            allocArrays(capacity)
+        }
+    }
+
+    /**
+     * Make the array map empty.  All storage is released.
+     *
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    actual override fun clear() {
+        clearInternal()
+    }
+
+    /**
+     * Ensure the array map can hold at least [minimumCapacity]
+     * items.
+     *
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun ensureCapacity(minimumCapacity: Int) {
+        ensureCapacityInternal(minimumCapacity)
+    }
+
+    /**
+     * Check whether a value exists in the set.
+     *
+     * @param element The value to search for.
+     * @return Returns true if the value exists, else false.
+     */
+    actual override operator fun contains(element: E): Boolean {
+        return containsInternal(element)
+    }
+
+    /**
+     * Returns the index of a value in the set.
+     *
+     * @param key The value to search for.
+     * @return Returns the index of the value if it exists, else a negative integer.
+     */
+    public actual fun indexOf(key: Any?): Int {
+        return indexOfInternal(key)
+    }
+
+    /**
+     * Return the value at the given index in the array.
+     *
+     * @param index The desired index, must be between 0 and [size]-1.
+     * @return Returns the value stored at the given index.
+     */
+    public actual fun valueAt(index: Int): E {
+        return valueAtInternal(index)
+    }
+
+    /**
+     * Return `true` if the array map contains no items.
+     */
+    actual override fun isEmpty(): Boolean {
+        return isEmptyInternal()
+    }
+
+    /**
+     * Adds the specified object to this set. The set is not modified if it
+     * already contains the object.
+     *
+     * @param element the object to add.
+     * @return `true` if this set is modified, `false` otherwise.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    actual override fun add(element: E): Boolean {
+        return addInternal(element)
+    }
+
+    /**
+     * Perform a [add] of all values in [array]
+     *
+     * @param array The array whose contents are to be retrieved.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun addAll(array: ArraySet<out E>) {
+        addAllInternal(array)
+    }
+
+    /**
+     * Removes the specified object from this set.
+     *
+     * @param element the object to remove.
+     * @return `true` if this set was modified, `false` otherwise.
+     */
+    actual override fun remove(element: E): Boolean {
+        return removeInternal(element)
+    }
+
+    /**
+     * Remove the key/value mapping at the given index.
+     *
+     * @param index The desired index, must be between 0 and [size]-1.
+     * @return Returns the value that was stored at this index.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun removeAt(index: Int): E {
+        return removeAtInternal(index)
+    }
+
+    /**
+     * Perform a [remove] of all values in [array]
+     *
+     * @param array The array whose contents are to be removed.
+     */
+    public actual fun removeAll(array: ArraySet<out E>): Boolean {
+        return removeAllInternal(array)
+    }
+
+    @Suppress("ArrayReturn")
+    public fun toArray(): Array<Any?> {
+        return array.copyOfRange(fromIndex = 0, toIndex = _size)
+    }
+
+    @Suppress("ArrayReturn")
+    public fun <T> toArray(array: Array<T>): Array<T> {
+        val result = ArraySetJvmUtil.resizeForToArray(array, _size)
+
+        @Suppress("UNCHECKED_CAST")
+        this.array.copyInto(result as Array<Any?>, 0, 0, _size)
+        return result
+    }
+
+    /**
+     * This implementation returns false if the object is not a set, or
+     * if the sets have different sizes.  Otherwise, for each value in this
+     * set, it checks to make sure the value also exists in the other set.
+     * If any value doesn't exist, the method returns false; otherwise, it
+     * returns true.
+     *
+     * @see Any.equals
+     */
+    actual override fun equals(other: Any?): Boolean {
+        return equalsInternal(other)
+    }
+
+    /**
+     * @see Any.hashCode
+     */
+    actual override fun hashCode(): Int {
+        return hashCodeInternal()
+    }
+
+    /**
+     * This implementation composes a string by iterating over its values. If
+     * this set contains itself as a value, the string "(this Set)"
+     * will appear in its place.
+     */
+    actual override fun toString(): String {
+        return toStringInternal()
+    }
+
+    /**
+     * Return a [MutableIterator] over all values in the set.
+     *
+     * **Note:** this is a less efficient way to access the array contents compared to
+     * looping from 0 until [size] and calling [valueAt].
+     */
+    actual override fun iterator(): MutableIterator<E> = ElementIterator()
+
+    private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
+        override fun elementAt(index: Int): E = valueAt(index)
+
+        override fun removeAt(index: Int) {
+            this@ArraySet.removeAt(index)
+        }
+    }
+
+    /**
+     * Determine if the array set contains all of the values in the given collection.
+     *
+     * @param elements The collection whose contents are to be checked against.
+     * @return Returns true if this array set contains a value for every entry
+     * in [elements] else returns false.
+     */
+    actual override fun containsAll(elements: Collection<E>): Boolean {
+        return containsAllInternal(elements)
+    }
+
+    /**
+     * Perform an [add] of all values in [elements]
+     *
+     * @param elements The collection whose contents are to be retrieved.
+     */
+    actual override fun addAll(elements: Collection<E>): Boolean {
+        return addAllInternal(elements)
+    }
+
+    /**
+     * Remove all values in the array set that exist in the given collection.
+     *
+     * @param elements The collection whose contents are to be used to remove values.
+     * @return Returns true if any values were removed from the array set, else false.
+     */
+    actual override fun removeAll(elements: Collection<E>): Boolean {
+        return removeAllInternal(elements)
+    }
+
+    /**
+     * Remove all values in the array set that do **not** exist in the given collection.
+     *
+     * @param elements The collection whose contents are to be used to determine which
+     * values to keep.
+     * @return Returns true if any values were removed from the array set, else false.
+     */
+    actual override fun retainAll(elements: Collection<E>): Boolean {
+        return retainAllInternal(elements)
+    }
+}
diff --git a/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt
new file mode 100644
index 0000000..df77ccc
--- /dev/null
+++ b/collection/collection/src/jvmTest/kotlin/androidx/collection/ArraySetJvmTest.kt
@@ -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.collection
+
+import kotlin.test.assertNull
+import org.junit.Test
+
+class ArraySetJvmTest {
+
+    @Test
+    fun toArray_emptyTypedDestination() {
+        val set = ArraySet<Int>()
+        for (i in 0..5) {
+            set.add(i)
+        }
+
+        // Forces casting, otherwise may not pick up certain failures. Typing just the destination
+        // array or return type is not sufficient to test on JVM.
+        @Suppress("UNUSED_VARIABLE")
+        val result: Array<Int> = set.toArray(emptyArray())
+    }
+
+    @Test
+    fun toArray_nullsLastElement() {
+        val set = ArraySet<Int>()
+        for (i in 0..4) {
+            set.add(i)
+        }
+
+        val result: Array<Int> = set.toArray(Array(10) { -1 })
+        assertNull(result[5])
+    }
+}
\ No newline at end of file
diff --git a/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
new file mode 100644
index 0000000..9c7be56
--- /dev/null
+++ b/collection/collection/src/nativeMain/kotlin/androidx/collection/ArraySet.native.kt
@@ -0,0 +1,285 @@
+/*
+ * 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.collection
+
+import androidx.collection.internal.EMPTY_INTS
+import androidx.collection.internal.EMPTY_OBJECTS
+
+/**
+ * ArraySet is a generic set data structure that is designed to be more memory efficient than a
+ * traditional [HashSet].  The design is very similar to
+ * [ArrayMap], with all of the caveats described there.  This implementation is
+ * separate from ArrayMap, however, so the Object array contains only one item for each
+ * entry in the set (instead of a pair for a mapping).
+ *
+ * Note that this implementation is not intended to be appropriate for data structures
+ * that may contain large numbers of items.  It is generally slower than a traditional
+ * HashSet, since lookups require a binary search and adds and removes require inserting
+ * and deleting entries in the array.  For containers holding up to hundreds of items,
+ * the performance difference is not significant, less than 50%.
+ *
+ * Because this container is intended to better balance memory use, unlike most other
+ * standard Java containers it will shrink its array as items are removed from it.  Currently
+ * you have no control over this shrinking -- if you set a capacity and then remove an
+ * item, it may reduce the capacity to better match the current size.  In the future an
+ * explicit call to set the capacity should turn off this aggressive shrinking behavior.
+ *
+ * This structure is **NOT** thread-safe.
+ *
+ * @constructor Creates a new empty ArraySet. The default capacity of an array map is 0, and
+ * will grow once items are added to it.
+ */
+public actual class ArraySet<E> actual constructor(
+    capacity: Int
+) : MutableCollection<E>, MutableSet<E> {
+
+    internal actual var hashes: IntArray = EMPTY_INTS
+    internal actual var array: Array<Any?> = EMPTY_OBJECTS
+
+    internal actual var _size = 0
+    actual override val size: Int
+        get() = _size
+
+    /**
+     * Create a new ArraySet with the mappings from the given ArraySet.
+     */
+    public actual constructor(set: ArraySet<out E>?) : this(capacity = 0) {
+        if (set != null) {
+            addAll(set)
+        }
+    }
+
+    /**
+     * Create a new ArraySet with the mappings from the given [Collection].
+     */
+    public actual constructor(set: Collection<E>?) : this(capacity = 0) {
+        if (set != null) {
+            addAll(set)
+        }
+    }
+
+    /**
+     * Create a new ArraySet with items from the given array.
+     */
+    public actual constructor(array: Array<out E>?) : this(capacity = 0) {
+        if (array != null) {
+            for (value in array) {
+                add(value)
+            }
+        }
+    }
+
+    init {
+        if (capacity > 0) {
+            allocArrays(capacity)
+        }
+    }
+
+    /**
+     * Make the array map empty.  All storage is released.
+     *
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    actual override fun clear() {
+        clearInternal()
+    }
+
+    /**
+     * Ensure the array map can hold at least [minimumCapacity]
+     * items.
+     *
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun ensureCapacity(minimumCapacity: Int) {
+        ensureCapacityInternal(minimumCapacity)
+    }
+
+    /**
+     * Check whether a value exists in the set.
+     *
+     * @param element The value to search for.
+     * @return Returns true if the value exists, else false.
+     */
+    actual override operator fun contains(element: E): Boolean {
+        return containsInternal(element)
+    }
+
+    /**
+     * Returns the index of a value in the set.
+     *
+     * @param key The value to search for.
+     * @return Returns the index of the value if it exists, else a negative integer.
+     */
+    public actual fun indexOf(key: Any?): Int {
+        return indexOfInternal(key)
+    }
+
+    /**
+     * Return the value at the given index in the array.
+     *
+     * @param index The desired index, must be between 0 and [size]-1.
+     * @return Returns the value stored at the given index.
+     */
+    public actual fun valueAt(index: Int): E {
+        return valueAtInternal(index)
+    }
+
+    /**
+     * Return `true` if the array map contains no items.
+     */
+    actual override fun isEmpty(): Boolean {
+        return isEmptyInternal()
+    }
+
+    /**
+     * Adds the specified object to this set. The set is not modified if it
+     * already contains the object.
+     *
+     * @param element the object to add.
+     * @return `true` if this set is modified, `false` otherwise.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    actual override fun add(element: E): Boolean {
+        return addInternal(element)
+    }
+
+    /**
+     * Perform a [add] of all values in [array]
+     *
+     * @param array The array whose contents are to be retrieved.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun addAll(array: ArraySet<out E>) {
+        addAllInternal(array)
+    }
+
+    /**
+     * Removes the specified object from this set.
+     *
+     * @param element the object to remove.
+     * @return `true` if this set was modified, `false` otherwise.
+     */
+    actual override fun remove(element: E): Boolean {
+        return removeInternal(element)
+    }
+
+    /**
+     * Remove the key/value mapping at the given index.
+     *
+     * @param index The desired index, must be between 0 and [size]-1.
+     * @return Returns the value that was stored at this index.
+     * @throws ConcurrentModificationException if concurrent modifications detected.
+     */
+    public actual fun removeAt(index: Int): E {
+        return removeAtInternal(index)
+    }
+
+    /**
+     * Perform a [remove] of all values in [array]
+     *
+     * @param array The array whose contents are to be removed.
+     */
+    public actual fun removeAll(array: ArraySet<out E>): Boolean {
+        return removeAllInternal(array)
+    }
+
+    /**
+     * This implementation returns false if the object is not a set, or
+     * if the sets have different sizes.  Otherwise, for each value in this
+     * set, it checks to make sure the value also exists in the other set.
+     * If any value doesn't exist, the method returns false; otherwise, it
+     * returns true.
+     *
+     * @see Any.equals
+     */
+    actual override fun equals(other: Any?): Boolean {
+        return equalsInternal(other)
+    }
+
+    /**
+     * @see Any.hashCode
+     */
+    actual override fun hashCode(): Int {
+        return hashCodeInternal()
+    }
+
+    /**
+     * This implementation composes a string by iterating over its values. If
+     * this set contains itself as a value, the string "(this Set)"
+     * will appear in its place.
+     */
+    actual override fun toString(): String {
+        return toStringInternal()
+    }
+
+    /**
+     * Return a [MutableIterator] over all values in the set.
+     *
+     * **Note:** this is a less efficient way to access the array contents compared to
+     * looping from 0 until [size] and calling [valueAt].
+     */
+    actual override fun iterator(): MutableIterator<E> = ElementIterator()
+
+    private inner class ElementIterator : IndexBasedArrayIterator<E>(_size) {
+        override fun elementAt(index: Int): E = valueAt(index)
+
+        override fun removeAt(index: Int) {
+            this@ArraySet.removeAt(index)
+        }
+    }
+
+    /**
+     * Determine if the array set contains all of the values in the given collection.
+     *
+     * @param elements The collection whose contents are to be checked against.
+     * @return Returns true if this array set contains a value for every entry
+     * in [elements] else returns false.
+     */
+    actual override fun containsAll(elements: Collection<E>): Boolean {
+        return containsAllInternal(elements)
+    }
+
+    /**
+     * Perform an [add] of all values in [elements]
+     *
+     * @param elements The collection whose contents are to be retrieved.
+     */
+    actual override fun addAll(elements: Collection<E>): Boolean {
+        return addAllInternal(elements)
+    }
+
+    /**
+     * Remove all values in the array set that exist in the given collection.
+     *
+     * @param elements The collection whose contents are to be used to remove values.
+     * @return Returns true if any values were removed from the array set, else false.
+     */
+    actual override fun removeAll(elements: Collection<E>): Boolean {
+        return removeAll(elements)
+    }
+
+    /**
+     * Remove all values in the array set that do **not** exist in the given collection.
+     *
+     * @param elements The collection whose contents are to be used to determine which
+     * values to keep.
+     * @return Returns true if any values were removed from the array set, else false.
+     */
+    actual override fun retainAll(elements: Collection<E>): Boolean {
+        return retainAllInternal(elements)
+    }
+}
diff --git a/compose/animation/animation-core/api/public_plus_experimental_current.txt b/compose/animation/animation-core/api/public_plus_experimental_current.txt
index 1fd90c0..7198311 100644
--- a/compose/animation/animation-core/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-core/api/public_plus_experimental_current.txt
@@ -339,7 +339,7 @@
     property public static final androidx.compose.animation.core.Easing LinearOutSlowInEasing;
   }
 
-  @kotlin.RequiresOptIn(message="This is an experimental animation API for Transition. It may change in the future.") public @interface ExperimentalTransitionApi {
+  @kotlin.RequiresOptIn(message="This is an experimental animation API for Transition. It may change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTransitionApi {
   }
 
   public interface FiniteAnimationSpec<T> extends androidx.compose.animation.core.AnimationSpec<T> {
@@ -424,7 +424,7 @@
     method @androidx.compose.runtime.Composable public static androidx.compose.animation.core.InfiniteTransition rememberInfiniteTransition();
   }
 
-  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface InternalAnimationApi {
+  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface InternalAnimationApi {
   }
 
   @androidx.compose.runtime.Immutable public final class KeyframesSpec<T> implements androidx.compose.animation.core.DurationBasedAnimationSpec<T> {
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
index 9972083..8fd3df7 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/ExperimentalTransitionApi.kt
@@ -19,4 +19,5 @@
 @RequiresOptIn(
     message = "This is an experimental animation API for Transition. It may change in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTransitionApi
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
index 952630f..d87eb4c 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/InternalAnimationApi.kt
@@ -21,4 +21,5 @@
     AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
     AnnotationTarget.FIELD, AnnotationTarget.PROPERTY_GETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalAnimationApi
\ No newline at end of file
diff --git a/compose/animation/animation-graphics/api/public_plus_experimental_current.txt b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
index f650e30..08a1623 100644
--- a/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation-graphics/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.compose.animation.graphics {
 
-  @kotlin.RequiresOptIn(message="This is an experimental animation graphics API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface ExperimentalAnimationGraphicsApi {
+  @kotlin.RequiresOptIn(message="This is an experimental animation graphics API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface ExperimentalAnimationGraphicsApi {
   }
 
 }
diff --git a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
index 0dc6ac5..79068b1 100644
--- a/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
+++ b/compose/animation/animation-graphics/src/commonMain/kotlin/androidx/compose/animation/graphics/ExperimentalAnimationGraphicsApi.kt
@@ -18,4 +18,5 @@
 
 @RequiresOptIn(message = "This is an experimental animation graphics API.")
 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY)
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalAnimationGraphicsApi
diff --git a/compose/animation/animation/api/public_plus_experimental_current.txt b/compose/animation/animation/api/public_plus_experimental_current.txt
index 90ce764..a5aedf1 100644
--- a/compose/animation/animation/api/public_plus_experimental_current.txt
+++ b/compose/animation/animation/api/public_plus_experimental_current.txt
@@ -132,7 +132,7 @@
     property public final androidx.compose.animation.ExitTransition None;
   }
 
-  @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
+  @kotlin.RequiresOptIn(message="This is an experimental animation API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalAnimationApi {
   }
 
   public final class FlingCalculatorKt {
diff --git a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
index 33ed868..742fcbe 100644
--- a/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
+++ b/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/EnterExitTransition.kt
@@ -62,6 +62,7 @@
     AnnotationTarget.FIELD,
     AnnotationTarget.PROPERTY_GETTER,
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalAnimationApi
 
 /**
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 db43f57..6843730 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
@@ -262,7 +262,12 @@
                     }
                 }
             }
-        """.trimIndent()
+
+            inline fun <T> Identity(block: () -> T): T = block()
+
+            @Composable
+            fun Stack(content: @Composable () -> Unit) = content()
+        """
     )
 
     @Test
@@ -287,6 +292,7 @@
             fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(Test)<A()>,<M3>,<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
@@ -302,6 +308,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (condition) {
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -359,6 +366,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 tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(a)) 0b0100 else 0b0010
@@ -377,6 +385,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (a) {
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -397,6 +406,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (b) {
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -456,12 +466,14 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 A(%composer, 0)
+                val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (condition) {
+                      %composer.endToMarker(tmp0_marker)
                     }
                     A(%composer, 0)
                   } else {
@@ -501,6 +513,7 @@
               T {
                 %this%T.compose(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
                   sourceInformation(%composer, "C<M1>:Test.kt")
+                  val tmp0_marker = %composer.currentMarker
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     if (isTraceInProgress()) {
                       traceEventStart(<>, %changed, -1, <>)
@@ -510,6 +523,7 @@
                       sourceInformation(%composer, "C:Test.kt")
                       if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                         if (condition) {
+                          %composer.endToMarker(tmp0_marker)
                           if (isTraceInProgress()) {
                             traceEventEnd()
                           }
@@ -584,11 +598,13 @@
                   sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
+                    val tmp0_marker = %composer.currentMarker
                     M1({ %composer: Composer?, %changed: Int ->
                       %composer.startReplaceableGroup(<>)
                       sourceInformation(%composer, "C:Test.kt")
                       if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                         if (condition) {
+                          %composer.endToMarker(tmp0_marker)
                         }
                       } else {
                         %composer.skipToGroupEnd()
@@ -647,6 +663,7 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 A(%composer, 0)
+                val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<A()>,<M1>,<A()>:Test.kt")
@@ -657,6 +674,7 @@
                       sourceInformation(%composer, "C:Test.kt")
                       if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                         if (condition) {
+                          %composer.endToMarker(tmp0_marker)
                         }
                       } else {
                         %composer.skipToGroupEnd()
@@ -709,6 +727,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 tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -728,8 +747,7 @@
                     while (true) {
                       A(%composer, 0)
                       if (condition) {
-                        %composer.endReplaceableGroup()
-                        %composer.endReplaceableGroup()
+                        %composer.endToMarker(tmp0_marker)
                         if (isTraceInProgress()) {
                           traceEventEnd()
                         }
@@ -799,12 +817,14 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 A(%composer, 0)
+                val tmp0_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (condition) {
+                      %composer.endToMarker(tmp0_marker)
                     }
                     A(%composer, 0)
                   } else {
@@ -812,12 +832,14 @@
                   }
                   %composer.endReplaceableGroup()
                 }, %composer, 0)
+                val tmp1_marker = %composer.currentMarker
                 M3({ %composer: Composer?, %changed: Int ->
                   %composer.startReplaceableGroup(<>)
                   sourceInformation(%composer, "C<A()>,<A()>:Test.kt")
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (condition) {
+                      %composer.endToMarker(tmp1_marker)
                     }
                     A(%composer, 0)
                   } else {
@@ -865,6 +887,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 tmp0_marker = %composer.currentMarker
               val %dirty = %changed
               if (%changed and 0b1110 === 0) {
                 %dirty = %dirty or if (%composer.changed(condition)) 0b0100 else 0b0010
@@ -888,8 +911,7 @@
                         sourceInformation(%composer, "C<Text("...>:Test.kt")
                         if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                           Text("In CCM1", %composer, 0b0110)
-                          %composer.endReplaceableGroup()
-                          %composer.endReplaceableGroup()
+                          %composer.endToMarker(tmp0_marker)
                           if (isTraceInProgress()) {
                             traceEventEnd()
                           }
@@ -951,11 +973,13 @@
               if (isTraceInProgress()) {
                 traceEventStart(<>, %changed, -1, <>)
               }
+              val tmp0_marker = %composer.currentMarker
               FakeBox({ %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 {
@@ -1127,11 +1151,13 @@
                 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 {
@@ -1153,6 +1179,178 @@
     )
 
     @Test
+    fun testVerifyEarlyExitFromNonComposable() = verifyInlineReturn(
+        source = """
+            @Composable
+            fun Test(condition: Boolean) {
+                Text("Some text")
+                Identity {
+                    if (condition) return@Test
+                }
+                Text("Some more text")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<Text("...>,<Text("...>: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, <>)
+                }
+                Text("Some text", %composer, 0b0110)
+                Identity {
+                  if (condition) {
+                    if (isTraceInProgress()) {
+                      traceEventEnd()
+                    }
+                    %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                      Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                    }
+                    return
+                  }
+                }
+                Text("Some more text", %composer, 0b0110)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testVerifyEarlyExitFromNonComposable_M1() = verifyInlineReturn(
+        source = """
+            @Composable
+            fun Test(condition: Boolean) {
+                Text("Some text")
+                M1 {
+                    Identity {
+                        if (condition) return@Test
+                    }
+                }
+                Text("Some more text")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<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("Some text", %composer, 0b0110)
+                M1({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C:Test.kt")
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                    Identity {
+                      if (condition) {
+                        %composer.endToMarker(tmp0_marker)
+                        if (isTraceInProgress()) {
+                          traceEventEnd()
+                        }
+                        %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                          Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+                        }
+                        return
+                      }
+                    }
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("Some more text", %composer, 0b0110)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(condition, %composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+        """
+    )
+
+    @Test
+    fun testVerifyEarlyExitFromNonComposable_M1_RM1() = verifyInlineReturn(
+        source = """
+            @Composable
+            fun Test(condition: Boolean) {
+                Text("Some text")
+                M1 {
+                    Identity {
+                        if (condition) return@M1
+                    }
+                }
+                Text("Some more text")
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<Text("...>,<M1>,<Text("...>: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, <>)
+                }
+                Text("Some text", %composer, 0b0110)
+                val tmp0_marker = %composer.currentMarker
+                M1({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C:Test.kt")
+                  if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                    Identity {
+                      if (condition) {
+                        %composer.endToMarker(tmp0_marker)
+                      }
+                    }
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                Text("Some more text", %composer, 0b0110)
+                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(
             """
@@ -1203,6 +1401,7 @@
             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
@@ -1218,6 +1417,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     Text("M1 - before", %composer, 0b0110)
                     if (condition) {
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
@@ -5828,4 +6028,4 @@
             }
         """
     )
-}
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
index 1fe4e9a..1f5756b 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ControlFlowTransformTestsNoSource.kt
@@ -155,11 +155,13 @@
                   traceEventStart(<>, %changed, -1, <>)
                 }
                 IW({ %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                   } else {
                     %composer.skipToGroupEnd()
                   }
+                  %composer.endReplaceableGroup()
                 }, %composer, 0)
                 if (isTraceInProgress()) {
                   traceEventEnd()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
index 656fb00..cbaf15d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/FunctionBodySkippingTransformTests.kt
@@ -3882,6 +3882,77 @@
             }
         """
     )
+
+    @Test // regression test for 204897513
+    fun test_InlineForLoop() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Test() {
+                Bug(listOf(1, 2, 3)) {
+                    Text(it.toString())
+                }
+            }
+
+            @Composable
+            inline fun <T> Bug(items: List<T>, content: @Composable (item: T) -> Unit) {
+                for (item in items) content(item)
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            fun Test(%composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              sourceInformation(%composer, "C(Test)<Bug(li...>:Test.kt")
+              if (%changed !== 0 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  sourceInformation(%composer, "C<Text(i...>:Test.kt")
+                  val %dirty = %changed
+                  if (%changed and 0b1110 === 0) {
+                    %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
+                  }
+                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
+                    Text(it.toString(), %composer, 0)
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(%composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+            @Composable
+            @ComposableInferredTarget(scheme = "[0[0]]")
+            fun <T> Bug(items: List<T>, content: Function3<@[ParameterName(name = 'item')] T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
+              %composer.startReplaceableGroup(<>)
+              sourceInformation(%composer, "C(Bug)P(1)*<conten...>:Test.kt")
+              val tmp0_iterator = items.iterator()
+              while (tmp0_iterator.hasNext()) {
+                val item = tmp0_iterator.next()
+                content(item, %composer, 0b01110000 and %changed)
+              }
+              %composer.endReplaceableGroup()
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) {}
+        """
+    )
 }
 
 class FunctionBodySkippingTransformTestsNoSource : FunctionBodySkippingTransformTestsBase() {
@@ -3945,4 +4016,72 @@
             }
         """
     )
+
+    @Test // regression test for 204897513
+    fun test_InlineForLoop_no_source_info() = verifyComposeIrTransform(
+        source = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            private fun Test() {
+                Bug(listOf(1, 2, 3)) {
+                    Text(it.toString())
+                }
+            }
+
+            @Composable
+            private inline fun <T> Bug(items: List<T>, content: @Composable (item: T) -> Unit) {
+                for (item in items) content(item)
+            }
+        """,
+        expectedTransformed = """
+            @Composable
+            private fun Test(%composer: Composer?, %changed: Int) {
+              %composer = %composer.startRestartGroup(<>)
+              if (%changed !== 0 || !%composer.skipping) {
+                if (isTraceInProgress()) {
+                  traceEventStart(<>, %changed, -1, <>)
+                }
+                Bug(listOf(1, 2, 3), { it: Int, %composer: Composer?, %changed: Int ->
+                  %composer.startReplaceableGroup(<>)
+                  val %dirty = %changed
+                  if (%changed and 0b1110 === 0) {
+                    %dirty = %dirty or if (%composer.changed(it)) 0b0100 else 0b0010
+                  }
+                  if (%dirty and 0b01011011 !== 0b00010010 || !%composer.skipping) {
+                    Text(it.toString(), %composer, 0)
+                  } else {
+                    %composer.skipToGroupEnd()
+                  }
+                  %composer.endReplaceableGroup()
+                }, %composer, 0)
+                if (isTraceInProgress()) {
+                  traceEventEnd()
+                }
+              } else {
+                %composer.skipToGroupEnd()
+              }
+              %composer.endRestartGroup()?.updateScope { %composer: Composer?, %force: Int ->
+                Test(%composer, updateChangedFlags(%changed or 0b0001))
+              }
+            }
+            @Composable
+            @ComposableInferredTarget(scheme = "[0[0]]")
+            private fun <T> Bug(items: List<T>, content: Function3<@[ParameterName(name = 'item')] T, Composer, Int, Unit>, %composer: Composer?, %changed: Int) {
+              %composer.startReplaceableGroup(<>)
+              val tmp0_iterator = items.iterator()
+              while (tmp0_iterator.hasNext()) {
+                val item = tmp0_iterator.next()
+                content(item, %composer, 0b01110000 and %changed)
+              }
+              %composer.endReplaceableGroup()
+            }
+        """,
+        extra = """
+            import androidx.compose.runtime.*
+
+            @Composable
+            fun Text(value: String) {}
+        """
+    )
 }
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 e8ffd3f..d085612 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,6 +118,7 @@
             fun Test(condition: Boolean, %composer: Composer?, %changed: Int) {
               %composer = %composer.startRestartGroup(<>)
               sourceInformation(%composer, "C(Test)<A()>,<Wrappe...>,<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
@@ -133,6 +134,7 @@
                   if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
                     A(%composer, 0)
                     if (!condition) {
+                      %composer.endToMarker(tmp0_marker)
                       if (isTraceInProgress()) {
                         traceEventEnd()
                       }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index f465c25..0e0b663 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -201,7 +201,7 @@
 
     companion object {
         fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
-            val KOTLIN_VERSION_EXPECTATION = "1.7.20"
+            val KOTLIN_VERSION_EXPECTATION = "1.7.21"
             KotlinCompilerVersion.getVersion()?.let { version ->
                 val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
                 val suppressKotlinVersionCheck = configuration.get(
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 b29e3df..096d0f6 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
@@ -96,8 +96,10 @@
             8601 to "1.3.0-rc02",
             8602 to "1.3.0",
             8603 to "1.3.1",
+            8604 to "1.3.2",
             9000 to "1.4.0-alpha01",
             9001 to "1.4.0-alpha02",
+            9100 to "1.4.0-alpha03",
         )
 
         /**
@@ -110,7 +112,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.4.0-alpha01"
+        const val compilerVersion: String = "1.4.0-alpha02"
         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 79b1f93c..3541e35 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
@@ -616,9 +616,8 @@
             }
     }
 
-    private val rollbackGroupMarkerEnabled get() = false
-        // Temporarily disabled for b/255722247
-        // currentMarkerProperty != null && endToMarkerFunction != null
+    private val rollbackGroupMarkerEnabled get() =
+        currentMarkerProperty != null && endToMarkerFunction != null
 
     private val endRestartGroupFunction by guardedLazy {
         composerIrClass
@@ -1092,7 +1091,7 @@
                 body.startOffset,
                 body.endOffset,
                 listOfNotNull(
-                    if (collectSourceInformation && scope.isInlinedLambda)
+                    if (scope.isInlinedLambda)
                         irStartReplaceableGroup(body, scope, irFunctionSourceKey())
                     else null,
                     *sourceInformationPreamble.statements.toTypedArray(),
@@ -1100,9 +1099,7 @@
                     *skipPreamble.statements.toTypedArray(),
                     *bodyPreamble.statements.toTypedArray(),
                     transformedBody,
-                    if (collectSourceInformation && scope.isInlinedLambda)
-                        irEndReplaceableGroup()
-                    else null,
+                    if (scope.isInlinedLambda) irEndReplaceableGroup() else null,
                     returnVar?.let { irReturn(declaration.symbol, irGet(it)) }
                 )
             )
@@ -2633,7 +2630,8 @@
 
                         break@loop
                     }
-                    if (scope.isInlinedLambda) leavingInlinedLambda = true
+                    if (scope.isInlinedLambda && scope.inComposableCall)
+                        leavingInlinedLambda = true
                 }
                 is Scope.BlockScope -> {
                     blockScopeMarks.add(scope)
diff --git a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
index f788920..f586629 100644
--- a/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation-layout/api/public_plus_experimental_current.txt
@@ -111,7 +111,7 @@
     method @androidx.compose.runtime.Stable public androidx.compose.ui.Modifier weight(androidx.compose.ui.Modifier, float weight, optional boolean fill);
   }
 
-  @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") public @interface ExperimentalLayoutApi {
+  @kotlin.RequiresOptIn(message="The API of this layout is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalLayoutApi {
   }
 
   public final class IntrinsicKt {
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
index 4b2c4f5..c9ae79f 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/ExperimentalLayoutApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.foundation.layout
 
 @RequiresOptIn("The API of this layout is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalLayoutApi
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 28d77a3..69436b3 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -139,6 +139,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollTo(int value, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
     property public boolean isScrollInProgress;
     property public final int maxValue;
@@ -245,8 +247,12 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
     method public float dispatchRawDelta(float delta);
+    method public default boolean getCanScrollBackward();
+    method public default boolean getCanScrollForward();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(optional androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public default boolean canScrollBackward;
+    property public default boolean canScrollForward;
     property public abstract boolean isScrollInProgress;
   }
 
@@ -514,6 +520,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -658,6 +666,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -745,6 +755,16 @@
 
 }
 
+package androidx.compose.foundation.pager {
+
+  public final class PagerKt {
+  }
+
+  public final class PagerStateKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 2b2aca4..bdc1b11 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -66,7 +66,7 @@
     method @Deprecated public static androidx.compose.ui.Modifier excludeFromSystemGesture(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.LayoutCoordinates,androidx.compose.ui.geometry.Rect> exclusion);
   }
 
-  @kotlin.RequiresOptIn(message="This foundation API is experimental and is likely to change or be removed in the " + "future.") public @interface ExperimentalFoundationApi {
+  @kotlin.RequiresOptIn(message="This foundation API is experimental and is likely to change or be removed in the " + "future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalFoundationApi {
   }
 
   public final class FocusableKt {
@@ -103,7 +103,7 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.foundation.Indication> LocalIndication;
   }
 
-  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationApi {
+  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationApi {
   }
 
   public final class MagnifierKt {
@@ -190,6 +190,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollTo(int value, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
     property public boolean isScrollInProgress;
     property public final int maxValue;
@@ -298,8 +300,12 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
     method public float dispatchRawDelta(float delta);
+    method public default boolean getCanScrollBackward();
+    method public default boolean getCanScrollForward();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(optional androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public default boolean canScrollBackward;
+    property public default boolean canScrollForward;
     property public abstract boolean isScrollInProgress;
   }
 
@@ -371,8 +377,8 @@
 
   @androidx.compose.foundation.ExperimentalFoundationApi public interface SnapLayoutInfoProvider {
     method public float calculateApproachOffset(androidx.compose.ui.unit.Density, float initialVelocity);
+    method public float calculateSnapStepSize(androidx.compose.ui.unit.Density);
     method public kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> calculateSnappingOffsetBounds(androidx.compose.ui.unit.Density);
-    method public float snapStepSize(androidx.compose.ui.unit.Density);
   }
 
 }
@@ -583,6 +589,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -728,6 +736,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -944,6 +954,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -977,6 +989,16 @@
 
 }
 
+package androidx.compose.foundation.pager {
+
+  public final class PagerKt {
+  }
+
+  public final class PagerStateKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
@@ -1175,7 +1197,7 @@
     method public static void appendInlineContent(androidx.compose.ui.text.AnnotatedString.Builder, String id, optional String alternateText);
   }
 
-  @kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationTextApi {
+  @kotlin.RequiresOptIn(message="Internal/Unstable API for use only between foundation modules sharing " + "the same exact version, subject to change without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalFoundationTextApi {
   }
 
   public final class KeyEventHelpers_androidKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 28d77a3..69436b3 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -139,6 +139,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollTo(int value, kotlin.coroutines.Continuation<? super java.lang.Float>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
     property public boolean isScrollInProgress;
     property public final int maxValue;
@@ -245,8 +247,12 @@
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface ScrollableState {
     method public float dispatchRawDelta(float delta);
+    method public default boolean getCanScrollBackward();
+    method public default boolean getCanScrollForward();
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(optional androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public default boolean canScrollBackward;
+    property public default boolean canScrollForward;
     property public abstract boolean isScrollInProgress;
   }
 
@@ -514,6 +520,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -658,6 +666,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -745,6 +755,16 @@
 
 }
 
+package androidx.compose.foundation.pager {
+
+  public final class PagerKt {
+  }
+
+  public final class PagerStateKt {
+  }
+
+}
+
 package androidx.compose.foundation.relocation {
 
   public final class BringIntoViewKt {
diff --git a/compose/foundation/foundation/build.gradle b/compose/foundation/foundation/build.gradle
index 1d0a744..dd28bf30 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -36,7 +36,7 @@
          */
         api("androidx.annotation:annotation:1.1.0")
         api("androidx.compose.animation:animation:1.2.1")
-        api("androidx.compose.runtime:runtime:1.3.0-rc01")
+        api("androidx.compose.runtime:runtime:1.3.1")
         api(project(":compose:ui:ui"))
 
         implementation(libs.kotlinStdlibCommon)
@@ -64,6 +64,7 @@
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation "androidx.activity:activity-compose:1.3.1"
+        androidTestImplementation("androidx.core:core:1.9.0")
         androidTestImplementation(libs.espressoCore)
         androidTestImplementation(libs.junit)
         androidTestImplementation(libs.kotlinTest)
@@ -133,6 +134,7 @@
                 implementation(project(":test:screenshot:screenshot"))
                 implementation(project(":internal-testutils-runtime"))
                 implementation("androidx.activity:activity-compose:1.3.1")
+                implementation("androidx.core:core:1.9.0")
 
                 implementation(libs.testUiautomator)
                 implementation(libs.testRules)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
index 024b8f1..14a977b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/FoundationDemos.kt
@@ -25,6 +25,7 @@
 import androidx.compose.foundation.demos.snapping.SnappingDemos
 import androidx.compose.foundation.samples.BringIntoViewResponderSample
 import androidx.compose.foundation.samples.BringPartOfComposableIntoViewSample
+import androidx.compose.foundation.samples.CanScrollSample
 import androidx.compose.foundation.samples.ControlledScrollableRowSample
 import androidx.compose.foundation.samples.CustomTouchSlopSample
 import androidx.compose.foundation.samples.InteractionSourceFlowSample
@@ -52,6 +53,7 @@
     "Foundation",
     listOf(
         ComposableDemo("Draggable, Scrollable, Zoomable, Focusable") { HighLevelGesturesDemo() },
+        ComposableDemo("Can scroll forward / backward") { CanScrollSample() },
         ComposableDemo("Vertical scroll") { VerticalScrollExample() },
         ComposableDemo("Controlled Scrollable Row") { ControlledScrollableRowSample() },
         ComposableDemo("Draw Modifiers") { DrawModifiersDemo() },
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
index 9971ff9..da578d4 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyGridSnapLayoutInfoProvider.kt
@@ -68,7 +68,7 @@
         return distanceFromItemBeforeTarget.rangeTo(distanceFromItemAfterTarget)
     }
 
-    override fun Density.snapStepSize(): Float {
+    override fun Density.calculateSnapStepSize(): Float {
         return if (singleAxisItems.isNotEmpty()) {
             val size = if (layoutInfo.orientation == Orientation.Vertical) {
                 singleAxisItems.sumOf { it.size.height }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
index 197006b..58c29e2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/LazyListSnappingDemos.kt
@@ -27,6 +27,7 @@
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
 
 val LazyListSnappingDemos = listOf(
@@ -42,9 +43,7 @@
 @Composable
 private fun SamePageSizeDemo() {
     val lazyListState = rememberLazyListState()
-    val layoutInfoProvider = remember(lazyListState) {
-        SnapLayoutInfoProvider(lazyListState)
-    }
+    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
     val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(
@@ -59,9 +58,8 @@
 @Composable
 private fun MultiSizePageDemo() {
     val lazyListState = rememberLazyListState()
-    val snapLayoutInfoProvider =
-        remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
-    val flingBehavior = rememberSnapFlingBehavior(snapLayoutInfoProvider)
+    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+    val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
         ResizableSnapDemoItem(
@@ -76,8 +74,8 @@
 @Composable
 private fun LargePageSizeDemo() {
     val lazyListState = rememberLazyListState()
-    val snappingLayout = remember(lazyListState) { SnapLayoutInfoProvider(lazyListState) }
-    val flingBehavior = rememberSnapFlingBehavior(snappingLayout)
+    val layoutInfoProvider = rememberNextPageSnappingLayoutInfoProvider(lazyListState)
+    val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
 
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
         ResizableSnapDemoItem(
@@ -109,8 +107,7 @@
 @Composable
 private fun MultiPageSnappingDemo() {
     val lazyListState = rememberLazyListState()
-    val layoutInfoProvider = rememberMultiPageSnappingLayoutInfoProvider(lazyListState)
-    val flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+    val flingBehavior = rememberSnapFlingBehavior(lazyListState)
     SnappingDemoMainLayout(lazyListState = lazyListState, flingBehavior = flingBehavior) {
         DefaultSnapDemoItem(position = it)
     }
@@ -130,15 +127,16 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 @Composable
-private fun rememberMultiPageSnappingLayoutInfoProvider(
+private fun rememberNextPageSnappingLayoutInfoProvider(
     state: LazyListState
 ): SnapLayoutInfoProvider {
-    val animation: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
     return remember(state) {
-        MultiPageSnappingLayoutInfoProvider(
-            SnapLayoutInfoProvider(lazyListState = state),
-            animation
-        )
+        val basedSnappingLayoutInfoProvider = SnapLayoutInfoProvider(lazyListState = state)
+        object : SnapLayoutInfoProvider by basedSnappingLayoutInfoProvider {
+            override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+                return 0f
+            }
+        }
     }
 }
 
@@ -147,10 +145,12 @@
 private fun rememberViewPortSnappingLayoutInfoProvider(
     state: LazyListState
 ): SnapLayoutInfoProvider {
+    val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
     return remember(state) {
         ViewPortBasedSnappingLayoutInfoProvider(
-            SnapLayoutInfoProvider(lazyListState = state)
-        ) { state.layoutInfo.visibleItemsInfo.sumOf { it.size }.toFloat() }
+            SnapLayoutInfoProvider(lazyListState = state),
+            decayAnimationSpec,
+        ) { state.layoutInfo.viewportSize.width.toFloat() }
     }
 }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
index 37802cd..81bce0f 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnapLayoutInfoProvider.kt
@@ -33,22 +33,24 @@
 
     fun Density.nextFullItemCenter(layoutCenter: Float): Float {
         val intItemSize = itemSize().roundToInt()
-        return floor((layoutCenter + snapStepSize()) / itemSize().roundToInt()) * intItemSize
+        return floor((layoutCenter + calculateSnapStepSize()) / itemSize().roundToInt()) *
+            intItemSize
     }
 
     fun Density.previousFullItemCenter(layoutCenter: Float): Float {
         val intItemSize = itemSize().roundToInt()
-        return ceil((layoutCenter - snapStepSize()) / itemSize().roundToInt()) * intItemSize
+        return ceil((layoutCenter - calculateSnapStepSize()) / itemSize().roundToInt()) *
+            intItemSize
     }
 
     override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
-        val layoutCenter = layoutSize() / 2f + scrollState.value + snapStepSize() / 2f
+        val layoutCenter = layoutSize() / 2f + scrollState.value + calculateSnapStepSize() / 2f
         val lowerBound = nextFullItemCenter(layoutCenter) - layoutCenter
         val upperBound = previousFullItemCenter(layoutCenter) - layoutCenter
         return upperBound.rangeTo(lowerBound)
     }
 
-    override fun Density.snapStepSize(): Float {
+    override fun Density.calculateSnapStepSize(): Float {
         return itemSize()
     }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
index 9806c16..ef0f3387 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/RowSnappingDemos.kt
@@ -178,13 +178,15 @@
     layoutSize: Density.() -> Float
 ): SnapLayoutInfoProvider {
     val density = LocalDensity.current
+    val decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay()
     return remember(scrollState, density, layoutSize) {
         ViewPortBasedSnappingLayoutInfoProvider(
             SnapLayoutInfoProvider(
                 scrollState = scrollState,
                 itemSize = { RowItemSize.toPx() },
                 layoutSize = layoutSize
-            )
+            ),
+            decayAnimationSpec
         ) { with(density, layoutSize) }
     }
 }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
index b10ec35..f4e5e58 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/snapping/SnappingDemos.kt
@@ -42,11 +42,13 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.Stroke
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import kotlin.math.absoluteValue
+import kotlin.math.sign
 
 val SnappingDemos = listOf(
     DemoCategory("Lazy List Snapping", LazyListSnappingDemos),
@@ -60,17 +62,22 @@
     private val decayAnimationSpec: DecayAnimationSpec<Float>
 ) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
     override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        return decayAnimationSpec.calculateTargetValue(0f, initialVelocity) / 2f
+        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+        val finalDecayedOffset = (offset.absoluteValue - calculateSnapStepSize()).coerceAtLeast(0f)
+        return finalDecayedOffset * initialVelocity.sign
     }
 }
 
 @OptIn(ExperimentalFoundationApi::class)
 internal class ViewPortBasedSnappingLayoutInfoProvider(
     private val baseSnapLayoutInfoProvider: SnapLayoutInfoProvider,
+    private val decayAnimationSpec: DecayAnimationSpec<Float>,
     private val viewPortStep: () -> Float
 ) : SnapLayoutInfoProvider by baseSnapLayoutInfoProvider {
     override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
-        return viewPortStep()
+        val offset = decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+        val viewPortOffset = viewPortStep()
+        return offset.coerceIn(-viewPortOffset, viewPortOffset)
     }
 }
 
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
index e51f4fe..6186f91 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputField.kt
@@ -40,7 +40,6 @@
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.window.Dialog
 
 @Preview
 @Composable
@@ -77,13 +76,6 @@
     }
 }
 
-@Composable
-fun DialogInputFieldDemo(onNavigateUp: () -> Unit) {
-    Dialog(onDismissRequest = onNavigateUp) {
-        InputFieldDemo()
-    }
-}
-
 @OptIn(ExperimentalComposeUiApi::class)
 @Composable
 internal fun EditLine(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt
new file mode 100644
index 0000000..c750f11
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/FocusTextFieldImmediatelyDemo.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.foundation.demos.text
+
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+
+@Composable
+fun FocusTextFieldImmediatelyDemo() {
+    val focusRequester = remember { FocusRequester() }
+    var value by remember { mutableStateOf("") }
+
+    DisposableEffect(focusRequester) {
+        focusRequester.requestFocus()
+        onDispose {}
+    }
+
+    TextField(
+        value,
+        onValueChange = { value = it },
+        modifier = Modifier
+            .wrapContentSize()
+            .focusRequester(focusRequester)
+    )
+}
\ No newline at end of file
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 2b5a89f..eb423ca 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
@@ -95,9 +95,7 @@
                 ComposableDemo("Full-screen field") { FullScreenTextFieldDemo() },
                 ComposableDemo("Ime Action") { ImeActionDemo() },
                 ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
-                ComposableDemo("Inside Dialog") { onNavigateUp ->
-                    DialogInputFieldDemo(onNavigateUp)
-                },
+                ComposableDemo("Inside Dialog") { TextFieldsInDialogDemo() },
                 ComposableDemo("Inside scrollable") { TextFieldsInScrollableDemo() },
                 ComposableDemo("Keyboard Types") { KeyboardTypeDemo() },
                 ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
@@ -106,6 +104,7 @@
                 ComposableDemo("Visual Transformation") { VisualTransformationDemo() },
                 ComposableDemo("TextFieldValue") { TextFieldValueDemo() },
                 ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
+                ComposableDemo("Focus immediately") { FocusTextFieldImmediatelyDemo() },
             )
         ),
         DemoCategory(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt
new file mode 100644
index 0000000..bce2a5f
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldsInDialogDemo.kt
@@ -0,0 +1,157 @@
+/*
+ * 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(ExperimentalComposeUiApi::class)
+
+package androidx.compose.foundation.demos.text
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.integration.demos.common.ComposableDemo
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ListItem
+import androidx.compose.material.Text
+import androidx.compose.material.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.window.Dialog
+import androidx.compose.ui.window.DialogProperties
+
+private val dialogDemos = listOf(
+    ComposableDemo("Full screen dialog, multiple fields") { onNavigateUp ->
+        Dialog(onDismissRequest = onNavigateUp) {
+            InputFieldDemo()
+        }
+    },
+    ComposableDemo(
+        "Small dialog, single field (platform default width, decor fits system windows)"
+    ) { onNavigateUp ->
+        Dialog(
+            onDismissRequest = onNavigateUp,
+            properties = DialogProperties(
+                usePlatformDefaultWidth = true,
+                decorFitsSystemWindows = true
+            )
+        ) { SingleTextFieldDialog() }
+    },
+    ComposableDemo(
+        "Small dialog, single field (decor fits system windows)"
+    ) { onNavigateUp ->
+        Dialog(
+            onDismissRequest = onNavigateUp,
+            properties = DialogProperties(
+                usePlatformDefaultWidth = false,
+                decorFitsSystemWindows = true
+            )
+        ) { SingleTextFieldDialog() }
+    },
+    ComposableDemo(
+        "Small dialog, single field (platform default width)"
+    ) { onNavigateUp ->
+        Dialog(
+            onDismissRequest = onNavigateUp,
+            properties = DialogProperties(
+                usePlatformDefaultWidth = true,
+                decorFitsSystemWindows = false
+            )
+        ) { SingleTextFieldDialog() }
+    },
+    ComposableDemo(
+        "Small dialog, single field"
+    ) { onNavigateUp ->
+        Dialog(
+            onDismissRequest = onNavigateUp,
+            properties = DialogProperties(
+                usePlatformDefaultWidth = false,
+                decorFitsSystemWindows = false
+            )
+        ) { SingleTextFieldDialog() }
+    },
+    ComposableDemo("Show keyboard automatically") { onNavigateUp ->
+        Dialog(onDismissRequest = onNavigateUp) {
+            AutoFocusTextFieldDialog()
+        }
+    }
+)
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TextFieldsInDialogDemo() {
+    val listState = rememberLazyListState()
+    val (currentDemoIndex, setDemoIndex) = rememberSaveable { mutableStateOf(-1) }
+
+    if (currentDemoIndex == -1) {
+        LazyColumn(state = listState) {
+            itemsIndexed(dialogDemos) { index, demo ->
+                ListItem(Modifier.clickable { setDemoIndex(index) }) {
+                    Text(demo.title)
+                }
+            }
+        }
+    } else {
+        val currentDemo = dialogDemos[currentDemoIndex]
+        Text(
+            currentDemo.title,
+            modifier = Modifier
+                .fillMaxSize()
+                .wrapContentSize(),
+            textAlign = TextAlign.Center
+        )
+        Layout(
+            content = { currentDemo.content(onNavigateUp = { setDemoIndex(-1) }) }
+        ) { measurables, _ ->
+            check(measurables.isEmpty()) { "Dialog demo must only emit a Dialog composable." }
+            layout(0, 0) {}
+        }
+    }
+}
+
+@Composable
+private fun SingleTextFieldDialog() {
+    var text by remember { mutableStateOf("") }
+    TextField(text, onValueChange = { text = it })
+}
+
+@Composable
+private fun AutoFocusTextFieldDialog() {
+    var text by remember { mutableStateOf("") }
+    val focusRequester = remember { FocusRequester() }
+
+    LaunchedEffect(focusRequester) {
+        focusRequester.requestFocus()
+    }
+
+    TextField(
+        text,
+        onValueChange = { text = it },
+        modifier = Modifier.focusRequester(focusRequester)
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
index 94940dd..1645cb76 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/ScrollableSamples.kt
@@ -22,14 +22,24 @@
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Icon
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.KeyboardArrowDown
+import androidx.compose.material.icons.filled.KeyboardArrowUp
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
@@ -62,4 +72,44 @@
         // the delta values in the scrollable state
         Text(offset.value.roundToInt().toString(), style = TextStyle(fontSize = 32.sp))
     }
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun CanScrollSample() {
+    val state = rememberLazyListState()
+    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+        Icon(
+            Icons.Filled.KeyboardArrowUp,
+            null,
+            Modifier.graphicsLayer {
+                // Hide the icon if we cannot scroll backward (we are the start of the list)
+                // We use graphicsLayer here to control the alpha so that we only redraw when this
+                // value changes, instead of recomposing
+                alpha = if (state.canScrollBackward) 1f else 0f
+            },
+            Color.Red
+        )
+        val items = (1..100).toList()
+        LazyColumn(
+            Modifier
+                .weight(1f)
+                .fillMaxWidth(), state
+        ) {
+            items(items) {
+                Text("Item is $it")
+            }
+        }
+        Icon(
+            Icons.Filled.KeyboardArrowDown,
+            null,
+            Modifier.graphicsLayer {
+                // Hide the icon if we cannot scroll forward (we are the end of the list)
+                // We use graphicsLayer here to control the alpha so that we only redraw when this
+                // value changes, instead of recomposing
+                alpha = if (state.canScrollForward) 1f else 0f
+            },
+            Color.Red
+        )
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
index 1a5794f..5633465 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/BorderTest.kt
@@ -254,6 +254,7 @@
         }
     }
 
+    @SdkSuppress(maxSdkVersion = 32) // b/257069369
     @Test
     fun border_non_simple_rounded_rect() {
         val topleft = 0f
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
index eb18a04..0ceccea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ClickableInScrollableViewGroupTest.kt
@@ -37,9 +37,7 @@
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -205,10 +203,6 @@
     /**
      * Test case for a [clickable] inside an [AndroidView] inside a non-scrollable Compose container
      */
-    @Ignore(
-        "b/203141462 - currently this is not implemented so AndroidView()s will always " +
-            "appear scrollable"
-    )
     @Test
     fun clickable_androidViewInNotScrollableContainer() {
         val interactionSource = MutableInteractionSource()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
index b05fcaa..d6563b6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
@@ -60,7 +60,6 @@
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalDensity
@@ -105,6 +104,7 @@
 import kotlinx.coroutines.launch
 import org.junit.After
 import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
 import org.junit.Before
 import org.junit.Rule
@@ -969,6 +969,87 @@
         assertThat(sizeParam).isEqualTo(Constraints.Infinity)
     }
 
+    @Test
+    fun canNotScrollForwardOrBackward() {
+        val scrollState = ScrollState(initial = 0)
+
+        composeScroller(scrollState)
+
+        rule.runOnIdle {
+            assertTrue(scrollState.maxValue == 0)
+            assertFalse(scrollState.canScrollForward)
+            assertFalse(scrollState.canScrollBackward)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun canScrollForward() {
+        val scrollState = ScrollState(initial = 0)
+        val size = 30
+
+        composeScroller(scrollState, mainAxisSize = size)
+
+        validateScroller(mainAxis = size)
+
+        rule.runOnIdle {
+            assertTrue(scrollState.value == 0)
+            assertTrue(scrollState.maxValue > 0)
+            assertTrue(scrollState.canScrollForward)
+            assertFalse(scrollState.canScrollBackward)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun canScrollBackward() {
+        val scrollState = ScrollState(initial = 0)
+        val scrollDistance = 10
+        val size = 30
+
+        composeScroller(scrollState, mainAxisSize = size)
+
+        validateScroller(mainAxis = size)
+
+        rule.waitForIdle()
+        assertEquals(scrollDistance, scrollState.maxValue)
+        scope.launch {
+            scrollState.scrollTo(scrollDistance)
+        }
+
+        rule.runOnIdle {
+            assertTrue(scrollState.value == scrollDistance)
+            assertTrue(scrollState.maxValue == scrollDistance)
+            assertFalse(scrollState.canScrollForward)
+            assertTrue(scrollState.canScrollBackward)
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = 26)
+    @Test
+    fun canScrollForwardAndBackward() {
+        val scrollState = ScrollState(initial = 0)
+        val scrollDistance = 5
+        val size = 30
+
+        composeScroller(scrollState, mainAxisSize = size)
+
+        validateScroller(mainAxis = size)
+
+        rule.waitForIdle()
+        assertEquals(scrollDistance, scrollState.maxValue / 2)
+        scope.launch {
+            scrollState.scrollTo(scrollDistance)
+        }
+
+        rule.runOnIdle {
+            assertTrue(scrollState.value == scrollDistance)
+            assertTrue(scrollState.maxValue == scrollDistance * 2)
+            assertTrue(scrollState.canScrollForward)
+            assertTrue(scrollState.canScrollBackward)
+        }
+    }
+
     private fun Modifier.intrinsicMainAxisSize(size: IntrinsicSize): Modifier =
         if (config.orientation == Horizontal) {
             width(size)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index 2b284d1..69973c1 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.animation.rememberSplineBasedDecay
 import androidx.compose.foundation.gestures.DefaultFlingBehavior
 import androidx.compose.foundation.gestures.FlingBehavior
-import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.ScrollScope
 import androidx.compose.foundation.gestures.ScrollableDefaults
@@ -60,6 +59,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -69,8 +69,6 @@
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.input.pointer.util.VelocityTracker
 import androidx.compose.ui.materialize
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
 import androidx.compose.ui.platform.AbstractComposeView
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalFocusManager
@@ -1689,49 +1687,73 @@
     fun scrollable_setsModifierLocalScrollableContainer() {
         val controller = ScrollableState { it }
 
-        var isOuterInScrollableContainer: Boolean? = null
-        var isInnerInScrollableContainer: Boolean? = null
+        var isOuterInVerticalScrollableContainer: Boolean? = null
+        var isInnerInVerticalScrollableContainer: Boolean? = null
+        var isOuterInHorizontalScrollableContainer: Boolean? = null
+        var isInnerInHorizontalScrollableContainer: Boolean? = null
         rule.setContent {
             Box {
                 Box(
                     modifier = Modifier
                         .testTag(scrollableBoxTag)
                         .size(100.dp)
-                        .then(
-                            object : ModifierLocalConsumer {
-                                override fun onModifierLocalsUpdated(
-                                    scope: ModifierLocalReadScope
-                                ) {
-                                    with(scope) {
-                                        isOuterInScrollableContainer =
-                                            ModifierLocalScrollableContainer.current
-                                    }
-                                }
-                            }
-                        )
+                        .consumeScrollContainerInfo {
+                            isOuterInVerticalScrollableContainer = it?.canScrollVertically()
+                            isOuterInHorizontalScrollableContainer = it?.canScrollHorizontally()
+                        }
                         .scrollable(
                             state = controller,
                             orientation = Orientation.Horizontal
                         )
-                        .then(
-                            object : ModifierLocalConsumer {
-                                override fun onModifierLocalsUpdated(
-                                    scope: ModifierLocalReadScope
-                                ) {
-                                    with(scope) {
-                                        isInnerInScrollableContainer =
-                                            ModifierLocalScrollableContainer.current
-                                    }
-                                }
-                            }
-                        )
+                        .consumeScrollContainerInfo {
+                            isInnerInHorizontalScrollableContainer = it?.canScrollHorizontally()
+                            isInnerInVerticalScrollableContainer = it?.canScrollVertically()
+                        }
                 )
             }
         }
 
         rule.runOnIdle {
-            assertThat(isOuterInScrollableContainer).isFalse()
-            assertThat(isInnerInScrollableContainer).isTrue()
+            assertThat(isInnerInHorizontalScrollableContainer).isTrue()
+            assertThat(isInnerInVerticalScrollableContainer).isFalse()
+            assertThat(isOuterInVerticalScrollableContainer).isFalse()
+            assertThat(isOuterInHorizontalScrollableContainer).isFalse()
+        }
+    }
+
+    @Test
+    fun scrollable_nested_setsModifierLocalScrollableContainer() {
+        val horizontalController = ScrollableState { it }
+        val verticalController = ScrollableState { it }
+
+        var horizontalDrag: Boolean? = null
+        var verticalDrag: Boolean? = null
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .size(100.dp)
+                    .scrollable(
+                        state = horizontalController,
+                        orientation = Orientation.Horizontal
+                    )
+            ) {
+                Box(
+                    modifier = Modifier
+                        .size(100.dp)
+                        .scrollable(
+                            state = verticalController,
+                            orientation = Orientation.Vertical
+                        )
+                        .consumeScrollContainerInfo {
+                            horizontalDrag = it?.canScrollHorizontally()
+                            verticalDrag = it?.canScrollVertically()
+                        })
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(horizontalDrag).isTrue()
+            assertThat(verticalDrag).isTrue()
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
index d889aac..42dd8ee 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/LazyListSnapLayoutInfoProviderTest.kt
@@ -16,6 +16,8 @@
 
 package androidx.compose.foundation.gesture.snapping
 
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
@@ -34,6 +36,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
+import kotlin.math.absoluteValue
 import kotlin.math.round
 import kotlin.math.sign
 import kotlin.test.assertEquals
@@ -59,7 +62,7 @@
             val density = LocalDensity.current
             val state = rememberLazyListState()
             val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.snapStepSize() }
+                actualItemSize = with(it) { density.calculateSnapStepSize() }
             }
             expectedItemSize = with(density) { FixedItemSize.toPx() }
             MainLayout(
@@ -84,7 +87,7 @@
             val density = LocalDensity.current
             val state = rememberLazyListState()
             val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                actualItemSize = with(it) { density.snapStepSize() }
+                actualItemSize = with(it) { density.calculateSnapStepSize() }
             }
             expectedItemSize = state.layoutInfo.visibleItemsInfo.map { it.size }.average().toFloat()
 
@@ -104,7 +107,7 @@
             val density = LocalDensity.current
             val state = rememberLazyListState()
             val layoutInfoProvider = remember(state) { createLayoutInfo(state) }.also {
-                snapStepSize = with(it) { density.snapStepSize() }
+                snapStepSize = with(it) { density.calculateSnapStepSize() }
             }
 
             actualItemSize = with(density) { (FixedItemSize + FixedItemSize / 2).toPx() }
@@ -150,7 +153,40 @@
     }
 
     @Test
-    fun calculateApproachOffset_approachOffsetIsAlwaysZero() {
+    fun calculateApproachOffset_highVelocity_approachOffsetIsEqualToDecayMinusItemSize() {
+        lateinit var layoutInfoProvider: SnapLayoutInfoProvider
+        val decay = splineBasedDecay<Float>(rule.density)
+        fun calculateTargetOffset(velocity: Float): Float {
+            val offset = decay.calculateTargetValue(0f, velocity).absoluteValue
+            return (offset - with(density) { 200.dp.toPx() }).coerceAtLeast(0f) * velocity.sign
+        }
+        rule.setContent {
+            val state = rememberLazyListState()
+            layoutInfoProvider = remember(state) { createLayoutInfo(state) }
+            LazyColumnOrRow(
+                state = state,
+                flingBehavior = rememberSnapFlingBehavior(layoutInfoProvider)
+            ) {
+                items(200) {
+                    Box(modifier = Modifier.size(200.dp))
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertEquals(
+                with(layoutInfoProvider) { density.calculateApproachOffset(10000f) },
+                calculateTargetOffset(10000f)
+            )
+            assertEquals(
+                with(layoutInfoProvider) { density.calculateApproachOffset(-10000f) },
+                calculateTargetOffset(-10000f)
+            )
+        }
+    }
+
+    @Test
+    fun calculateApproachOffset_lowVelocity_approachOffsetIsEqualToZero() {
         lateinit var layoutInfoProvider: SnapLayoutInfoProvider
         rule.setContent {
             val state = rememberLazyListState()
@@ -166,8 +202,14 @@
         }
 
         rule.runOnIdle {
-            assertEquals(with(layoutInfoProvider) { density.calculateApproachOffset(1000f) }, 0f)
-            assertEquals(with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) }, 0f)
+            assertEquals(
+                with(layoutInfoProvider) { density.calculateApproachOffset(1000f) },
+                0f
+            )
+            assertEquals(
+                with(layoutInfoProvider) { density.calculateApproachOffset(-1000f) },
+                0f
+            )
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
index 047223a..6c5dffe 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/gesture/snapping/SnapFlingBehaviorTest.kt
@@ -257,7 +257,7 @@
 ) : SnapLayoutInfoProvider {
     var calculateApproachOffsetCount = 0
 
-    override fun Density.snapStepSize(): Float {
+    override fun Density.calculateSnapStepSize(): Float {
         return snapStep
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index 92acae1..9c6721f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.foundation.layout.requiredHeightIn
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.foundation.lazy.items
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -1673,6 +1674,39 @@
         }
     }
 
+    @Test
+    fun itemWithSpecsIsMovingOut() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyGrid(1, maxSize = itemSizeDp * 2) {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            val listSize = itemSize * 2
+            val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                if (item1Offset < listSize) {
+                    add(1 to AxisIntOffset(0, item1Offset))
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
     private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
         if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
index b11236d..02475c7 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridTest.kt
@@ -1375,6 +1375,39 @@
         }
         rule.waitUntil(timeoutMillis = 10000) { animationFinished }
     }
+
+    @Test
+    fun fillingFullSize_nextItemIsNotComposed() {
+        val state = LazyGridState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                1,
+                Modifier
+                    .testTag(LazyGridTag)
+                    .mainAxisSize(itemSize),
+                state
+            ) {
+                items(3) { index ->
+                    Box(Modifier.size(itemSize).testTag("$index"))
+                }
+            }
+        }
+
+        repeat(3) { index ->
+            rule.onNodeWithTag("$index")
+                .assertIsDisplayed()
+            rule.onNodeWithTag("${index + 1}")
+                .assertDoesNotExist()
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
 }
 
 internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
index c4bf183..50475ce 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridsContentPaddingTest.kt
@@ -442,7 +442,7 @@
             .assertTopPositionInRootIsEqualTo(0.dp)
         // Not visible.
         rule.onNodeWithTag("1")
-            .assertIsNotDisplayed()
+            .assertDoesNotExist()
 
         // Scroll to the top.
         state.scrollBy(itemSize * 5f)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
index 962cda5..5d6408a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyScrollTest.kt
@@ -243,6 +243,33 @@
         assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
     }
 
+    @Test
+    fun canScrollForward() = runBlocking {
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isFalse()
+    }
+
+    @Test
+    fun canScrollBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+        assertThat(state.canScrollForward).isFalse()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
+    @Test
+    fun canScrollForwardAndBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
     private fun assertSpringAnimation(
         toIndex: Int,
         toOffset: Int = 0,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
index ffc7d89..5580a92 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutStateRestorationTest.kt
@@ -34,7 +34,6 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -46,7 +45,6 @@
     @get:Rule
     val rule = createComposeRule()
 
-    @Ignore // b/244308934
     @Test
     fun visibleItemsStateRestored() {
         val restorationTester = StateRestorationTester(rule)
@@ -83,7 +81,6 @@
         }
     }
 
-    @Ignore // b/244308934
     @Test
     fun itemsStateRestoredWhenWeScrolledBackToIt() {
         var counter0 = 1
@@ -123,7 +120,6 @@
         }
     }
 
-    @Ignore // b/244308934
     @Test
     fun nestedLazy_itemsStateRestoredWhenWeScrolledBackToIt() {
         var counter0 = 1
@@ -206,7 +202,6 @@
         }
     }
 
-    @Ignore // b/244308934
     @Test
     fun stateRestoredWhenUsedWithCustomKeysAfterReordering() {
         val restorationTester = StateRestorationTester(rule)
@@ -218,7 +213,7 @@
         restorationTester.setContent {
             LazyLayout(
                 itemCount = list.size,
-                indexToKey = { "${list[it]}" }
+                indexToKey = { "${list.getOrNull(it)}" }
             ) { index ->
                 val it = list[index]
                 if (it == 0) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index d183f39..8a426a4 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -58,6 +58,13 @@
             this.fillMaxHeight()
         }
 
+    fun LazyItemScope.fillParentMaxMainAxis() =
+        if (vertical) {
+            Modifier.fillParentMaxHeight()
+        } else {
+            Modifier.fillParentMaxWidth()
+        }
+
     fun LazyItemScope.fillParentMaxCrossAxis() =
         if (vertical) {
             Modifier.fillParentMaxWidth()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
index 44c7e34..bb58ce9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -1219,6 +1219,39 @@
         }
     }
 
+    @Test
+    fun itemWithSpecsIsMovingOut() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyList(maxSize = itemSizeDp * 2) {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            val listSize = itemSize * 2
+            val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                add(0 to 0)
+                if (item1Offset < listSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
     private fun assertPositions(
         vararg expected: Pair<Any, Int>,
         crossAxis: List<Pair<Any, Int>>? = null,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index 3af1774..9838ba9 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -106,9 +106,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'contains' with 'containsExactly'.
-            assertThat(placedItems).contains(0)
-            assertThat(visibleItems).contains(0)
+            assertThat(placedItems).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
         }
     }
 
@@ -127,9 +126,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(0, 1)
-            assertThat(visibleItems).containsAtLeast(0, 1)
+            assertThat(placedItems).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
         }
     }
 
@@ -148,9 +146,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(0, 1, 2)
-            assertThat(visibleItems).containsAtLeast(0, 1, 2)
+            assertThat(placedItems).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
         }
     }
 
@@ -189,13 +186,11 @@
             beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
                 // Assert that the beyond bounds items are present.
                 if (expectedExtraItemsBeforeVisibleBounds()) {
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(4, 5, 6, 7)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 } else {
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(5, 6, 7, 8)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 }
                 placedItems.clear()
                 // Just return true so that we stop as soon as we run this once.
@@ -206,9 +201,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -254,13 +248,11 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     } else {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     placedItems.clear()
                     // Return true to stop the search.
@@ -271,9 +263,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -320,13 +311,11 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     } else {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     placedItems.clear()
                     // Return true to end the search.
@@ -337,8 +326,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -368,11 +356,15 @@
                 Box(
                     Modifier
                         .size(10.toDp())
-                        .onPlaced { placedItems += index + 5 }
+                        .onPlaced { placedItems += index + 6 }
                 )
             }
         }
-        rule.runOnIdle { placedItems.clear() }
+        rule.runOnIdle {
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+            placedItems.clear()
+        }
 
         // Act.
         rule.runOnUiThread {
@@ -380,19 +372,16 @@
                 beyondBoundsLayoutCount++
                 when (beyondBoundsLayoutDirection) {
                     Left, Right, Above, Below -> {
-                        assertThat(placedItems).containsExactlyElementsIn(visibleItems)
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     Before, After -> {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
                         if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsAtLeast(4, 5, 6, 7)
-                            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
                         } else {
-                            assertThat(placedItems).containsAtLeast(5, 6, 7, 8)
-                            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
                         }
                     }
                 }
@@ -412,9 +401,8 @@
                     assertThat(beyondBoundsLayoutCount).isEqualTo(1)
 
                     // Assert that the beyond bounds items are removed.
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(5, 6, 7)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 }
                 else -> error("Unsupported BeyondBoundsLayoutDirection")
             }
@@ -470,9 +458,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
index c24bfad..0e2d86f 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListFocusMoveTest.kt
@@ -316,7 +316,7 @@
         }
         rule.runOnIdle {
             // Scroll so that the focused item is in the middle.
-            runBlocking { lazyListState.scrollToItem(1) }
+            runBlocking { lazyListState.scrollToItem(1, 10) }
             initiallyFocused.requestFocus()
 
             // Move focus to the last visible item.
@@ -370,7 +370,7 @@
         }
         rule.runOnIdle {
             // Scroll so that the focused item is in the middle.
-            runBlocking { lazyListState.scrollToItem(1) }
+            runBlocking { lazyListState.scrollToItem(2, 0) }
             initiallyFocused.requestFocus()
 
             // Move focus to the last visible item.
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index 25e78a4..72f5302 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.VelocityTrackerCalculationThreshold
 import androidx.compose.foundation.background
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxWithConstraints
 import androidx.compose.foundation.layout.Column
@@ -2146,6 +2147,67 @@
             .assertCrossAxisSizeIsEqualTo(0.dp)
     }
 
+    @Test
+    fun fillingFullSize_nextItemIsNotComposed() {
+        val state = LazyListState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier
+                    .testTag(LazyListTag)
+                    .mainAxisSize(itemSize),
+                state = state
+            ) {
+                items(3) { index ->
+                    Box(fillParentMaxMainAxis().crossAxisSize(1.dp).testTag("$index"))
+                }
+            }
+        }
+
+        repeat(3) { index ->
+            rule.onNodeWithTag("$index")
+                .assertIsDisplayed()
+            rule.onNodeWithTag("${index + 1}")
+                .assertDoesNotExist()
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun fillingFullSize_crossAxisSizeOfVisibleItemIsUsed() {
+        val state = LazyListState()
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier
+                    .testTag(LazyListTag)
+                    .mainAxisSize(itemSize),
+                state = state
+            ) {
+                items(5) { index ->
+                    Box(fillParentMaxMainAxis().crossAxisSize(index.dp))
+                }
+            }
+        }
+
+        repeat(5) { index ->
+            rule.onNodeWithTag(LazyListTag)
+                .assertCrossAxisSizeIsEqualTo(index.dp)
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
+
     // ********************* END OF TESTS *********************
     // Helper functions, etc. live below here
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
index 8b7a6ce..c46bf7d 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListsContentPaddingTest.kt
@@ -418,7 +418,7 @@
         rule.onNodeWithTag("0")
             .assertStartPositionInRootIsEqualTo(0.dp)
         // Shouldn't be visible
-        rule.onNodeWithTag("1").assertIsNotDisplayed()
+        rule.onNodeWithTag("1").assertDoesNotExist()
 
         // Scroll to the top.
         state.scrollBy(itemSize * 5f)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
index 22da052..e0fdf831 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyScrollTest.kt
@@ -243,6 +243,33 @@
         assertSpringAnimation(toIndex = 0, toOffset = 20, fromIndex = 8)
     }
 
+    @Test
+    fun canScrollForward() = runBlocking {
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isFalse()
+    }
+
+    @Test
+    fun canScrollBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+        assertThat(state.canScrollForward).isFalse()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
+    @Test
+    fun canScrollForwardAndBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(1)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
     private fun assertSpringAnimation(
         toIndex: Int,
         toOffset: Int = 0,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
index 10040bc..49c2a4e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridScrollTest.kt
@@ -33,6 +33,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -170,6 +171,39 @@
         assertThat(mainAxisOffset).isEqualTo(itemSizePx * 3) // x5 (grid) - x2 (item)
     }
 
+    @Test
+    fun canScrollForward() = runBlocking {
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isFalse()
+    }
+
+    @Test
+    fun canScrollBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(99)
+        }
+        val lastItem = state.layoutInfo.visibleItemsInfo.last()
+        val mainAxisOffset = if (orientation == Orientation.Vertical) {
+            lastItem.offset.y
+        } else {
+            lastItem.offset.x
+        }
+        assertThat(mainAxisOffset).isEqualTo(itemSizePx * 3) // x5 (grid) - x2 (item)
+        assertThat(state.canScrollForward).isFalse()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
+    @Test
+    fun canScrollForwardAndBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
     @Composable
     private fun TestContent() {
         // |-|-|
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index fc86b6b..a9dc3a2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -19,9 +19,11 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.list.setContentWithTestViewConfiguration
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -258,7 +260,7 @@
         }
 
         rule.onNodeWithTag("3")
-            .assertIsNotDisplayed()
+            .assertDoesNotExist()
 
         state.scrollBy(itemSizeDp * 3)
 
@@ -1210,4 +1212,37 @@
         rule.waitForIdle()
         assertThat(state.measurePassCount).isEqualTo(1)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun fillingFullSize_nextItemIsNotComposed() {
+        val state = LazyStaggeredGridState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyStaggeredGrid(
+                1,
+                Modifier
+                    .testTag(LazyStaggeredGridTag)
+                    .mainAxisSize(itemSize),
+                state
+            ) {
+                items(3) { index ->
+                    Box(Modifier.size(itemSize).testTag("$index"))
+                }
+            }
+        }
+
+        repeat(3) { index ->
+            rule.onNodeWithTag("$index")
+                .assertIsDisplayed()
+            rule.onNodeWithTag("${index + 1}")
+                .assertDoesNotExist()
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
index 421bdef..019a837 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldKeyboardScrollableInteractionTest.kt
@@ -45,7 +45,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.click
@@ -55,6 +54,7 @@
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
+import androidx.test.filters.MediumTest
 import org.junit.Assume.assumeTrue
 import org.junit.Rule
 import org.junit.Test
@@ -62,6 +62,7 @@
 import org.junit.runners.Parameterized
 import org.junit.runners.Parameterized.Parameters
 
+@MediumTest
 @RunWith(Parameterized::class)
 class CoreTextFieldKeyboardScrollableInteractionTest(
     private val scrollableType: ScrollableType,
@@ -96,11 +97,11 @@
 
     @Test
     fun test() {
-        // TODO(b/192043120) This is known broken when soft input mode is Resize.
-        assumeTrue(softInputMode != AdjustResize)
+        // TODO(b/179203700) This is known broken for lazy lists.
+        assumeTrue(scrollableType != LazyList)
 
         rule.setContent {
-            keyboardHelper.view = LocalView.current
+            keyboardHelper.initialize()
             TestContent()
         }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
index 630e70e..a9b6fe6 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/DefaultKeyboardActionsTest.kt
@@ -24,19 +24,18 @@
 import androidx.compose.ui.focus.focusProperties
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performImeAction
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.ImeAction.Companion.Done
 import androidx.compose.ui.text.input.ImeAction.Companion.Go
+import androidx.compose.ui.text.input.ImeAction.Companion.Next
+import androidx.compose.ui.text.input.ImeAction.Companion.Previous
 import androidx.compose.ui.text.input.ImeAction.Companion.Search
 import androidx.compose.ui.text.input.ImeAction.Companion.Send
-import androidx.compose.ui.text.input.ImeAction.Companion.Previous
-import androidx.compose.ui.text.input.ImeAction.Companion.Next
-import androidx.compose.ui.text.input.ImeAction.Companion.Done
-import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.test.filters.LargeTest
@@ -82,8 +81,7 @@
         val keyboardHelper = KeyboardHelper(rule)
 
         rule.setContent {
-            keyboardHelper.view = LocalView.current
-
+            keyboardHelper.initialize()
             Column {
                 CoreTextField(
                     value = value1,
@@ -164,8 +162,7 @@
         val keyboardHelper = KeyboardHelper(rule)
 
         rule.setContent {
-            keyboardHelper.view = LocalView.current
-
+            keyboardHelper.initialize()
             Column {
                 CoreTextField(
                     value = value1,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
index 9d52ba0..0418c0e 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardActionsTest.kt
@@ -18,24 +18,23 @@
 
 import android.os.Build
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performImeAction
 import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.ImeAction.Companion.Done
 import androidx.compose.ui.text.input.ImeAction.Companion.Go
+import androidx.compose.ui.text.input.ImeAction.Companion.Next
+import androidx.compose.ui.text.input.ImeAction.Companion.Previous
 import androidx.compose.ui.text.input.ImeAction.Companion.Search
 import androidx.compose.ui.text.input.ImeAction.Companion.Send
-import androidx.compose.ui.text.input.ImeAction.Companion.Previous
-import androidx.compose.ui.text.input.ImeAction.Companion.Next
-import androidx.compose.ui.text.input.ImeAction.Companion.Done
 import androidx.compose.ui.text.input.ImeOptions
 import androidx.compose.ui.text.input.TextFieldValue
-import com.google.common.truth.Truth.assertThat
 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
@@ -113,8 +112,7 @@
         var wasCallbackTriggered = false
 
         rule.setContent {
-            keyboardHelper.view = LocalView.current
-
+            keyboardHelper.initialize()
             CoreTextField(
                 value = textFieldValue,
                 onValueChange = {},
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
index 018cd82..f222389 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/KeyboardHelper.kt
@@ -16,43 +16,60 @@
 
 package androidx.compose.foundation.text
 
+import android.app.Activity
 import android.content.Context
+import android.content.ContextWrapper
 import android.os.Build
 import android.view.View
+import android.view.Window
 import android.view.WindowInsets
 import android.view.WindowInsetsAnimation
 import android.view.inputmethod.InputMethodManager
 import androidx.annotation.RequiresApi
-import androidx.compose.ui.test.junit4.ComposeTestRule
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 
 /**
  * Helper methods for hiding and showing the keyboard in tests.
- * Must set [view] before calling any methods on this class.
+ * Call [initialize] from your test rule's content before calling any other methods on this class.
  */
+@OptIn(ExperimentalComposeUiApi::class)
 class KeyboardHelper(
-    private val composeRule: ComposeTestRule,
+    private val composeRule: ComposeContentTestRule,
     private val timeout: Long = 15_000L
 ) {
     /**
      * The [View] hosting the compose rule's content. Must be set before calling any methods on this
      * class.
      */
-    lateinit var view: View
+    private lateinit var view: View
     private val imm by lazy {
         view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
     }
 
     /**
+     * Call this at the top of your test composition before using the helper.
+     */
+    @Composable
+    fun initialize() {
+        view = LocalView.current
+    }
+
+    /**
      * Requests the keyboard to be hidden without waiting for it.
-     * Should be called from the main thread.
      */
     fun hideKeyboard() {
-        if (Build.VERSION.SDK_INT >= 30) {
+        composeRule.runOnIdle {
+            // Use both techniques to hide it, at least one of them will hopefully work.
             hideKeyboardWithInsets()
-        } else {
             hideKeyboardWithImm()
         }
     }
@@ -68,26 +85,25 @@
     }
 
     fun hideKeyboardIfShown() {
-        composeRule.runOnIdle {
-            if (isSoftwareKeyboardShown()) {
-                hideKeyboard()
-                waitForKeyboardVisibility(visible = false)
-            }
+        if (composeRule.runOnIdle { isSoftwareKeyboardShown() }) {
+            hideKeyboard()
+            waitForKeyboardVisibility(visible = false)
         }
     }
 
     fun isSoftwareKeyboardShown(): Boolean {
-        return if (Build.VERSION.SDK_INT >= 30) {
+        return if (Build.VERSION.SDK_INT >= 23) {
             isSoftwareKeyboardShownWithInsets()
         } else {
             isSoftwareKeyboardShownWithImm()
         }
     }
 
-    @RequiresApi(30)
+    @RequiresApi(23)
     private fun isSoftwareKeyboardShownWithInsets(): Boolean {
-        return view.rootWindowInsets != null &&
-            view.rootWindowInsets.isVisible(WindowInsets.Type.ime())
+        val insets = view.rootWindowInsets ?: return false
+        val insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets, view)
+        return insetsCompat.isVisible(WindowInsetsCompat.Type.ime())
     }
 
     private fun isSoftwareKeyboardShownWithImm(): Boolean {
@@ -97,35 +113,61 @@
     }
 
     private fun hideKeyboardWithImm() {
-        imm.hideSoftInputFromWindow(view.windowToken, 0)
+        view.post {
+            imm.hideSoftInputFromWindow(view.windowToken, 0)
+        }
     }
 
-    @RequiresApi(30)
     private fun hideKeyboardWithInsets() {
-        view.windowInsetsController?.hide(WindowInsets.Type.ime())
+        view.findWindow()?.let { WindowInsetsControllerCompat(it, view) }
+            ?.hide(WindowInsetsCompat.Type.ime())
     }
 
     private fun waitUntil(timeout: Long, condition: () -> Boolean) {
         if (Build.VERSION.SDK_INT >= 30) {
-            view.waitUntil(timeout, condition)
+            view.waitForWindowInsetsUntil(timeout, condition)
         } else {
             composeRule.waitUntil(timeout, condition)
         }
     }
-}
 
-@RequiresApi(30)
-fun View.waitUntil(timeoutMillis: Long, condition: () -> Boolean) {
-    val latch = CountDownLatch(1)
-    rootView.setWindowInsetsAnimationCallback(
-        InsetAnimationCallback {
+    // TODO(b/221889664) Replace with composition local when available.
+    private fun View.findWindow(): Window? =
+        (parent as? DialogWindowProvider)?.window
+            ?: context.findWindow()
+
+    private tailrec fun Context.findWindow(): Window? =
+        when (this) {
+            is Activity -> window
+            is ContextWrapper -> baseContext.findWindow()
+            else -> null
+        }
+
+    @RequiresApi(30)
+    fun View.waitForWindowInsetsUntil(timeoutMillis: Long, condition: () -> Boolean) {
+        val latch = CountDownLatch(1)
+        rootView.setOnApplyWindowInsetsListener { view, windowInsets ->
             if (condition()) {
                 latch.countDown()
             }
+            view.onApplyWindowInsets(windowInsets)
+            windowInsets
         }
-    )
-    val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
-    assertThat(conditionMet).isTrue()
+        rootView.setWindowInsetsAnimationCallback(
+            InsetAnimationCallback {
+                if (condition()) {
+                    latch.countDown()
+                }
+            }
+        )
+
+        // if condition already met return
+        if (condition()) return
+
+        // else wait for condition to be met
+        val conditionMet = latch.await(timeoutMillis, TimeUnit.MILLISECONDS)
+        assertThat(conditionMet).isTrue()
+    }
 }
 
 @RequiresApi(30)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
index 25a01d3..e9a3958 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldFocusTest.kt
@@ -25,8 +25,11 @@
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.KeyboardHelper
 import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -43,8 +46,10 @@
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.compose.ui.window.Dialog
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
@@ -113,7 +118,7 @@
     }
 
     @Test
-    fun noCrushWhenSwitchingBetweenEnabledFocusedAndDisabledTextField() {
+    fun noCrashWhenSwitchingBetweenEnabledFocusedAndDisabledTextField() {
         val enabled = mutableStateOf(true)
         var focused = false
         val tag = "textField"
@@ -122,9 +127,11 @@
                 value = TextFieldValue(),
                 onValueChange = {},
                 enabled = enabled.value,
-                modifier = Modifier.testTag(tag).onFocusChanged {
-                    focused = it.isFocused
-                }
+                modifier = Modifier
+                    .testTag(tag)
+                    .onFocusChanged {
+                        focused = it.isFocused
+                    }
             )
         }
         // bring enabled text field into focus
@@ -198,4 +205,95 @@
             ).isEqualTo(innerCoordinates!!.size.toSize())
         }
     }
+
+    @Test
+    fun keyboardIsShown_forFieldInActivity_whenFocusRequestedImmediately_fromLaunchedEffect() {
+        keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+            runEffect = {
+                LaunchedEffect(Unit) {
+                    it()
+                }
+            }
+        )
+    }
+
+    @Test
+    fun keyboardIsShown_forFieldInActivity_whenFocusRequestedImmediately_fromDisposableEffect() {
+        keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+            runEffect = {
+                DisposableEffect(Unit) {
+                    it()
+                    onDispose {}
+                }
+            }
+        )
+    }
+
+    // TODO(b/229378542) We can't accurately detect IME visibility from dialogs before API 30 so
+    //  this test can't assert.
+    @SdkSuppress(minSdkVersion = 30)
+    @Test
+    fun keyboardIsShown_forFieldInDialog_whenFocusRequestedImmediately_fromLaunchedEffect() {
+        keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+            runEffect = {
+                LaunchedEffect(Unit) {
+                    it()
+                }
+            },
+            wrapContent = {
+                Dialog(onDismissRequest = {}, content = it)
+            }
+        )
+    }
+
+    // TODO(b/229378542) We can't accurately detect IME visibility from dialogs before API 30 so
+    //  this test can't assert.
+    @SdkSuppress(minSdkVersion = 30)
+    @Test
+    fun keyboardIsShown_forFieldInDialog_whenFocusRequestedImmediately_fromDisposableEffect() {
+        keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+            runEffect = {
+                DisposableEffect(Unit) {
+                    it()
+                    onDispose {}
+                }
+            },
+            wrapContent = {
+                Dialog(onDismissRequest = {}, content = it)
+            }
+        )
+    }
+
+    private fun keyboardIsShown_whenFocusRequestedImmediately_fromEffect(
+        runEffect: @Composable (body: () -> Unit) -> Unit,
+        wrapContent: @Composable (@Composable () -> Unit) -> Unit = { it() }
+    ) {
+        val focusRequester = FocusRequester()
+        val keyboardHelper = KeyboardHelper(rule)
+
+        rule.setContent {
+            wrapContent {
+                keyboardHelper.initialize()
+
+                runEffect {
+                    assertThat(keyboardHelper.isSoftwareKeyboardShown()).isFalse()
+                    focusRequester.requestFocus()
+                }
+
+                BasicTextField(
+                    value = "",
+                    onValueChange = {},
+                    modifier = Modifier.focusRequester(focusRequester)
+                )
+            }
+        }
+
+        keyboardHelper.waitForKeyboardVisibility(visible = true)
+
+        // Ensure the keyboard doesn't leak in to the next test. Can't do this at the start of the
+        // test since the KeyboardHelper won't be initialized until composition runs, and this test
+        // is checking behavior that all happens on the first frame.
+        keyboardHelper.hideKeyboard()
+        keyboardHelper.waitForKeyboardVisibility(visible = false)
+    }
 }
diff --git a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
index 6b5cef6..79247de 100644
--- a/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
+++ b/compose/foundation/foundation/src/androidMain/kotlin/androidx/compose/foundation/Clickable.android.kt
@@ -19,37 +19,13 @@
 import android.view.KeyEvent.KEYCODE_DPAD_CENTER
 import android.view.KeyEvent.KEYCODE_ENTER
 import android.view.KeyEvent.KEYCODE_NUMPAD_ENTER
-import android.view.View
 import android.view.ViewConfiguration
-import android.view.ViewGroup
-import androidx.compose.runtime.Composable
 import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown
 import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp
 import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.nativeKeyCode
 import androidx.compose.ui.input.key.type
-import androidx.compose.ui.platform.LocalView
-
-@Composable
-internal actual fun isComposeRootInScrollableContainer(): () -> Boolean {
-    val view = LocalView.current
-    return {
-        view.isInScrollableViewGroup()
-    }
-}
-
-// Copied from View#isInScrollingContainer() which is @hide
-private fun View.isInScrollableViewGroup(): Boolean {
-    var p = parent
-    while (p != null && p is ViewGroup) {
-        if (p.shouldDelayChildPressedState()) {
-            return true
-        }
-        p = p.parent
-    }
-    return false
-}
 
 internal actual val TapIndicationDelay: Long = ViewConfiguration.getTapTimeout().toLong()
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
index b249514..64643f1 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.foundation
 
-import androidx.compose.foundation.gestures.ModifierLocalScrollableContainer
 import androidx.compose.foundation.gestures.PressGestureScope
 import androidx.compose.foundation.gestures.detectTapAndPress
 import androidx.compose.foundation.gestures.detectTapGestures
@@ -38,8 +37,8 @@
 import androidx.compose.ui.input.key.key
 import androidx.compose.ui.input.key.onKeyEvent
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.modifier.ModifierLocalConsumer
-import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.semantics.Role
 import androidx.compose.ui.semantics.disabled
@@ -144,11 +143,8 @@
                 currentKeyPressInteractions
             )
         }
-        val isRootInScrollableContainer = isComposeRootInScrollableContainer()
-        val isClickableInScrollableContainer = remember { mutableStateOf(true) }
-        val delayPressInteraction = rememberUpdatedState {
-            isClickableInScrollableContainer.value || isRootInScrollableContainer()
-        }
+
+        val delayPressInteraction = remember { mutableStateOf({ true }) }
         val centreOffset = remember { mutableStateOf(Offset.Zero) }
 
         val gesture = Modifier.pointerInput(interactionSource, enabled) {
@@ -168,18 +164,9 @@
             )
         }
         Modifier
-            .then(
-                remember {
-                    object : ModifierLocalConsumer {
-                        override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
-                            with(scope) {
-                                isClickableInScrollableContainer.value =
-                                    ModifierLocalScrollableContainer.current
-                            }
-                        }
-                    }
-                }
-            )
+            .consumeScrollContainerInfo { scrollContainerInfo ->
+                delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
+            }
             .genericClickableWithoutGesture(
                 gestureModifiers = gesture,
                 interactionSource = interactionSource,
@@ -330,13 +317,9 @@
                 currentKeyPressInteractions
             )
         }
-        val isRootInScrollableContainer = isComposeRootInScrollableContainer()
-        val isClickableInScrollableContainer = remember { mutableStateOf(true) }
-        val delayPressInteraction = rememberUpdatedState {
-            isClickableInScrollableContainer.value || isRootInScrollableContainer()
-        }
-        val centreOffset = remember { mutableStateOf(Offset.Zero) }
 
+        val delayPressInteraction = remember { mutableStateOf({ true }) }
+        val centreOffset = remember { mutableStateOf(Offset.Zero) }
         val gesture =
             Modifier.pointerInput(interactionSource, hasLongClick, hasDoubleClick, enabled) {
                 centreOffset.value = size.center.toOffset()
@@ -365,18 +348,6 @@
                 )
             }
         Modifier
-            .then(
-                remember {
-                    object : ModifierLocalConsumer {
-                        override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
-                            with(scope) {
-                                isClickableInScrollableContainer.value =
-                                    ModifierLocalScrollableContainer.current
-                            }
-                        }
-                    }
-                }
-            )
             .genericClickableWithoutGesture(
                 gestureModifiers = gesture,
                 interactionSource = interactionSource,
@@ -391,6 +362,9 @@
                 onLongClick = onLongClick,
                 onClick = onClick
             )
+            .consumeScrollContainerInfo { scrollContainerInfo ->
+                delayPressInteraction.value = { scrollContainerInfo?.canScroll() == true }
+            }
     },
     inspectorInfo = debugInspectorInfo {
         name = "combinedClickable"
@@ -475,20 +449,6 @@
 internal expect val TapIndicationDelay: Long
 
 /**
- * Returns a lambda that calculates whether the root Compose layout node is hosted in a scrollable
- * container outside of Compose. On Android this will be whether the root View is in a scrollable
- * ViewGroup, as even if nothing in the Compose part of the hierarchy is scrollable, if the View
- * itself is in a scrollable container, we still want to delay presses in case presses in Compose
- * convert to a scroll outside of Compose.
- *
- * Combine this with [ModifierLocalScrollableContainer], which returns whether a [Modifier] is
- * within a scrollable Compose layout, to calculate whether this modifier is within some form of
- * scrollable container, and hence should delay presses.
- */
-@Composable
-internal expect fun isComposeRootInScrollableContainer(): () -> Boolean
-
-/**
  * Whether the specified [KeyEvent] should trigger a press for a clickable component.
  */
 internal expect val KeyEvent.isPress: Boolean
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
index 9b19cf5..193eaab 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ExperimentalFoundationApi.kt
@@ -20,4 +20,5 @@
     "This foundation API is experimental and is likely to change or be removed in the " +
         "future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalFoundationApi
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
index 0e0fe4a..19a79c5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/InternalFoundationApi.kt
@@ -21,4 +21,5 @@
     AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
     AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalFoundationApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 18ca13c..9c1374a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -30,6 +30,7 @@
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -148,6 +149,10 @@
     override val isScrollInProgress: Boolean
         get() = scrollableState.isScrollInProgress
 
+    override val canScrollForward: Boolean by derivedStateOf { value < maxValue }
+
+    override val canScrollBackward: Boolean by derivedStateOf { value > 0 }
+
     /**
      * Scroll to position in pixels with animation.
      *
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
index 6507207..161c854 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt
@@ -24,6 +24,7 @@
 import androidx.compose.foundation.gestures.DragEvent.DragStopped
 import androidx.compose.foundation.interaction.DragInteraction
 import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -52,7 +53,6 @@
 import kotlinx.coroutines.channels.SendChannel
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.isActive
-import androidx.compose.foundation.internal.JvmDefaultWithCompatibility
 
 /**
  * State of [draggable]. Allows for a granular control of how deltas are consumed by the user as
@@ -255,6 +255,7 @@
             }
         }
     }
+
     Modifier.pointerInput(orientation, enabled, reverseDirection) {
         if (!enabled) return@pointerInput
         coroutineScope {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index 7aac595..b89a959 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -37,6 +37,7 @@
 import androidx.compose.ui.MotionDurationScale
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
@@ -48,8 +49,7 @@
 import androidx.compose.ui.input.pointer.PointerEventType
 import androidx.compose.ui.input.pointer.PointerType
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.modifier.ModifierLocalProvider
-import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.Density
@@ -172,7 +172,6 @@
                 overscrollEffect,
                 enabled
             )
-            .then(if (enabled) ModifierLocalScrollableContainerProvider else Modifier)
     }
 )
 
@@ -266,6 +265,14 @@
     val draggableState = remember { ScrollDraggableState(scrollLogic) }
     val scrollConfig = platformScrollConfig()
 
+    val scrollContainerInfo = remember(orientation, enabled) {
+        object : ScrollContainerInfo {
+            override fun canScrollHorizontally() = enabled && orientation == Horizontal
+
+            override fun canScrollVertically() = enabled && orientation == Orientation.Vertical
+        }
+    }
+
     return draggable(
         draggableState,
         orientation = orientation,
@@ -282,6 +289,7 @@
     )
         .mouseWheelScroll(scrollLogic, scrollConfig)
         .nestedScroll(nestedScrollConnection, nestedScrollDispatcher.value)
+        .provideScrollContainerInfo(scrollContainerInfo)
 }
 
 private fun Modifier.mouseWheelScroll(
@@ -580,21 +588,9 @@
     }
 }
 
-// TODO: b/203141462 - make this public and move it to ui
-/**
- * Whether this modifier is inside a scrollable container, provided by [Modifier.scrollable].
- * Defaults to false.
- */
-internal val ModifierLocalScrollableContainer = modifierLocalOf { false }
-
-private object ModifierLocalScrollableContainerProvider : ModifierLocalProvider<Boolean> {
-    override val key = ModifierLocalScrollableContainer
-    override val value = true
-}
-
 private const val DefaultScrollMotionDurationScaleFactor = 1f
 
 private val DefaultScrollMotionDurationScale = object : MotionDurationScale {
     override val scaleFactor: Float
         get() = DefaultScrollMotionDurationScaleFactor
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableState.kt
index 9d469a4..9bb6e31 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/ScrollableState.kt
@@ -78,6 +78,34 @@
      * not.
      */
     val isScrollInProgress: Boolean
+
+    /**
+     * Whether this [ScrollableState] can scroll forward (consume a positive delta). This is
+     * typically false if the scroll position is equal to its maximum value, and true otherwise.
+     *
+     * Note that `true` here does not imply that delta *will* be consumed - the ScrollableState may
+     * decide not to handle the incoming delta (such as if it is already being scrolled separately).
+     * Additionally, for backwards compatibility with previous versions of ScrollableState this
+     * value defaults to `true`.
+     *
+     * @sample androidx.compose.foundation.samples.CanScrollSample
+     */
+    val canScrollForward: Boolean
+        get() = true
+
+    /**
+     * Whether this [ScrollableState] can scroll backward (consume a negative delta). This is
+     * typically false if the scroll position is equal to its minimum value, and true otherwise.
+     *
+     * Note that `true` here does not imply that delta *will* be consumed - the ScrollableState may
+     * decide not to handle the incoming delta (such as if it is already being scrolled separately).
+     * Additionally, for backwards compatibility with previous versions of ScrollableState this
+     * value defaults to `true`.
+     *
+     * @sample androidx.compose.foundation.samples.CanScrollSample
+     */
+    val canScrollBackward: Boolean
+        get() = true
 }
 
 /**
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
index edb54bf..036babe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/LazyListSnapLayoutInfoProvider.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.foundation.gestures.snapping
 
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.splineBasedDecay
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.gestures.FlingBehavior
 import androidx.compose.foundation.gestures.Orientation
@@ -27,6 +30,8 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastSumBy
+import kotlin.math.absoluteValue
+import kotlin.math.sign
 
 /**
  * A [SnapLayoutInfoProvider] for LazyLists.
@@ -48,8 +53,18 @@
     private val layoutInfo: LazyListLayoutInfo
         get() = lazyListState.layoutInfo
 
-    // Single page snapping is the default
-    override fun Density.calculateApproachOffset(initialVelocity: Float): Float = 0f
+    // Decayed page snapping is the default
+    override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+        val decayAnimationSpec: DecayAnimationSpec<Float> = splineBasedDecay(this)
+        val offset =
+            decayAnimationSpec.calculateTargetValue(NoDistance, initialVelocity).absoluteValue
+        val finalDecayOffset = (offset - calculateSnapStepSize()).coerceAtLeast(0f)
+        return if (finalDecayOffset == 0f) {
+            finalDecayOffset
+        } else {
+            finalDecayOffset * initialVelocity.sign
+        }
+    }
 
     override fun Density.calculateSnappingOffsetBounds(): ClosedFloatingPointRange<Float> {
         var lowerBoundOffset = Float.NEGATIVE_INFINITY
@@ -73,7 +88,7 @@
         return lowerBoundOffset.rangeTo(upperBoundOffset)
     }
 
-    override fun Density.snapStepSize(): Float = with(layoutInfo) {
+    override fun Density.calculateSnapStepSize(): Float = with(layoutInfo) {
         if (visibleItemsInfo.isNotEmpty()) {
             visibleItemsInfo.fastSumBy { it.size } / visibleItemsInfo.size.toFloat()
         } else {
@@ -83,7 +98,7 @@
 }
 
 /**
- * Create and remember a FlingBehavior for single page snapping in Lazy Lists. This will snap
+ * Create and remember a FlingBehavior for decayed snapping in Lazy Lists. This will snap
  * the item's center to the center of the viewport.
  *
  * @param lazyListState The [LazyListState] from the LazyList where this [FlingBehavior] will
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
index 0d9cfc7..9d632b2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapFlingBehavior.kt
@@ -41,6 +41,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import kotlin.math.abs
+import kotlin.math.absoluteValue
 import kotlin.math.sign
 
 /**
@@ -100,12 +101,14 @@
     }
 
     private suspend fun ScrollScope.shortSnap(velocity: Float) {
+        debugLog { "Short Snapping" }
         val closestOffset = findClosestOffset(0f, snapLayoutInfoProvider, density)
         val animationState = AnimationState(NoDistance, velocity)
         animateSnap(closestOffset, closestOffset, animationState, snapAnimationSpec)
     }
 
     private suspend fun ScrollScope.longSnap(initialVelocity: Float) {
+        debugLog { "Long Snapping" }
         val initialOffset =
             with(snapLayoutInfoProvider) { density.calculateApproachOffset(initialVelocity) }.let {
                 abs(it) * sign(initialVelocity) // ensure offset sign is correct
@@ -113,7 +116,14 @@
 
         val (remainingOffset, animationState) = runApproach(initialOffset, initialVelocity)
 
-        animateSnap(remainingOffset, remainingOffset, animationState, snapAnimationSpec)
+        debugLog { "Settling Final Bound=$remainingOffset" }
+
+        animateSnap(
+            remainingOffset,
+            remainingOffset,
+            animationState.copy(value = 0f),
+            snapAnimationSpec
+        )
     }
 
     private suspend fun ScrollScope.runApproach(
@@ -123,8 +133,10 @@
 
         val animation =
             if (isDecayApproachPossible(offset = initialTargetOffset, velocity = initialVelocity)) {
+                debugLog { "High Velocity Approach" }
                 HighVelocityApproachAnimation(highVelocityAnimationSpec)
             } else {
+                debugLog { "Low Velocity Approach" }
                 LowVelocityApproachAnimation(
                     lowVelocityAnimationSpec,
                     snapLayoutInfoProvider,
@@ -149,7 +161,8 @@
         velocity: Float
     ): Boolean {
         val decayOffset = highVelocityAnimationSpec.calculateTargetValue(NoDistance, velocity)
-        return abs(decayOffset) > abs(offset)
+        val snapStepSize = with(snapLayoutInfoProvider) { density.calculateSnapStepSize() }
+        return decayOffset.absoluteValue >= (offset.absoluteValue + snapStepSize)
     }
 
     override fun equals(other: Any?): Boolean {
@@ -267,6 +280,7 @@
                 lowerBound
             }
         }
+
         1f -> upperBound
         -1f -> lowerBound
         else -> NoDistance
@@ -377,7 +391,9 @@
     ): AnimationState<Float, AnimationVector1D> {
         val animationState = AnimationState(initialValue = 0f, initialVelocity = velocity)
         val targetOffset =
-            (abs(offset) + with(layoutInfoProvider) { density.snapStepSize() }) * sign(velocity)
+            (abs(offset) + with(layoutInfoProvider) { density.calculateSnapStepSize() }) * sign(
+                velocity
+            )
         return with(scope) {
             animateSnap(
                 targetOffset = targetOffset,
@@ -406,4 +422,11 @@
 
 internal val MinFlingVelocityDp = 400.dp
 internal const val NoDistance = 0f
-internal const val NoVelocity = 0f
\ No newline at end of file
+internal const val NoVelocity = 0f
+private const val DEBUG = false
+
+private inline fun debugLog(generateMsg: () -> String) {
+    if (DEBUG) {
+        println("SnapFlingBehavior: ${generateMsg()}")
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
index 9b6ca10..71dea06 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/snapping/SnapLayoutInfoProvider.kt
@@ -29,9 +29,9 @@
 @ExperimentalFoundationApi
 interface SnapLayoutInfoProvider {
     /**
-     * The minimum offset that snapping will use to animate. (e.g. an item size)
+     * The minimum offset that snapping will use to animate.(e.g. an item size)
      */
-    fun Density.snapStepSize(): Float
+    fun Density.calculateSnapStepSize(): Float
 
     /**
      * Calculate the distance to navigate before settling into the next snapping bound.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
index b1e300d..eadd33f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemPlacementAnimator.kt
@@ -71,7 +71,7 @@
         positionedItems: MutableList<LazyListPositionedItem>,
         itemProvider: LazyMeasuredItemProvider
     ) {
-        if (!positionedItems.fastAny { it.hasAnimations }) {
+        if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
             // no animations specified - no work needed
             reset()
             return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
index 4665785..cc6ac54 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListMeasure.kt
@@ -149,8 +149,10 @@
         // then composing visible items forward until we fill the whole viewport.
         // we want to have at least one item in visibleItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
-        while ((currentMainAxisOffset <= maxMainAxis || visibleItems.isEmpty()) &&
-            index.value < itemsCount
+        while (index.value < itemsCount &&
+            (currentMainAxisOffset < maxMainAxis ||
+                currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+                visibleItems.isEmpty())
         ) {
             val measuredItem = itemProvider.getAndMeasure(index)
             currentMainAxisOffset += measuredItem.sizeWithSpacings
@@ -302,7 +304,7 @@
         return LazyListMeasureResult(
             firstVisibleItem = firstItem,
             firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
-            canScrollForward = currentMainAxisOffset > maxOffset,
+            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index ffb504c..cb7e890 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -197,6 +197,7 @@
      */
     internal var remeasurement: Remeasurement? by mutableStateOf(null)
         private set
+
     /**
      * The modifier which provides [remeasurement].
      */
@@ -267,8 +268,9 @@
     override val isScrollInProgress: Boolean
         get() = scrollableState.isScrollInProgress
 
-    private var canScrollBackward: Boolean = false
-    internal var canScrollForward: Boolean = false
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
         private set
 
     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 351a8a1..d8f4f5f 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -76,7 +76,7 @@
         measuredItemProvider: LazyMeasuredItemProvider,
         spanLayoutProvider: LazyGridSpanLayoutProvider
     ) {
-        if (!positionedItems.fastAny { it.hasAnimations }) {
+        if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
             // no animations specified - no work needed
             reset()
             return
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
index b1ebdd0..5eb6c73 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridMeasure.kt
@@ -139,7 +139,11 @@
         // then composing visible lines forward until we fill the whole viewport.
         // we want to have at least one line in visibleItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
-        while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
+        while (index.value < itemsCount &&
+            (currentMainAxisOffset < maxMainAxis ||
+                currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+                visibleLines.isEmpty())
+        ) {
             val measuredLine = measuredLineProvider.getAndMeasure(index)
             if (measuredLine.isEmpty()) {
                 --index
@@ -251,7 +255,7 @@
         return LazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = currentMainAxisOffset > maxOffset,
+            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
index 428f04e..a19e3d2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt
@@ -273,8 +273,9 @@
     override val isScrollInProgress: Boolean
         get() = scrollableState.isScrollInProgress
 
-    private var canScrollBackward: Boolean = false
-    internal var canScrollForward: Boolean = false
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
         private set
 
     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index 511da2d0..e0f601d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -333,7 +333,10 @@
         // we want to have at least one item in visibleItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
         while (
-            currentItemOffsets.any { it <= maxOffset } || measuredItems.all { it.isEmpty() }
+            currentItemOffsets.any {
+                it < maxOffset ||
+                    it <= 0 // filling beforeContentPadding area
+            } || measuredItems.all { it.isEmpty() }
         ) {
             val currentLaneIndex = currentItemOffsets.indexOfMinValue()
             val nextItemIndex =
@@ -542,7 +545,8 @@
         // only scroll backward if the first item is not on screen or fully visible
         val canScrollBackward = !(firstItemIndices[0] == 0 && firstItemOffsets[0] <= 0)
         // only scroll forward if the last item is not on screen or fully visible
-        val canScrollForward = currentItemOffsets.any { it > mainAxisAvailableSize }
+        val canScrollForward = currentItemOffsets.any { it > mainAxisAvailableSize } ||
+            currentItemIndices.all { it < itemCount - 1 }
 
         @Suppress("UNCHECKED_CAST")
         return LazyStaggeredGridMeasureResult(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index b6fbafa..cd08ee3 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -29,9 +29,11 @@
 import androidx.compose.foundation.lazy.layout.animateScrollToItem
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
 import androidx.compose.ui.unit.Constraints
@@ -133,8 +135,10 @@
     /** storage for lane assignments for each item for consistent scrolling in both directions **/
     internal val spans = LazyStaggeredGridSpans()
 
-    internal var canScrollForward = true
-    private var canScrollBackward = true
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
+        private set
 
     /** implementation of [LazyAnimateScrollScope] scope required for [animateScrollToItem] **/
     private val animateScrollScope = LazyStaggeredGridAnimateScrollScope(this)
@@ -147,6 +151,12 @@
         }
     }
 
+    /**
+     * Only used for testing to disable prefetching when needed to test the main logic.
+     */
+    /*@VisibleForTesting*/
+    internal var prefetchingEnabled: Boolean = true
+
     /** prefetch state used for precomputing items in the direction of scroll **/
     internal val prefetchState: LazyLayoutPrefetchState = LazyLayoutPrefetchState()
 
@@ -218,7 +228,9 @@
         if (abs(scrollToBeConsumed) > 0.5f) {
             val preScrollToBeConsumed = scrollToBeConsumed
             remeasurement?.forceRemeasure()
-            notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+            if (prefetchingEnabled) {
+                notifyPrefetch(preScrollToBeConsumed - scrollToBeConsumed)
+            }
         }
 
         // here scrollToBeConsumed is already consumed during the forceRemeasure invocation
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
new file mode 100644
index 0000000..6169203
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt
@@ -0,0 +1,500 @@
+/*
+ * 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.foundation.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.LinearOutSlowInEasing
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior
+import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyList
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.filter
+
+/**
+ * A Pager that scrolls horizontally. Pages are lazily placed in accordance to the available
+ * viewport size. You can use [beyondBoundsPageCount] to place more pages before and after the
+ * visible pages.
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be applied to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using Lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param verticalAlignment How pages are aligned vertically in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun HorizontalPager(
+    pageCount: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    pageSize: PageSize = PageSize.Fill,
+    beyondBoundsPageCount: Int = 0,
+    pageSpacing: Dp = 0.dp,
+    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+    userScrollEnabled: Boolean = true,
+    reverseLayout: Boolean = false,
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    Pager(
+        modifier = modifier,
+        state = state,
+        pageCount = pageCount,
+        pageSpacing = pageSpacing,
+        userScrollEnabled = userScrollEnabled,
+        orientation = Orientation.Horizontal,
+        verticalAlignment = verticalAlignment,
+        reverseLayout = reverseLayout,
+        contentPadding = contentPadding,
+        beyondBoundsPageCount = beyondBoundsPageCount,
+        pageSize = pageSize,
+        flingBehavior = flingBehavior,
+        pageContent = pageContent
+    )
+}
+
+/**
+ * A Pager that scrolls vertically. Tha backing mechanism for this is a LazyList, therefore
+ * pages are lazily placed in accordance to the available viewport size. You can use
+ * [beyondBoundsPageCount] to place more pages before and after the visible pages.
+ *
+ * @param pageCount The number of pages this Pager will contain
+ * @param modifier A modifier instance to be apply to this Pager outer layout
+ * @param state The state to control this pager
+ * @param contentPadding a padding around the whole content. This will add padding for the
+ * content after it has been clipped, which is not possible via [modifier] param. You can use it
+ * to add a padding before the first page or after the last one. Use [pageSpacing] to add spacing
+ * between the pages.
+ * @param pageSize Use this to change how the pages will look like inside this pager.
+ * @param beyondBoundsPageCount Pages to load before and after the list of visible
+ * pages. Note: Be aware that using a large value for [beyondBoundsPageCount] will cause a lot of
+ * pages to be composed, measured and placed which will defeat the purpose of using Lazy loading.
+ * This should be used as an optimization to pre-load a couple of pages before and after the visible
+ * ones.
+ * @param pageSpacing The amount of space to be used to separate the pages in this Pager
+ * @param horizontalAlignment How pages are aligned horizontally in this Pager.
+ * @param flingBehavior The [FlingBehavior] to be used for post scroll gestures.
+ * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions
+ * is allowed. You can still scroll programmatically using [PagerState.scroll] even when it is
+ * disabled.
+ * @param reverseLayout reverse the direction of scrolling and layout.
+ * @param pageContent This Pager's page Composable.
+ */
+@Composable
+@ExperimentalFoundationApi
+internal fun VerticalPager(
+    pageCount: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    pageSize: PageSize = PageSize.Fill,
+    beyondBoundsPageCount: Int = 0,
+    pageSpacing: Dp = 0.dp,
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state = state),
+    userScrollEnabled: Boolean = true,
+    reverseLayout: Boolean = false,
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    Pager(
+        modifier = modifier,
+        state = state,
+        pageCount = pageCount,
+        pageSpacing = pageSpacing,
+        horizontalAlignment = horizontalAlignment,
+        userScrollEnabled = userScrollEnabled,
+        orientation = Orientation.Vertical,
+        reverseLayout = reverseLayout,
+        contentPadding = contentPadding,
+        beyondBoundsPageCount = beyondBoundsPageCount,
+        pageSize = pageSize,
+        flingBehavior = flingBehavior,
+        pageContent = pageContent
+    )
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+internal fun Pager(
+    modifier: Modifier,
+    state: PagerState,
+    pageCount: Int,
+    pageSize: PageSize,
+    pageSpacing: Dp,
+    orientation: Orientation,
+    beyondBoundsPageCount: Int,
+    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    contentPadding: PaddingValues,
+    flingBehavior: FlingBehavior,
+    userScrollEnabled: Boolean,
+    reverseLayout: Boolean,
+    pageContent: @Composable (page: Int) -> Unit
+) {
+    require(beyondBoundsPageCount >= 0) {
+        "beyondBoundsPageCount should be greater than or equal to 0, " +
+            "you selected $beyondBoundsPageCount"
+    }
+
+    val isVertical = orientation == Orientation.Vertical
+    val density = LocalDensity.current
+    val layoutDirection = LocalLayoutDirection.current
+    val calculatedContentPaddings = remember(contentPadding, orientation, layoutDirection) {
+        calculateContentPaddings(
+            contentPadding,
+            orientation,
+            layoutDirection
+        )
+    }
+
+    LaunchedEffect(density, state, pageSpacing) {
+        with(density) { state.pageSpacing = pageSpacing.roundToPx() }
+    }
+
+    LaunchedEffect(state) {
+        snapshotFlow { state.isScrollInProgress }
+            .filter { !it }
+            .drop(1) // Initial scroll is false
+            .collect { state.updateOnScrollStopped() }
+    }
+
+    BoxWithConstraints(modifier = modifier) {
+        val mainAxisSize = if (isVertical) constraints.maxHeight else constraints.maxWidth
+        // Calculates how pages are shown across the main axis
+        val pageAvailableSize = remember(
+            density,
+            mainAxisSize,
+            pageSpacing,
+            calculatedContentPaddings
+        ) {
+            with(density) {
+                val pageSpacingPx = pageSpacing.roundToPx()
+                val contentPaddingPx = calculatedContentPaddings.roundToPx()
+                with(pageSize) {
+                    density.calculateMainAxisPageSize(
+                        mainAxisSize - contentPaddingPx,
+                        pageSpacingPx
+                    )
+                }.toDp()
+            }
+        }
+
+        val horizontalAlignmentForSpacedArrangement =
+            if (!reverseLayout) Alignment.Start else Alignment.End
+        val verticalAlignmentForSpacedArrangement =
+            if (!reverseLayout) Alignment.Top else Alignment.Bottom
+
+        LazyList(
+            modifier = Modifier,
+            state = state.lazyListState,
+            contentPadding = contentPadding,
+            flingBehavior = flingBehavior,
+            horizontalAlignment = horizontalAlignment,
+            horizontalArrangement = Arrangement.spacedBy(
+                pageSpacing,
+                horizontalAlignmentForSpacedArrangement
+            ),
+            verticalArrangement = Arrangement.spacedBy(
+                pageSpacing,
+                verticalAlignmentForSpacedArrangement
+            ),
+            verticalAlignment = verticalAlignment,
+            isVertical = isVertical,
+            reverseLayout = reverseLayout,
+            userScrollEnabled = userScrollEnabled,
+            beyondBoundsItemCount = beyondBoundsPageCount
+        ) {
+            items(pageCount) {
+                val pageMainAxisSizeModifier = if (isVertical) {
+                    Modifier.height(pageAvailableSize)
+                } else {
+                    Modifier.width(pageAvailableSize)
+                }
+                Box(
+                    modifier = Modifier.then(pageMainAxisSizeModifier),
+                    contentAlignment = Alignment.Center
+                ) {
+                    pageContent(it)
+                }
+            }
+        }
+    }
+}
+
+private fun calculateContentPaddings(
+    contentPadding: PaddingValues,
+    orientation: Orientation,
+    layoutDirection: LayoutDirection
+): Dp {
+
+    val startPadding = if (orientation == Orientation.Vertical) {
+        contentPadding.calculateTopPadding()
+    } else {
+        contentPadding.calculateLeftPadding(layoutDirection)
+    }
+
+    val endPadding = if (orientation == Orientation.Vertical) {
+        contentPadding.calculateBottomPadding()
+    } else {
+        contentPadding.calculateRightPadding(layoutDirection)
+    }
+
+    return startPadding + endPadding
+}
+
+/**
+ * This is used to determine how Pages are laid out in [Pager]. By changing the size of the pages
+ * one can change how many pages are shown.
+ */
+@ExperimentalFoundationApi
+internal interface PageSize {
+
+    /**
+     * Based on [availableSpace] pick a size for the pages
+     * @param availableSpace The amount of space the pages in this Pager can use.
+     * @param pageSpacing The amount of space used to separate pages.
+     */
+    fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int
+
+    /**
+     * Pages take up the whole Pager size.
+     */
+    @ExperimentalFoundationApi
+    object Fill : PageSize {
+        override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+            return availableSpace
+        }
+    }
+
+    /**
+     * Multiple pages in a viewport
+     * @param pageSize A fixed size for pages
+     */
+    @ExperimentalFoundationApi
+    class Fixed(val pageSize: Dp) : PageSize {
+        override fun Density.calculateMainAxisPageSize(availableSpace: Int, pageSpacing: Int): Int {
+            return pageSize.roundToPx()
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+internal object PagerDefaults {
+
+    /**
+     * @param state The [PagerState] that controls the which to which this FlingBehavior will
+     * be applied to.
+     * @param pagerSnapDistance A way to control the snapping destination for this [Pager].
+     * The default behavior will result in any fling going to the next page in the direction of the
+     * fling (if the fling has enough velocity, otherwise we'll bounce back). Use
+     * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to
+     * fling after scrolling is finished and fling has started.
+     * @param lowVelocityAnimationSpec The animation spec used to approach the target offset. When
+     * the fling velocity is not large enough. Large enough means large enough to naturally decay.
+     * @param highVelocityAnimationSpec The animation spec used to approach the target offset. When
+     * the fling velocity is large enough. Large enough means large enough to naturally decay.
+     * @param snapAnimationSpec The animation spec used to finally snap to the position.
+     *
+     * @return An instance of [FlingBehavior] that will perform Snapping to the next page. The
+     * animation will be governed by the post scroll velocity and we'll use either
+     * [lowVelocityAnimationSpec] or [highVelocityAnimationSpec] to approach the snapped position
+     * and the last step of the animation will be performed by [snapAnimationSpec].
+     */
+    @Composable
+    fun flingBehavior(
+        state: PagerState,
+        pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1),
+        lowVelocityAnimationSpec: AnimationSpec<Float> = tween(easing = LinearOutSlowInEasing),
+        highVelocityAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+        snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow),
+    ): FlingBehavior {
+        val density = LocalDensity.current
+
+        return remember(
+            lowVelocityAnimationSpec,
+            highVelocityAnimationSpec,
+            snapAnimationSpec,
+            pagerSnapDistance,
+            density
+        ) {
+            val snapLayoutInfoProvider =
+                SnapLayoutInfoProvider(state, pagerSnapDistance, highVelocityAnimationSpec)
+            SnapFlingBehavior(
+                snapLayoutInfoProvider = snapLayoutInfoProvider,
+                lowVelocityAnimationSpec = lowVelocityAnimationSpec,
+                highVelocityAnimationSpec = highVelocityAnimationSpec,
+                snapAnimationSpec = snapAnimationSpec,
+                density = density
+            )
+        }
+    }
+}
+
+/**
+ * [PagerSnapDistance] defines the way the [Pager] will treat the distance between the current
+ * page and the page where it will settle.
+ */
+@ExperimentalFoundationApi
+@Stable
+internal interface PagerSnapDistance {
+
+    /** Provides a chance to change where the [Pager] fling will settle.
+     *
+     * @param startPage The current page right before the fling starts.
+     * @param suggestedTargetPage The proposed target page where this fling will stop. This target
+     * will be the page that will be correctly positioned (snapped) after naturally decaying with
+     * [velocity] using a [DecayAnimationSpec].
+     * @param velocity The initial fling velocity.
+     * @param pageSize The page size for this [Pager].
+     * @param pageSpacing The spacing used between pages.
+     *
+     * @return An updated target page where to settle. Note that this value needs to be between 0
+     * and the total count of pages in this pager. If an invalid value is passed, the pager will
+     * coerce within the valid values.
+     */
+    fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int
+    ): Int
+
+    companion object {
+        /**
+         * Limits the maximum number of pages that can be flung per fling gesture.
+         * @param pages The maximum number of extra pages that can be flung at once.
+         */
+        fun atMost(pages: Int): PagerSnapDistance {
+            require(pages >= 0) {
+                "pages should be greater than or equal to 0. You have used $pages."
+            }
+            return PagerSnapDistanceMaxPages(pages)
+        }
+    }
+}
+
+/**
+ * Limits the maximum number of pages that can be flung per fling gesture.
+ * @param pagesLimit The maximum number of extra pages that can be flung at once.
+ */
+@OptIn(ExperimentalFoundationApi::class)
+internal class PagerSnapDistanceMaxPages(private val pagesLimit: Int) : PagerSnapDistance {
+    override fun calculateTargetPage(
+        startPage: Int,
+        suggestedTargetPage: Int,
+        velocity: Float,
+        pageSize: Int,
+        pageSpacing: Int,
+    ): Int {
+        return suggestedTargetPage.coerceIn(startPage - pagesLimit, startPage + pagesLimit)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        return if (other is PagerSnapDistanceMaxPages) {
+            this.pagesLimit == other.pagesLimit
+        } else {
+            false
+        }
+    }
+
+    override fun hashCode(): Int {
+        return pagesLimit.hashCode()
+    }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+private fun SnapLayoutInfoProvider(
+    pagerState: PagerState,
+    pagerSnapDistance: PagerSnapDistance,
+    decayAnimationSpec: DecayAnimationSpec<Float>
+): SnapLayoutInfoProvider {
+    return object : SnapLayoutInfoProvider by SnapLayoutInfoProvider(
+        lazyListState = pagerState.lazyListState,
+        positionInLayout = SnapAlignmentStartToStart
+    ) {
+        override fun Density.calculateApproachOffset(initialVelocity: Float): Float {
+            val effectivePageSize = pagerState.pageSize + pagerState.pageSpacing
+            val initialOffset = pagerState.currentPageOffset * effectivePageSize
+            val animationOffset =
+                decayAnimationSpec.calculateTargetValue(0f, initialVelocity)
+
+            val startPage = pagerState.currentPage
+            val startPageOffset = startPage * effectivePageSize
+
+            val targetOffset =
+                (startPageOffset + initialOffset + animationOffset) / effectivePageSize
+
+            val targetPage = targetOffset.toInt()
+            val correctedTargetPage = pagerSnapDistance.calculateTargetPage(
+                startPage,
+                targetPage,
+                initialVelocity,
+                pagerState.pageSize,
+                pagerState.pageSpacing
+            ).coerceIn(0, pagerState.pageCount)
+
+            val finalOffset = (correctedTargetPage - startPage) * effectivePageSize
+
+            return (finalOffset - initialOffset)
+        }
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
new file mode 100644
index 0000000..2e89671
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt
@@ -0,0 +1,241 @@
+/*
+* 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.foundation.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.gestures.animateScrollBy
+import androidx.compose.foundation.gestures.snapping.calculateDistanceToDesiredSnapPosition
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.util.fastMaxBy
+import kotlin.math.abs
+
+/**
+ * Creates and remember a [PagerState] to be used with a [Pager]
+ *
+ * @param initialPage The pager that should be shown first.
+ * @param initialPageOffset The offset of the initial page with respect to the start of the layout.
+ */
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberPagerState(initialPage: Int = 0, initialPageOffset: Int = 0): PagerState {
+    return rememberSaveable(saver = PagerState.Saver) {
+        PagerState(initialPage = initialPage, initialPageOffset = initialPageOffset)
+    }
+}
+
+/**
+ * The state that can be used to control [VerticalPager] and [HorizontalPager]
+ * @param initialPage The initial page to be displayed
+ * @param initialPageOffset The offset of the initial page with respect to the start of the layout.
+ */
+@ExperimentalFoundationApi
+internal class PagerState(
+    initialPage: Int = 0,
+    initialPageOffset: Int = 0
+) : ScrollableState {
+
+    internal val lazyListState = LazyListState(initialPage, initialPageOffset)
+
+    internal var pageSpacing by mutableStateOf(0)
+
+    internal val pageSize: Int
+        get() = visiblePages.firstOrNull()?.size ?: 0
+
+    private val visiblePages: List<LazyListItemInfo>
+        get() = lazyListState.layoutInfo.visibleItemsInfo
+
+    private val pageAvailableSpace: Int
+        get() = pageSize + pageSpacing
+
+    internal val pageCount: Int
+        get() = lazyListState.layoutInfo.totalItemsCount
+
+    private val closestPageToSnappedPosition: LazyListItemInfo?
+        get() = visiblePages.fastMaxBy {
+            -abs(
+                lazyListState.density.calculateDistanceToDesiredSnapPosition(
+                    lazyListState.layoutInfo,
+                    it,
+                    SnapAlignmentStartToStart
+                )
+            )
+        }
+
+    private val distanceToSnapPosition: Float
+        get() = closestPageToSnappedPosition?.let {
+            lazyListState.density.calculateDistanceToDesiredSnapPosition(
+                lazyListState.layoutInfo,
+                it,
+                SnapAlignmentStartToStart
+            )
+        } ?: 0f
+
+    /**
+     * [InteractionSource] that will be used to dispatch drag events when this
+     * list is being dragged. If you want to know whether the fling (or animated scroll) is in
+     * progress, use [isScrollInProgress].
+     */
+    val interactionSource: InteractionSource get() = lazyListState.interactionSource
+
+    /**
+     * The page that sits closest to the snapped position.
+     */
+    val currentPage: Int by derivedStateOf { closestPageToSnappedPosition?.index ?: 0 }
+
+    private var settledPageState by mutableStateOf(initialPage)
+
+    /**
+     * The page that is currently "settled". This is an animation/gesture unaware page in the sense
+     * that it will not be updated while the pages are being scrolled, but rather when the
+     * animation/scroll settles.
+     */
+    val settledPage: Int by derivedStateOf {
+        if (pageCount == 0) 0 else settledPageState.coerceInPageRange()
+    }
+
+    /**
+     * Indicates how far the current page is to the snapped position, this will vary from
+     * -0.5 (page is offset towards the start of the layout) to 0.5 (page is offset towards the end
+     * of the layout). This is 0.0 if the [currentPage] is in the snapped position. The value will
+     * flip once the current page changes.
+     */
+    val currentPageOffset: Float by derivedStateOf {
+        val currentPagePositionOffset = closestPageToSnappedPosition?.offset ?: 0
+        val pageUsedSpace = pageAvailableSpace.toFloat()
+        ((-currentPagePositionOffset) / (pageUsedSpace)).coerceIn(
+            MinPageOffset, MaxPageOffset
+        )
+    }
+
+    /**
+     * Scroll (jump immediately) to a given [page]
+     * @param page The destination page to scroll to
+     */
+    suspend fun scrollToPage(page: Int) {
+        val targetPage = page.coerceInPageRange()
+        val pageOffsetToCorrectPosition = distanceToSnapPosition.toInt()
+        lazyListState.scrollToItem(targetPage, pageOffsetToCorrectPosition)
+    }
+
+    /**
+     * Scroll animate to a given [page]. If the [page] is too far away from [currentPage] we will
+     * not compose all pages in the way. We will pre-jump to a nearer page, compose and animate
+     * the rest of the pages until [page].
+     * @param page The destination page to scroll to
+     * @param animationSpec An [AnimationSpec] to move between pages. We'll use a [spring] as the
+     * default animation.
+     */
+    suspend fun animateScrollToPage(
+        page: Int,
+        animationSpec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessMediumLow)
+    ) {
+        if (page == currentPage) return
+        var currentPosition = currentPage
+        // If our future page is too far off, that is, outside of the current viewport
+        val firstVisiblePageIndex = visiblePages.first().index
+        val lastVisiblePageIndex = visiblePages.last().index
+        if (((page > currentPage && page > lastVisiblePageIndex) ||
+                (page < currentPage && page < firstVisiblePageIndex)) &&
+            abs(page - currentPage) >= MaxPagesForAnimateScroll
+        ) {
+            val preJumpPosition = if (page > currentPage) {
+                (page - visiblePages.size).coerceAtLeast(currentPosition)
+            } else {
+                page + visiblePages.size.coerceAtMost(currentPosition)
+            }
+            // Pre-jump to 1 viewport away from destination item, if possible
+            scrollToPage(preJumpPosition)
+            currentPosition = preJumpPosition
+        }
+
+        val targetPage = page.coerceInPageRange()
+        val targetOffset = targetPage * pageAvailableSpace
+        val currentOffset = currentPosition * pageAvailableSpace
+        val pageOffsetToSnappedPosition = distanceToSnapPosition
+
+        val displacement = targetOffset - currentOffset + pageOffsetToSnappedPosition
+        lazyListState.animateScrollBy(displacement, animationSpec)
+    }
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) {
+        lazyListState.scroll(scrollPriority, block)
+    }
+
+    override fun dispatchRawDelta(delta: Float): Float {
+        return lazyListState.dispatchRawDelta(delta)
+    }
+
+    override val isScrollInProgress: Boolean
+        get() = lazyListState.isScrollInProgress
+
+    override val canScrollForward: Boolean
+        get() = lazyListState.canScrollForward
+
+    override val canScrollBackward: Boolean
+        get() = lazyListState.canScrollBackward
+
+    private fun Int.coerceInPageRange() = coerceIn(0, pageCount - 1)
+    internal fun updateOnScrollStopped() {
+        settledPageState = currentPage
+    }
+
+    companion object {
+        /**
+         * To keep current page and current page offset saved
+         */
+        val Saver: Saver<PagerState, *> = listSaver(
+            save = {
+                listOf(
+                    it.closestPageToSnappedPosition?.index ?: 0,
+                    it.closestPageToSnappedPosition?.offset ?: 0
+                )
+            },
+            restore = {
+                PagerState(
+                    initialPage = it[0],
+                    initialPageOffset = it[1]
+                )
+            }
+        )
+    }
+}
+
+private const val MinPageOffset = -0.5f
+private const val MaxPageOffset = 0.5f
+internal val SnapAlignmentStartToStart: Density.(layoutSize: Float, itemSize: Float) -> Float =
+    { _, _ -> 0f }
+private const val MaxPagesForAnimateScroll = 3
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
index f1ff38a..4221b97 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/InternalFoundationTextApi.kt
@@ -24,4 +24,5 @@
     AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
     AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalFoundationTextApi
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
index f5d9e74..0122d8e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
@@ -17,13 +17,16 @@
 package androidx.compose.foundation.text
 
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.rememberScrollableState
 import androidx.compose.foundation.gestures.scrollable
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.offset
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.saveable.listSaver
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.structuralEqualityPolicy
@@ -64,7 +67,7 @@
     // do not reverse direction only in case of RTL in horizontal orientation
     val rtl = LocalLayoutDirection.current == LayoutDirection.Rtl
     val reverseDirection = scrollerPosition.orientation == Orientation.Vertical || !rtl
-    val controller = rememberScrollableState { delta ->
+    val scrollableState = rememberScrollableState { delta ->
         val newOffset = scrollerPosition.offset + delta
         val consumedDelta = when {
             newOffset > scrollerPosition.maximum ->
@@ -75,10 +78,20 @@
         scrollerPosition.offset += consumedDelta
         consumedDelta
     }
+    // TODO: b/255557085 remove when / if rememberScrollableState exposes lambda parameters for
+    //  setting these
+    val wrappedScrollableState = remember(scrollableState, scrollerPosition) {
+        object : ScrollableState by scrollableState {
+            override val canScrollForward by derivedStateOf {
+                scrollerPosition.offset < scrollerPosition.maximum
+            }
+            override val canScrollBackward by derivedStateOf { scrollerPosition.offset > 0f }
+        }
+    }
     val scroll = Modifier.scrollable(
         orientation = scrollerPosition.orientation,
         reverseDirection = reverseDirection,
-        state = controller,
+        state = wrappedScrollableState,
         interactionSource = interactionSource,
         enabled = enabled && scrollerPosition.maximum != 0f
     )
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
index 6d663a8..48eb943 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Clickable.desktop.kt
@@ -19,7 +19,6 @@
 import androidx.compose.foundation.gestures.awaitEachGesture
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -52,9 +51,6 @@
 import androidx.compose.ui.util.fastAll
 import java.awt.event.KeyEvent.VK_ENTER
 
-@Composable
-internal actual fun isComposeRootInScrollableContainer(): () -> Boolean = { false }
-
 // TODO: b/168524931 - should this depend on the input device?
 internal actual val TapIndicationDelay: Long = 0L
 
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 858095c..cfae285 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -369,10 +369,10 @@
 
   @androidx.compose.runtime.Stable public final class DrawerState {
     ctor public DrawerState(androidx.compose.material.DrawerValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DrawerValue,java.lang.Boolean> confirmStateChange);
-    method @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public suspend Object? animateTo(androidx.compose.material.DrawerValue targetValue, androidx.compose.animation.core.AnimationSpec<java.lang.Float> anim, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? close(kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public androidx.compose.material.DrawerValue getCurrentValue();
-    method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.runtime.State<java.lang.Float> getOffset();
+    method @androidx.compose.material.ExperimentalMaterialApi public Float? getOffset();
     method @androidx.compose.material.ExperimentalMaterialApi public androidx.compose.material.DrawerValue getTargetValue();
     method public boolean isAnimationRunning();
     method public boolean isClosed();
@@ -383,7 +383,7 @@
     property public final boolean isAnimationRunning;
     property public final boolean isClosed;
     property public final boolean isOpen;
-    property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.runtime.State<java.lang.Float> offset;
+    property @androidx.compose.material.ExperimentalMaterialApi public final Float? offset;
     property @androidx.compose.material.ExperimentalMaterialApi public final androidx.compose.material.DrawerValue targetValue;
     field public static final androidx.compose.material.DrawerState.Companion Companion;
   }
@@ -413,7 +413,7 @@
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.material.ElevationOverlay> LocalElevationOverlay;
   }
 
-  @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
+  @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterialApi {
   }
 
   @androidx.compose.material.ExperimentalMaterialApi public interface ExposedDropdownMenuBoxScope {
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 8b59f0e..c1678a8 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -47,8 +47,8 @@
 
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate:1.1.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
@@ -106,8 +106,8 @@
 
                 // TODO: remove next 3 dependencies when b/202810604 is fixed
                 implementation("androidx.savedstate:savedstate:1.1.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             desktopMain.dependencies {
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index cd1e8a0..66665e6 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -17,7 +17,6 @@
 package androidx.compose.material
 
 import android.os.SystemClock.sleep
-import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
@@ -27,6 +26,8 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -319,12 +320,12 @@
         rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
 
         // When the drawer state is set to Opened
-        drawerState.animateTo(DrawerValue.Open, TweenSpec())
+        drawerState.open()
         // Then the drawer should be opened
         rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(0.dp)
 
         // When the drawer state is set to Closed
-        drawerState.animateTo(DrawerValue.Closed, TweenSpec())
+        drawerState.close()
         // Then the drawer should be closed
         rule.onNodeWithTag("drawer").assertLeftPositionInRootIsEqualTo(-width)
     }
@@ -1186,4 +1187,74 @@
         topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
         assertEquals(2, topNode.children.size)
     }
+
+    @Test
+    fun modalDrawer_providesScrollableContainerInfo_enabled() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            ModalDrawer(
+                drawerContent = {},
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isTrue()
+    }
+
+    @Test
+    fun modalDrawer_providesScrollableContainerInfo_disabled() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            ModalDrawer(
+                drawerContent = {},
+                gesturesEnabled = false,
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isFalse()
+    }
+
+    @Test
+    fun bottomDrawer_providesScrollableContainerInfo_enabled() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            BottomDrawer(
+                drawerContent = {},
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isTrue()
+    }
+
+    @Test
+    fun bottomDrawer_providesScrollableContainerInfo_disabled() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            BottomDrawer(
+                drawerContent = {},
+                gesturesEnabled = false,
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isFalse()
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
index 6d31620..8d311a0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ModalBottomSheetTest.kt
@@ -25,6 +25,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.testTag
@@ -780,4 +782,44 @@
             assertThat(sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Expanded)
         }
     }
+
+    @Test
+    fun modalBottomSheet_providesScrollableContainerInfo_hidden() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            ModalBottomSheetLayout(
+                sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
+                content = { Box(Modifier.fillMaxSize().testTag(contentTag)) },
+                sheetContent = {
+                    Box(
+                        Modifier.fillMaxSize().consumeScrollContainerInfo {
+                            actualValue = { it!!.canScroll() }
+                        }
+                    )
+                }
+            )
+        }
+
+        assertThat(actualValue()).isFalse()
+    }
+
+    @Test
+    fun modalBottomSheet_providesScrollableContainerInfo_expanded() {
+        var actualValue = { false }
+        rule.setMaterialContent {
+            ModalBottomSheetLayout(
+                sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Expanded),
+                content = { Box(Modifier.fillMaxSize().testTag(contentTag)) },
+                sheetContent = {
+                    Box(
+                        Modifier.fillMaxSize().consumeScrollContainerInfo {
+                            actualValue = { it!!.canScroll() }
+                        }
+                    )
+                }
+            )
+        }
+
+        assertThat(actualValue()).isTrue()
+    }
 }
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 a748549..303ed51 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
@@ -350,9 +350,11 @@
     ) = SwipeableV2State(
         initialValue = initialState,
         positionalThreshold = positionalThreshold,
-        velocityThreshold = velocityThreshold,
-        density = density
-    ).apply { if (anchors != null) updateAnchors(anchors) }
+        velocityThreshold = velocityThreshold
+    ).apply {
+        if (anchors != null) updateAnchors(anchors)
+        this.density = density
+    }
 
     private fun TouchInjectionScope.endEdge(orientation: Orientation) =
         if (orientation == Orientation.Horizontal) right else bottom
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 4099380..12f80a5 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -32,7 +32,6 @@
 import androidx.compose.foundation.layout.sizeIn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -44,8 +43,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -58,10 +59,10 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.launch
 import kotlin.math.max
 import kotlin.math.roundToInt
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.launch
 
 /**
  * Possible values of [DrawerState].
@@ -113,10 +114,11 @@
     confirmStateChange: (DrawerValue) -> Boolean = { true }
 ) {
 
-    internal val swipeableState = SwipeableState(
+    internal val swipeableState = SwipeableV2State(
         initialValue = initialValue,
         animationSpec = AnimationSpec,
-        confirmStateChange = confirmStateChange
+        confirmValueChange = confirmStateChange,
+        velocityThreshold = DrawerVelocityThreshold
     )
 
     /**
@@ -158,7 +160,7 @@
      *
      * @return the reason the open animation ended
      */
-    suspend fun open() = animateTo(DrawerValue.Open, AnimationSpec)
+    suspend fun open() = swipeableState.animateTo(DrawerValue.Open)
 
     /**
      * Close the drawer with animation and suspend until it if fully closed or animation has been
@@ -167,17 +169,25 @@
      *
      * @return the reason the close animation ended
      */
-    suspend fun close() = animateTo(DrawerValue.Closed, AnimationSpec)
+    suspend fun close() = swipeableState.animateTo(DrawerValue.Closed)
 
     /**
      * Set the state of the drawer with specific animation
      *
      * @param targetValue The new value to animate to.
-     * @param anim The animation that will be used to animate to the new value.
+     * @param anim Set the state of the drawer with specific animation
      */
     @ExperimentalMaterialApi
-    suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec<Float>) {
-        swipeableState.animateTo(targetValue, anim)
+    @Deprecated(
+        message = "This method has been replaced by the open and close methods. The animation " +
+            "spec is now an implementation detail of ModalDrawer.",
+        level = DeprecationLevel.ERROR
+    )
+    suspend fun animateTo(
+        targetValue: DrawerValue,
+        @Suppress("UNUSED_PARAMETER") anim: AnimationSpec<Float>
+    ) {
+        swipeableState.animateTo(targetValue)
     }
 
     /**
@@ -204,14 +214,19 @@
         get() = swipeableState.targetValue
 
     /**
-     * The current position (in pixels) of the drawer sheet.
+     * The current position (in pixels) of the drawer sheet, or null before the offset is
+     * initialized.
+     * @see [SwipeableV2State.offset] for more information.
      */
     @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @get:Suppress("AutoBoxing")
     @ExperimentalMaterialApi
     @get:ExperimentalMaterialApi
-    val offset: State<Float>
+    val offset: Float?
         get() = swipeableState.offset
 
+    internal fun requireOffset(): Float = swipeableState.requireOffset()
+
     companion object {
         /**
          * The default [Saver] implementation for [DrawerState].
@@ -384,6 +399,14 @@
     content: @Composable () -> Unit
 ) {
     val scope = rememberCoroutineScope()
+
+    val containerInfo = remember(gesturesEnabled) {
+        object : ScrollContainerInfo {
+            override fun canScrollHorizontally() = gesturesEnabled
+
+            override fun canScrollVertically() = false
+        }
+    }
     BoxWithConstraints(modifier.fillMaxSize()) {
         val modalDrawerConstraints = constraints
         // TODO : think about Infinite max bounds case
@@ -394,19 +417,25 @@
         val minValue = -modalDrawerConstraints.maxWidth.toFloat()
         val maxValue = 0f
 
-        val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
         val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
         Box(
-            Modifier.swipeable(
-                state = drawerState.swipeableState,
-                anchors = anchors,
-                thresholds = { _, _ -> FractionalThreshold(0.5f) },
-                orientation = Orientation.Horizontal,
-                enabled = gesturesEnabled,
-                reverseDirection = isRtl,
-                velocityThreshold = DrawerVelocityThreshold,
-                resistance = null
-            )
+            Modifier
+                .swipeableV2(
+                    state = drawerState.swipeableState,
+                    orientation = Orientation.Horizontal,
+                    enabled = gesturesEnabled,
+                    reverseDirection = isRtl
+                )
+                .swipeAnchors(
+                    drawerState.swipeableState,
+                    possibleValues = setOf(DrawerValue.Closed, DrawerValue.Open)
+                ) { value, _ ->
+                    when (value) {
+                        DrawerValue.Closed -> minValue
+                        DrawerValue.Open -> maxValue
+                    }
+                }
+                .provideScrollContainerInfo(containerInfo)
         ) {
             Box {
                 content()
@@ -416,13 +445,13 @@
                 onClose = {
                     if (
                         gesturesEnabled &&
-                        drawerState.swipeableState.confirmStateChange(DrawerValue.Closed)
+                        drawerState.swipeableState.confirmValueChange(DrawerValue.Closed)
                     ) {
                         scope.launch { drawerState.close() }
                     }
                 },
                 fraction = {
-                    calculateFraction(minValue, maxValue, drawerState.offset.value)
+                    calculateFraction(minValue, maxValue, drawerState.requireOffset())
                 },
                 color = scrimColor
             )
@@ -437,7 +466,13 @@
                             maxHeight = modalDrawerConstraints.maxHeight.toDp()
                         )
                 }
-                    .offset { IntOffset(drawerState.offset.value.roundToInt(), 0) }
+                    .offset {
+                        IntOffset(
+                            drawerState
+                                .requireOffset()
+                                .roundToInt(), 0
+                        )
+                    }
                     .padding(end = EndDrawerPadding)
                     .semantics {
                         paneTitle = navigationMenu
@@ -445,7 +480,7 @@
                             dismiss {
                                 if (
                                     drawerState.swipeableState
-                                        .confirmStateChange(DrawerValue.Closed)
+                                        .confirmValueChange(DrawerValue.Closed)
                                 ) {
                                     scope.launch { drawerState.close() }
                                 }; true
@@ -541,6 +576,15 @@
         } else {
             Modifier
         }
+
+        val containerInfo = remember(gesturesEnabled) {
+            object : ScrollContainerInfo {
+                override fun canScrollHorizontally() = gesturesEnabled
+
+                override fun canScrollVertically() = false
+            }
+        }
+
         val swipeable = Modifier
             .then(nestedScroll)
             .swipeable(
@@ -550,6 +594,7 @@
                 enabled = gesturesEnabled,
                 resistance = null
             )
+            .provideScrollContainerInfo(containerInfo)
 
         Box(swipeable) {
             content()
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
index 553aafd..d0f78a0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ExperimentalMaterialApi.kt
@@ -20,4 +20,5 @@
     "This material API is experimental and is likely to change or to be removed in" +
         " the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalMaterialApi
\ No newline at end of file
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index 3ba169b..919da41 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -44,8 +44,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.isSpecified
+import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.semantics.collapse
 import androidx.compose.ui.semantics.contentDescription
@@ -338,6 +340,15 @@
                 visible = sheetState.targetValue != Hidden
             )
         }
+
+        val containerInfo = remember(sheetState) {
+            object : ScrollContainerInfo {
+                override fun canScrollHorizontally() = false
+
+                override fun canScrollVertically() = sheetState.currentValue != Hidden
+            }
+        }
+
         Surface(
             Modifier
                 .fillMaxWidth()
@@ -353,6 +364,7 @@
                     IntOffset(0, y)
                 }
                 .bottomSheetSwipeable(sheetState, fullHeight, sheetHeightState)
+                .provideScrollContainerInfo(containerInfo)
                 .onGloballyPositioned {
                     sheetHeightState.value = it.size.height.toFloat()
                 }
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 70c2766..9ee4ffb 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
@@ -33,8 +33,15 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
+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.layout.OnRemeasuredModifier
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
@@ -98,22 +105,32 @@
     possibleValues: Set<T>,
     anchorsChanged: ((oldAnchors: Map<T, Float>, newAnchors: Map<T, Float>) -> Unit)? = null,
     calculateAnchor: (value: T, layoutSize: IntSize) -> Float?,
-) = onSizeChanged { layoutSize ->
-    val previousAnchors = state.anchors
-    val newAnchors = mutableMapOf<T, Float>()
-    possibleValues.forEach {
-        val anchorValue = calculateAnchor(it, layoutSize)
-        if (anchorValue != null) {
-            newAnchors[it] = anchorValue
+) = this.then(SwipeAnchorsModifier(
+    onDensityChanged = { state.density = it },
+    onSizeChanged = { layoutSize ->
+        val previousAnchors = state.anchors
+        val newAnchors = mutableMapOf<T, Float>()
+        possibleValues.forEach {
+            val anchorValue = calculateAnchor(it, layoutSize)
+            if (anchorValue != null) {
+                newAnchors[it] = anchorValue
+            }
         }
+        if (previousAnchors != newAnchors) {
+            state.updateAnchors(newAnchors)
+            if (previousAnchors.isNotEmpty()) {
+                anchorsChanged?.invoke(previousAnchors, newAnchors)
+            }
+        }
+    },
+    inspectorInfo = debugInspectorInfo {
+        name = "swipeAnchors"
+        properties["state"] = state
+        properties["possibleValues"] = possibleValues
+        properties["anchorsChanged"] = anchorsChanged
+        properties["calculateAnchor"] = calculateAnchor
     }
-    if (previousAnchors == newAnchors) return@onSizeChanged
-    state.updateAnchors(newAnchors)
-
-    if (previousAnchors.isNotEmpty()) {
-        anchorsChanged?.invoke(previousAnchors, newAnchors)
-    }
-}
+))
 
 /**
  * State of the [swipeableV2] modifier.
@@ -123,7 +140,6 @@
  * [SwipeableV2State] use [rememberSwipeableV2State].
  *
  * @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 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
@@ -139,7 +155,6 @@
 @ExperimentalMaterialApi
 internal class SwipeableV2State<T>(
     initialValue: T,
-    internal val density: Density,
     internal val animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
     internal val confirmValueChange: (newValue: T) -> Boolean = { true },
     internal val positionalThreshold: Density.(totalDistance: Float) -> Float =
@@ -178,6 +193,7 @@
      *
      * To guarantee stricter semantics, consider using [requireOffset].
      */
+    @get:Suppress("AutoBoxing")
     val offset: Float? by derivedStateOf {
         dragPosition?.coerceIn(minBound, maxBound)
     }
@@ -228,14 +244,14 @@
     private val minBound by derivedStateOf { anchors.minOrNull() ?: Float.NEGATIVE_INFINITY }
     private val maxBound by derivedStateOf { anchors.maxOrNull() ?: Float.POSITIVE_INFINITY }
 
-    private val velocityThresholdPx = with(density) { velocityThreshold.toPx() }
-
     internal val draggableState = DraggableState {
         dragPosition = (dragPosition ?: 0f) + it
     }
 
     internal var anchors by mutableStateOf(emptyMap<T, Float>())
 
+    internal var density: Density? = null
+
     internal fun updateAnchors(newAnchors: Map<T, Float>) {
         val previousAnchorsEmpty = anchors.isEmpty()
         anchors = newAnchors
@@ -348,6 +364,8 @@
     ): T {
         val currentAnchors = anchors
         val currentAnchor = currentAnchors.requireAnchor(currentValue)
+        val currentDensity = requireDensity()
+        val velocityThresholdPx = with(currentDensity) { velocityThreshold.toPx() }
         return if (currentAnchor <= offset) {
             // Swiping from lower to upper (positive).
             if (velocity >= velocityThresholdPx) {
@@ -355,7 +373,7 @@
             } else {
                 val upper = currentAnchors.closestAnchor(offset, true)
                 val distance = abs(currentAnchors.getValue(upper) - currentAnchor)
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor + relativeThreshold)
                 if (offset < absoluteThreshold) currentValue else upper
             }
@@ -366,7 +384,7 @@
             } else {
                 val lower = currentAnchors.closestAnchor(offset, false)
                 val distance = abs(currentAnchor - currentAnchors.getValue(lower))
-                val relativeThreshold = abs(positionalThreshold(density, distance))
+                val relativeThreshold = abs(positionalThreshold(currentDensity, distance))
                 val absoluteThreshold = abs(currentAnchor - relativeThreshold)
                 if (offset < 0) {
                     // For negative offsets, larger absolute thresholds are closer to lower anchors
@@ -379,6 +397,11 @@
         }
     }
 
+    private fun requireDensity() = requireNotNull(density) {
+        "SwipeableState did not have a density attached. Are you using Modifier.swipeable with " +
+            "this=$this SwipeableState?"
+    }
+
     companion object {
         /**
          * The default [Saver] implementation for [SwipeableV2State].
@@ -388,8 +411,7 @@
             animationSpec: AnimationSpec<Float>,
             confirmValueChange: (T) -> Boolean,
             positionalThreshold: Density.(distance: Float) -> Float,
-            velocityThreshold: Dp,
-            density: Density
+            velocityThreshold: Dp
         ) = Saver<SwipeableV2State<T>, T>(
             save = { it.currentValue },
             restore = {
@@ -398,8 +420,7 @@
                     animationSpec = animationSpec,
                     confirmValueChange = confirmValueChange,
                     positionalThreshold = positionalThreshold,
-                    velocityThreshold = velocityThreshold,
-                    density = density
+                    velocityThreshold = velocityThreshold
                 )
             }
         )
@@ -420,15 +441,13 @@
     animationSpec: AnimationSpec<Float> = SwipeableV2Defaults.AnimationSpec,
     confirmValueChange: (newValue: T) -> Boolean = { true }
 ): SwipeableV2State<T> {
-    val density = LocalDensity.current
     return rememberSaveable(
-        initialValue, animationSpec, confirmValueChange, density,
+        initialValue, animationSpec, confirmValueChange,
         saver = SwipeableV2State.Saver(
             animationSpec = animationSpec,
             confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         ),
     ) {
         SwipeableV2State(
@@ -436,8 +455,7 @@
             animationSpec = animationSpec,
             confirmValueChange = confirmValueChange,
             positionalThreshold = SwipeableV2Defaults.PositionalThreshold,
-            velocityThreshold = SwipeableV2Defaults.VelocityThreshold,
-            density = density
+            velocityThreshold = SwipeableV2Defaults.VelocityThreshold
         )
     }
 }
@@ -491,6 +509,37 @@
         fixedPositionalThreshold(56.dp)
 }
 
+@Stable
+private class SwipeAnchorsModifier(
+    private val onDensityChanged: (density: Density) -> Unit,
+    private val onSizeChanged: (layoutSize: IntSize) -> Unit,
+    inspectorInfo: InspectorInfo.() -> Unit,
+) : LayoutModifier, OnRemeasuredModifier, InspectorValueInfo(inspectorInfo) {
+
+    private var lastDensity: Float = -1f
+    private var lastFontScale: Float = -1f
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        if (density != lastDensity || fontScale != lastFontScale) {
+            onDensityChanged(Density(density, fontScale))
+            lastDensity = density
+            lastFontScale = fontScale
+        }
+        val placeable = measurable.measure(constraints)
+        return layout(placeable.width, placeable.height) { placeable.place(0, 0) }
+    }
+
+    override fun onRemeasured(size: IntSize) {
+        onSizeChanged(size)
+    }
+
+    override fun toString() = "SwipeAnchorsModifierImpl(updateDensity=$onDensityChanged, " +
+        "onSizeChanged=$onSizeChanged)"
+}
+
 private fun <T> Map<T, Float>.closestAnchor(
     offset: Float = 0f,
     searchUpwards: Boolean = false
diff --git a/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt b/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
index e1590d8..81a9075 100644
--- a/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3-window-size-class/api/public_plus_experimental_current.txt
@@ -5,7 +5,7 @@
     method @androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi @androidx.compose.runtime.Composable public static androidx.compose.material3.windowsizeclass.WindowSizeClass calculateWindowSizeClass(android.app.Activity activity);
   }
 
-  @kotlin.RequiresOptIn(message="This material3-window-size-class API is experimental and is likely to change or to " + "be removed in the future.") public @interface ExperimentalMaterial3WindowSizeClassApi {
+  @kotlin.RequiresOptIn(message="This material3-window-size-class API is experimental and is likely to change or to " + "be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3WindowSizeClassApi {
   }
 
   public final class TestOnly_jvmKt {
diff --git a/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt b/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
index 0c19899..8f405e8 100644
--- a/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
+++ b/compose/material3/material3-window-size-class/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt
@@ -20,4 +20,5 @@
     "This material3-window-size-class API is experimental and is likely to change or to " +
         "be removed in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalMaterial3WindowSizeClassApi
\ No newline at end of file
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index c815b0b..d92a1b2 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -350,7 +350,7 @@
   public final class ElevationKt {
   }
 
-  @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterial3Api {
+  @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMaterial3Api {
   }
 
   @androidx.compose.material3.ExperimentalMaterial3Api public interface ExposedDropdownMenuBoxScope {
diff --git a/compose/material3/material3/build.gradle b/compose/material3/material3/build.gradle
index c479be1..7a79167 100644
--- a/compose/material3/material3/build.gradle
+++ b/compose/material3/material3/build.gradle
@@ -36,21 +36,21 @@
          * corresponding block below
          */
         implementation(libs.kotlinStdlibCommon)
-        implementation("androidx.compose.animation:animation-core:1.3.0-rc01")
-        implementation("androidx.compose.foundation:foundation-layout:1.3.0-rc01")
-        implementation("androidx.compose.ui:ui-util:1.3.0-rc01")
+        implementation("androidx.compose.animation:animation-core:1.3.1")
+        implementation("androidx.compose.foundation:foundation-layout:1.3.1")
+        implementation("androidx.compose.ui:ui-util:1.3.1")
         api(project(":compose:foundation:foundation"))
-        api("androidx.compose.material:material-icons-core:1.3.0-rc01")
-        api("androidx.compose.material:material-ripple:1.3.0-rc01")
-        api("androidx.compose.runtime:runtime:1.3.0-rc01")
-        api("androidx.compose.ui:ui-graphics:1.3.0-rc01")
-        api("androidx.compose.ui:ui:1.3.0-rc01")
-        api("androidx.compose.ui:ui-text:1.3.0-rc01")
+        api("androidx.compose.material:material-icons-core:1.3.1")
+        api("androidx.compose.material:material-ripple:1.3.1")
+        api("androidx.compose.runtime:runtime:1.3.1")
+        api("androidx.compose.ui:ui-graphics:1.3.1")
+        api("androidx.compose.ui:ui:1.3.1")
+        api("androidx.compose.ui:ui-text:1.3.1")
 
         // TODO: remove next 3 dependencies when b/202810604 is fixed
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
@@ -107,8 +107,8 @@
 
                 // TODO: remove next 3 dependencies when b/202810604 is fixed
                 implementation("androidx.savedstate:savedstate:1.1.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             desktopMain.dependencies {
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
index 2edb2e5..6deaf4d 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/DismissibleNavigationDrawerTest.kt
@@ -26,6 +26,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -558,6 +560,44 @@
                 .onParent()
                 .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
         }
+
+    @Test
+    fun dismissibleNavigationDrawer_providesScrollableContainerInfo_enabled() {
+        var actualValue = { false }
+        rule.setMaterialContent(lightColorScheme()) {
+
+            DismissibleNavigationDrawer(
+                gesturesEnabled = true,
+                drawerContent = {},
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isTrue()
+    }
+
+    @Test
+    fun dismissibleNavigationDrawer_providesScrollableContainerInfo_disabled() {
+        var actualValue = { false }
+        rule.setMaterialContent(lightColorScheme()) {
+
+            DismissibleNavigationDrawer(
+                gesturesEnabled = false,
+                drawerContent = {},
+                content = {
+                    Box(Modifier.consumeScrollContainerInfo {
+                        actualValue = { it!!.canScroll() }
+                    })
+                }
+            )
+        }
+
+        assertThat(actualValue()).isFalse()
+    }
 }
 
 private val DrawerTestTag = "drawer"
\ No newline at end of file
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
index 409120f..f18376cc 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ModalNavigationDrawerTest.kt
@@ -26,6 +26,8 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.semantics.SemanticsActions
@@ -656,6 +658,45 @@
         topNode = rule.onNodeWithTag(topTag).fetchSemanticsNode()
         assertEquals(2, topNode.children.size)
     }
+
+    @Test
+    fun navigationDrawer_providesScrollableContainerInfo_enabled() {
+        var actualValue = { false }
+        rule.setMaterialContent(lightColorScheme()) {
+            ModalNavigationDrawer(
+                drawerContent = { ModalDrawerSheet { } },
+                content = {
+                    Box(
+                        Modifier.consumeScrollContainerInfo {
+                            actualValue = { it!!.canScroll() }
+                        }
+                    )
+                }
+            )
+        }
+
+        assertThat(actualValue()).isTrue()
+    }
+
+    @Test
+    fun navigationDrawer_providesScrollableContainerInfo_disabled() {
+        var actualValue = { false }
+        rule.setMaterialContent(lightColorScheme()) {
+            ModalNavigationDrawer(
+                gesturesEnabled = false,
+                drawerContent = { ModalDrawerSheet { } },
+                content = {
+                    Box(
+                        Modifier.consumeScrollContainerInfo {
+                            actualValue = { it!!.canScroll() }
+                        }
+                    )
+                }
+            )
+        }
+
+        assertThat(actualValue()).isFalse()
+    }
 }
 
 private val DrawerTestTag = "drawer"
\ No newline at end of file
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
index 19f4e59..60263cf 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ExperimentalMaterial3Api.kt
@@ -20,4 +20,5 @@
     "This material API is experimental and is likely to change or to be removed in" +
         " the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalMaterial3Api
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index ae8de70..38b30e4 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -57,7 +57,9 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.ScrollContainerInfo
 import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.provideScrollContainerInfo
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
@@ -268,6 +270,15 @@
 
     val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+
+    val containerInfo = remember(gesturesEnabled) {
+        object : ScrollContainerInfo {
+            override fun canScrollHorizontally() = gesturesEnabled
+
+            override fun canScrollVertically() = false
+        }
+    }
+
     Box(
         modifier
             .fillMaxSize()
@@ -281,6 +292,7 @@
                 velocityThreshold = DrawerVelocityThreshold,
                 resistance = null
             )
+            .provideScrollContainerInfo(containerInfo)
     ) {
         Box {
             content()
@@ -361,6 +373,14 @@
 
     val anchors = mapOf(minValue to DrawerValue.Closed, maxValue to DrawerValue.Open)
     val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
+    val containerInfo = remember(gesturesEnabled) {
+        object : ScrollContainerInfo {
+            override fun canScrollHorizontally() = gesturesEnabled
+
+            override fun canScrollVertically() = false
+        }
+    }
+
     Box(
         modifier.swipeable(
             state = drawerState.swipeableState,
@@ -371,7 +391,7 @@
             reverseDirection = isRtl,
             velocityThreshold = DrawerVelocityThreshold,
             resistance = null
-        )
+        ).provideScrollContainerInfo(containerInfo)
     ) {
         Layout(content = {
             Box(Modifier.semantics {
diff --git a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
index cd738fe..1ed4cd9 100644
--- a/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
+++ b/compose/runtime/runtime-lint/src/main/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetector.kt
@@ -107,7 +107,7 @@
             if (name != "content" && parameterInfo.functionType.parameters.isEmpty()) {
                 context.report(
                     ComposableLambdaParameterNaming,
-                    node,
+                    uElement,
                     context.getNameLocation(uElement),
                     "Composable lambda parameter should be named `content`",
                     LintFix.create()
@@ -123,7 +123,7 @@
             if (parameter !== node.uastParameters.last()) {
                 context.report(
                     ComposableLambdaParameterPosition,
-                    node,
+                    uElement,
                     context.getNameLocation(uElement),
                     "Composable lambda parameter should be the last parameter so it can be used " +
                         "as a trailing lambda"
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 4e98ddb..56de5b4 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -769,9 +769,11 @@
   public final class ComposableMethod {
     method public java.lang.reflect.Method asMethod();
     method public int getParameterCount();
+    method public Class<?>![] getParameterTypes();
     method public java.lang.reflect.Parameter![] getParameters();
     method public operator Object? invoke(androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
     property public final int parameterCount;
+    property public final Class<?>![] parameterTypes;
     property public final java.lang.reflect.Parameter![] parameters;
   }
 
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 3688ea9..82341f0 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -289,7 +289,7 @@
   public final class ExpectKt {
   }
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose and is likely to change before becoming " + "stable.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalComposeApi {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is an experimental API for Compose and is likely to change before becoming " + "stable.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalComposeApi {
   }
 
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExplicitGroupsComposable {
@@ -298,10 +298,10 @@
   @androidx.compose.runtime.StableMarker @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.CLASS) public @interface Immutable {
   }
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API for Compose modules that may change frequently " + "and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalComposeApi {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API for Compose modules that may change frequently " + "and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalComposeApi {
   }
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") public @interface InternalComposeTracingApi {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalComposeTracingApi {
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface MonotonicFrameClock extends kotlin.coroutines.CoroutineContext.Element {
@@ -834,9 +834,11 @@
   public final class ComposableMethod {
     method public java.lang.reflect.Method asMethod();
     method public int getParameterCount();
+    method public Class<?>![] getParameterTypes();
     method public java.lang.reflect.Parameter![] getParameters();
     method public operator Object? invoke(androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
     property public final int parameterCount;
+    property public final Class<?>![] parameterTypes;
     property public final java.lang.reflect.Parameter![] parameters;
   }
 
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 5b1424f..900b19e 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -798,9 +798,11 @@
   public final class ComposableMethod {
     method public java.lang.reflect.Method asMethod();
     method public int getParameterCount();
+    method public Class<?>![] getParameterTypes();
     method public java.lang.reflect.Parameter![] getParameters();
     method public operator Object? invoke(androidx.compose.runtime.Composer composer, Object? instance, java.lang.Object?... args);
     property public final int parameterCount;
+    property public final Class<?>![] parameterTypes;
     property public final java.lang.reflect.Parameter![] parameters;
   }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index f91c9a1..3dbaa26 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 9001
+    const val version: Int = 9100
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 03e25a0..65ee2ec 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -3314,7 +3314,7 @@
                         invokeComposable(this, content)
                         endGroup()
                     } else if (
-                        forciblyRecompose &&
+                        (forciblyRecompose || providersInvalid) &&
                         savedContent != null &&
                         savedContent != Composer.Empty
                     ) {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
index 21db3b7..1bfbd64 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ExperimentalComposeApi.kt
@@ -27,4 +27,5 @@
     AnnotationTarget.PROPERTY,
     AnnotationTarget.PROPERTY_GETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalComposeApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
index 809a56d..c964187 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeApi.kt
@@ -26,4 +26,5 @@
     AnnotationTarget.FUNCTION,
     AnnotationTarget.PROPERTY
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalComposeApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
index 943df3a..b2e5633 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/InternalComposeTracingApi.kt
@@ -20,4 +20,5 @@
     level = RequiresOptIn.Level.ERROR,
     message = "This is internal API that may change frequently and without warning."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalComposeTracingApi
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index 5c32e29..285838a 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -54,7 +54,6 @@
 import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.test.runTest
-import org.junit.Ignore
 
 @Composable
 fun Container(content: @Composable () -> Unit) = content()
@@ -3235,7 +3234,6 @@
     }
 
     @Test
-    @Ignore("b/255722247")
     fun testNonLocalReturn_CM1_RetFunc_FalseTrue() = compositionTest {
         var condition by mutableStateOf(false)
 
@@ -3255,7 +3253,6 @@
     }
 
     @Test
-    @Ignore("b/255722247")
     fun testNonLocalReturn_CM1_RetFunc_TrueFalse() = compositionTest {
         var condition by mutableStateOf(true)
 
@@ -3275,7 +3272,6 @@
     }
 
     @Test
-    @Ignore("b/255722247")
     fun test_CM1_CCM1_RetFun_FalseTrue() = compositionTest {
         var condition by mutableStateOf(false)
 
@@ -3295,7 +3291,6 @@
     }
 
     @Test
-    @Ignore("b/255722247")
     fun test_CM1_CCM1_RetFun_TrueFalse() = compositionTest {
         var condition by mutableStateOf(true)
 
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.kt
index d0b63cc..b8f53ff 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/reflect/ComposableMethod.kt
@@ -119,6 +119,12 @@
         get() = method.parameters.copyOfRange(0, composableInfo.realParamsCount)
 
     /**
+     * Returns method parameters types excluding the utility Compose-specific parameters.
+     */
+    val parameterTypes: Array<Class<*>>
+        get() = method.parameterTypes.copyOfRange(0, composableInfo.realParamsCount)
+
+    /**
      * Calls the Composable method on the given [instance]. If the method accepts default values,
      * this function will call it with the correct options set.
      */
diff --git a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
index 3c92434..08334e85 100644
--- a/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
+++ b/compose/runtime/runtime/src/jvmTest/kotlin/androidx/compose/runtime/reflect/ComposableMethodTest.kt
@@ -473,6 +473,100 @@
             diffParametersMethod.parameters.map { it.type })
     }
 
+    @Throws(NoSuchMethodException::class)
+    @Test
+    fun test_realParameterTypes_returns_correct_parameterTypes() {
+        val function0 = clazz.getDeclaredComposableMethod("overloadedComposable")
+        val function1 =
+            clazz.getDeclaredComposableMethod("overloadedComposable", String::class.java)
+        val function10 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(10) { String::class.java }
+            )
+        val function11 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(11) { String::class.java }
+            )
+        val function12 =
+            clazz.getDeclaredComposableMethod(
+                "overloadedComposable",
+                *Array(12) { String::class.java }
+            )
+
+        val method0 = wrapperClazz.getDeclaredComposableMethod("overloadedComposableMethod")
+        val method1 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                String::class.java
+            )
+        val method10 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(10) { String::class.java }
+            )
+        val method11 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(11) { String::class.java }
+            )
+        val method12 =
+            wrapperClazz.getDeclaredComposableMethod(
+                "overloadedComposableMethod",
+                *Array(12) { String::class.java }
+            )
+
+        val diffParameters =
+            clazz.getDeclaredComposableMethod(
+                "differentParametersTypes",
+                String::class.java,
+                Any::class.java,
+                Int::class.java,
+                Float::class.java,
+                Double::class.java,
+                Long::class.java
+            )
+
+        val diffParametersMethod =
+            wrapperClazz.getDeclaredComposableMethod(
+                "differentParametersTypesMethod",
+                String::class.java,
+                Any::class.java,
+                Int::class.java,
+                Float::class.java,
+                Double::class.java,
+                Long::class.java
+            )
+
+        assertEquals(0, function0.parameterTypes.size)
+        assertEquals(1, function1.parameterTypes.size)
+        assertEquals(10, function10.parameterTypes.size)
+        assertEquals(11, function11.parameterTypes.size)
+        assertEquals(12, function12.parameterTypes.size)
+        assertEquals(12, function12.parameterTypes.size)
+
+        assertEquals(0, method0.parameterTypes.size)
+        assertEquals(1, method1.parameterTypes.size)
+        assertEquals(10, method10.parameterTypes.size)
+        assertEquals(11, method11.parameterTypes.size)
+        assertEquals(12, method12.parameterTypes.size)
+
+        assertEquals(0, composableMethod.asComposableMethod()!!.parameterTypes.size)
+
+        assertEquals(6, diffParameters.parameterTypes.size)
+        assertEquals(
+            listOf(String::class.java, Any::class.java, Int::class.java, Float::class.java,
+                Double::class.java, Long::class.java),
+            diffParameters.parameterTypes.toList())
+
+        assertEquals(6, diffParametersMethod.parameterTypes.size)
+        assertEquals(
+            listOf(String::class.java, Any::class.java, Int::class.java, Float::class.java,
+                Double::class.java, Long::class.java),
+            diffParametersMethod.parameterTypes.toList())
+    }
+
     private class TestFrameClock : MonotonicFrameClock {
         override suspend fun <R> withFrameNanos(onFrame: (Long) -> R): R = onFrame(0L)
     }
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index 47ef466..99ccb0c 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -43,7 +43,7 @@
         implementation(projectOrArtifact(":compose:runtime:runtime"))
         implementation(projectOrArtifact(":compose:ui:ui-unit"))
         implementation(projectOrArtifact(":compose:ui:ui-graphics"))
-        implementation(projectOrArtifact(":activity:activity-compose"))
+        implementation("androidx.activity:activity-compose:1.3.1")
         // old version of common-java8 conflicts with newer version, because both have
         // DefaultLifecycleEventObserver.
         // Outside of androidx this is resolved via constraint added to lifecycle-common,
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 08e2ebc..9b18442 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -399,7 +399,7 @@
   public final class DegreesKt {
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGraphicsApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGraphicsApi {
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class FilterQuality {
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
index aa38621..909c69d 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/ExperimentalGraphicsApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.ui.graphics
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalGraphicsApi
\ No newline at end of file
diff --git a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
index d786c2f..bcc5884 100644
--- a/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
+++ b/compose/ui/ui-lint/src/main/java/androidx/compose/ui/lint/ModifierParameterDetector.kt
@@ -75,7 +75,7 @@
             if (modifierParameter.name != ModifierParameterName) {
                 context.report(
                     ModifierParameter,
-                    node,
+                    modifierParameterElement,
                     context.getNameLocation(modifierParameterElement),
                     "$modifierName parameter should be named $ModifierParameterName",
                     LintFix.create()
@@ -91,7 +91,7 @@
             if (modifierParameter.type.canonicalText != Names.Ui.Modifier.javaFqn) {
                 context.report(
                     ModifierParameter,
-                    node,
+                    modifierParameterElement,
                     context.getNameLocation(modifierParameterElement),
                     "$modifierName parameter should have a type of $modifierName",
                     LintFix.create()
@@ -113,7 +113,7 @@
                 if (referenceExpression?.getReferencedName() != modifierName) {
                     context.report(
                         ModifierParameter,
-                        node,
+                        modifierParameterElement,
                         context.getNameLocation(modifierParameterElement),
                         "Optional $modifierName parameter should have a default value " +
                             "of `$modifierName`",
@@ -134,7 +134,7 @@
                 if (index != optionalParameterIndex) {
                     context.report(
                         ModifierParameter,
-                        node,
+                        modifierParameterElement,
                         context.getNameLocation(modifierParameterElement),
                         "$modifierName parameter should be the first optional parameter",
                         // Hard to make a lint fix for this and keep parameter formatting, so
diff --git a/compose/ui/ui-test-junit4/build.gradle b/compose/ui/ui-test-junit4/build.gradle
index 3113c29..d7e2dfb 100644
--- a/compose/ui/ui-test-junit4/build.gradle
+++ b/compose/ui/ui-test-junit4/build.gradle
@@ -46,12 +46,12 @@
         implementation("androidx.compose.runtime:runtime-saveable:1.2.1")
         implementation("androidx.activity:activity-compose:1.3.0")
         implementation("androidx.annotation:annotation:1.1.0")
-        implementation("androidx.lifecycle:lifecycle-common:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.test:core:1.4.0")
-        implementation(libs.testMonitor)
-        implementation("androidx.test.espresso:espresso-core:3.3.0")
-        implementation("androidx.test.espresso:espresso-idling-resource:3.3.0")
+        implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.test:core:1.5.0")
+        implementation("androidx.test:monitor:1.6.0")
+        implementation("androidx.test.espresso:espresso-core:3.5.0")
+        implementation("androidx.test.espresso:espresso-idling-resource:3.5.0")
         implementation(libs.kotlinCoroutinesCore)
         implementation(libs.kotlinCoroutinesTest)
 
@@ -104,8 +104,8 @@
                 implementation("androidx.annotation:annotation:1.1.0")
 
                 implementation(project(":compose:runtime:runtime-saveable"))
-                implementation("androidx.lifecycle:lifecycle-common:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-common:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
                 implementation("androidx.test:core:1.4.0")
                 implementation(libs.testMonitor)
                 implementation("androidx.test.espresso:espresso-core:3.3.0")
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index f3aa23b..08d0066 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -88,7 +88,7 @@
   public final class Expect_jvmKt {
   }
 
-  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") public @interface ExperimentalTestApi {
+  @kotlin.RequiresOptIn(message="This testing API is experimental and is likely to be changed or removed entirely") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTestApi {
   }
 
   public final class FiltersKt {
@@ -245,7 +245,7 @@
     property public default int width;
   }
 
-  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") public @interface InternalTestApi {
+  @kotlin.RequiresOptIn(message="This is internal API for Compose modules that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalTestApi {
   }
 
   @androidx.compose.ui.test.ExperimentalTestApi @kotlin.jvm.JvmDefaultWithCompatibility public interface KeyInjectionScope extends androidx.compose.ui.test.InjectionScope {
diff --git a/compose/ui/ui-test/build.gradle b/compose/ui/ui-test/build.gradle
index 99ae536..0863884 100644
--- a/compose/ui/ui-test/build.gradle
+++ b/compose/ui/ui-test/build.gradle
@@ -56,8 +56,8 @@
         implementation(project(":compose:ui:ui-util"))
         implementation("androidx.annotation:annotation:1.1.0")
         implementation("androidx.core:core-ktx:1.1.0")
-        implementation("androidx.test.espresso:espresso-core:3.3.0")
-        implementation(libs.testMonitor)
+        implementation("androidx.test.espresso:espresso-core:3.5.0")
+        implementation("androidx.test:monitor:1.6.0")
 
         testImplementation(project(":compose:test-utils"))
         testImplementation(libs.truth)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
index 1b8cad3..561c112 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/ExperimentalTestApi.kt
@@ -17,9 +17,11 @@
 package androidx.compose.ui.test
 
 @RequiresOptIn("This testing API is experimental and is likely to be changed or removed entirely")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTestApi
 
 @RequiresOptIn(
     "This is internal API for Compose modules that may change frequently and without warning."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalTestApi
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index ed86b4a..226cedf 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -93,10 +93,10 @@
     method public static inline <R> R withStyle(androidx.compose.ui.text.AnnotatedString.Builder, androidx.compose.ui.text.ParagraphStyle style, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,? extends R> block);
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalTextApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTextApi {
   }
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalTextApi {
   }
 
   public final class JvmAnnotatedString_jvmKt {
@@ -630,7 +630,7 @@
 
 package androidx.compose.ui.text.android {
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalPlatformTextApi {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR, message="This is internal API that may change frequently and without warning.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY}) public @interface InternalPlatformTextApi {
   }
 
   public final class LayoutCompatKt {
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
index 0c52428..b67456b 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/ExperimentalTextApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.ui.text
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTextApi
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
index e0b91a4..710ac39 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/InternalTextApi.kt
@@ -25,4 +25,5 @@
     AnnotationTarget.FUNCTION,
     AnnotationTarget.PROPERTY
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalTextApi
\ No newline at end of file
diff --git a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
index 092a10a..ffae455 100644
--- a/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling-data/api/public_plus_experimental_current.txt
@@ -116,7 +116,7 @@
     property public final String? sourceFile;
   }
 
-  @kotlin.RequiresOptIn(message="This API is for tooling only and is likely to change in the future.") public @interface UiToolingDataApi {
+  @kotlin.RequiresOptIn(message="This API is for tooling only and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface UiToolingDataApi {
   }
 
 }
diff --git a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
index 45578f6..15b71a6 100644
--- a/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
+++ b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.ui.tooling.data
 
 @RequiresOptIn("This API is for tooling only and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class UiToolingDataApi
\ 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 adea0bf..e99e3fb 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
@@ -19,7 +19,6 @@
 import android.app.Activity
 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
@@ -34,7 +33,6 @@
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -137,7 +135,7 @@
             assertTrue(clock.animatedVisibilityClocks.isEmpty())
         }
 
-        waitFor("Composable to have animations", 1, TimeUnit.SECONDS) {
+        waitFor(1, TimeUnit.SECONDS) {
             // Handle the case where onLayout was called too soon. Calling requestLayout will
             // make sure onLayout will be called again.
             composeViewAdapter.requestLayout()
@@ -152,22 +150,43 @@
 
     @Test
     fun transitionAnimatedVisibilityIsTrackedAsTransition() {
-        checkTransitionIsSubscribed("TransitionAnimatedVisibilityPreview", "transition.AV")
+        checkAnimationsAreSubscribed(
+            "TransitionAnimatedVisibilityPreview",
+            emptyList(),
+            listOf("transition.AV")
+        )
     }
 
     @Test
-    fun animatedContentIsSubscribed() {
-        checkAnimationsAreSubscribed("AnimatedContentPreview", listOf("AnimatedContent"))
+    fun animatedContentIsNotSubscribed() {
+        checkAnimationsAreSubscribed("AnimatedContentPreview")
+    }
+
+    @Test
+    fun animatedContentAndTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "AnimatedContentAndTransitionPreview",
+            listOf("AnimatedContent"),
+            listOf("checkBoxAnim")
+        )
     }
 
     @Test
     fun transitionAnimationsAreSubscribedToTheClock() {
-        checkTransitionIsSubscribed("TransitionPreview", "checkBoxAnim")
+        checkAnimationsAreSubscribed(
+            "TransitionPreview",
+            emptyList(),
+            listOf("checkBoxAnim")
+        )
     }
 
     @Test
     fun transitionAnimationsWithSubcomposition() {
-        checkTransitionIsSubscribed("TransitionWithScaffoldPreview", "checkBoxAnim")
+        checkAnimationsAreSubscribed(
+            "TransitionWithScaffoldPreview",
+            emptyList(),
+            listOf("checkBoxAnim")
+        )
     }
 
     @Test
@@ -197,28 +216,68 @@
     }
 
     @Test
-    fun animateContentSizeIsSubscribed() {
-        checkAnimationsAreSubscribed("AnimateContentSizePreview", listOf("animateContentSize"))
+    fun animateContentSizeIsNotSubscribed() {
+        checkAnimationsAreSubscribed("AnimateContentSizePreview")
+    }
+
+    @Test
+    fun animateContentSizeAndTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "AnimateContentSizeAndTransitionPreview",
+            listOf("animateContentSize"),
+            listOf("checkBoxAnim")
+        )
     }
 
     @Test
     fun crossFadeIsSubscribed() {
-        checkTransitionIsSubscribed("CrossFadePreview", "Crossfade")
+        checkAnimationsAreSubscribed(
+            "CrossFadePreview",
+            emptyList(),
+            listOf("Crossfade")
+        )
     }
 
     @Test
-    fun targetBasedAnimationIsSubscribed() {
-        checkAnimationsAreSubscribed("TargetBasedAnimationPreview", listOf("TargetBasedAnimation"))
+    fun targetBasedAnimationIsNotSubscribed() {
+        checkAnimationsAreSubscribed("TargetBasedAnimationPreview")
     }
 
     @Test
-    fun decayAnimationIsSubscribed() {
-        checkAnimationsAreSubscribed("DecayAnimationPreview", listOf("DecayAnimation"))
+    fun decayAnimationIsNotSubscribed() {
+        checkAnimationsAreSubscribed("DecayAnimationPreview")
     }
 
     @Test
-    fun infiniteTransitionIsSubscribed() {
-        checkAnimationsAreSubscribed("InfiniteTransitionPreview", listOf("InfiniteTransition"))
+    fun infiniteTransitionIsNotSubscribed() {
+        checkAnimationsAreSubscribed("InfiniteTransitionPreview")
+    }
+
+    @Test
+    fun targetBasedAndTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "TargetBasedAndTransitionPreview",
+            listOf("TargetBasedAnimation"),
+            listOf("checkBoxAnim")
+        )
+    }
+
+    @Test
+    fun decayAndTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "DecayAndTransitionPreview",
+            listOf("DecayAnimation"),
+            listOf("checkBoxAnim")
+        )
+    }
+
+    @Test
+    fun infiniteAndTransitionIsSubscribed() {
+        checkAnimationsAreSubscribed(
+            "InfiniteAndTransitionPreview",
+            listOf("InfiniteTransition"),
+            listOf("checkBoxAnim")
+        )
     }
 
     @Test
@@ -262,7 +321,7 @@
             assertTrue(clock.animatedVisibilityClocks.isEmpty())
         }
 
-        waitFor("Composable to have animations", 5, TimeUnit.SECONDS) {
+        waitFor(5, TimeUnit.SECONDS) {
             // Handle the case where onLayout was called too soon. Calling requestLayout will
             // make sure onLayout will be called again.
             composeViewAdapter.requestLayout()
@@ -278,33 +337,6 @@
         }
     }
 
-    @OptIn(InternalAnimationApi::class)
-    private fun checkTransitionIsSubscribed(composableName: String, label: String) {
-        val clock = PreviewAnimationClock()
-
-        activityTestRule.runOnUiThread {
-            composeViewAdapter.init(
-                "androidx.compose.ui.tooling.TestAnimationPreviewKt",
-                composableName
-            )
-            composeViewAdapter.clock = clock
-            assertFalse(composeViewAdapter.hasAnimations())
-            assertTrue(clock.transitionClocks.isEmpty())
-        }
-
-        waitFor("Composable to have animations", 1, TimeUnit.SECONDS) {
-            // Handle the case where onLayout was called too soon. Calling requestLayout will
-            // make sure onLayout will be called again.
-            composeViewAdapter.requestLayout()
-            composeViewAdapter.hasAnimations()
-        }
-
-        activityTestRule.runOnUiThread {
-            val animation = clock.transitionClocks.values.single().animation
-            assertEquals(label, animation.label)
-        }
-    }
-
     @Test
     fun lineNumberMapping() {
         val viewInfos = assertRendersCorrectly(
@@ -535,7 +567,6 @@
      * timing out. The condition is evaluated on the UI thread.
      */
     private fun waitFor(
-        conditionLabel: String,
         timeout: Long,
         timeUnit: TimeUnit,
         conditionExpression: () -> Boolean
@@ -548,7 +579,8 @@
                 conditionSatisfied.set(conditionExpression())
             }
             if ((System.nanoTime() - now) > timeoutNanos) {
-                fail("Timed out while waiting for condition <$conditionLabel> to be satisfied.")
+                // Some previews are expected not to have animations.
+                return
             }
             Thread.sleep(200)
         }
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 71c9d1d..b52329d 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
@@ -130,6 +130,13 @@
     }
 }
 
+@Preview
+@Composable
+fun AnimatedContentAndTransitionPreview() {
+    AnimatedContentPreview()
+    TransitionPreview()
+}
+
 @OptIn(ExperimentalAnimationApi::class)
 @Preview
 @Composable
@@ -317,6 +324,13 @@
 
 @Preview
 @Composable
+fun AnimateContentSizeAndTransitionPreview() {
+    AnimateContentSizePreview()
+    TransitionPreview()
+}
+
+@Preview
+@Composable
 fun TargetBasedAnimationPreview() {
     val anim = remember {
         TargetBasedAnimation(
@@ -340,6 +354,13 @@
 
 @Preview
 @Composable
+fun TargetBasedAndTransitionPreview() {
+    TargetBasedAnimationPreview()
+    TransitionPreview()
+}
+
+@Preview
+@Composable
 fun DecayAnimationPreview() {
     val anim = remember {
         DecayAnimation(
@@ -361,6 +382,13 @@
 
 @Preview
 @Composable
+fun DecayAndTransitionPreview() {
+    DecayAnimationPreview()
+    TransitionPreview()
+}
+
+@Preview
+@Composable
 fun InfiniteTransitionPreview() {
     val infiniteTransition = rememberInfiniteTransition()
     Row {
@@ -370,6 +398,13 @@
     }
 }
 
+@Preview
+@Composable
+fun InfiniteAndTransitionPreview() {
+    InfiniteTransitionPreview()
+    TransitionPreview()
+}
+
 @Composable
 fun InfiniteTransition.PulsingDot(startOffset: StartOffset) {
     val scale by animateFloat(
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 4f473ff..c477089 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
@@ -314,11 +314,17 @@
         val animatedVisibilitySearch = AnimationSearch.AnimatedVisibilitySearch {
             clock.trackAnimatedVisibility(it, ::requestLayout)
         }
+
+        fun animateXAsStateSearch() =
+            if (AnimateXAsStateComposeAnimation.apiAvailable)
+                setOf(AnimationSearch.AnimateXAsStateSearch { clock.trackAnimateXAsState(it) })
+            else emptyList()
+
         // All supported animations.
         fun supportedSearch() = setOf(
             transitionSearch,
             animatedVisibilitySearch,
-        )
+        ) + animateXAsStateSearch()
 
         fun unsupportedSearch() = if (UnsupportedComposeAnimation.apiAvailable) setOf(
             animatedContentSearch,
@@ -328,16 +334,11 @@
             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()
+        // All supported animations
+        val supportedSearch = supportedSearch()
 
         // Animations to track in PreviewAnimationClock.
-        val setToTrack = supportedSearch() + extraSearch
+        val setToTrack = supportedSearch + unsupportedSearch()
 
         // Animations to search. animatedContentSearch is included even if it's not going to be
         // tracked as it should be excluded from transitionSearch.
@@ -360,10 +361,12 @@
             transitionSearch.animations.removeAll(animatedContentSearch.animations)
         }
 
-        hasAnimations = setToTrack.any { it.hasAnimations() }
+        // If non of supported animations are detected, unsupported animations should not be
+        // available either.
+        hasAnimations = supportedSearch.any { it.hasAnimations() }
 
         // Make the `PreviewAnimationClock` track all the transitions found.
-        if (::clock.isInitialized) {
+        if (::clock.isInitialized && hasAnimations) {
             setToTrack.forEach { it.track() }
         }
     }
diff --git a/compose/ui/ui-unit/api/public_plus_experimental_current.txt b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
index 35ad3e1..a529f45 100644
--- a/compose/ui/ui-unit/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-unit/api/public_plus_experimental_current.txt
@@ -194,7 +194,7 @@
     property public final long Zero;
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalUnitApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalUnitApi {
   }
 
   @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class IntOffset {
diff --git a/compose/ui/ui-unit/samples/build.gradle b/compose/ui/ui-unit/samples/build.gradle
index 4055ea0..33161d2 100644
--- a/compose/ui/ui-unit/samples/build.gradle
+++ b/compose/ui/ui-unit/samples/build.gradle
@@ -32,8 +32,8 @@
     implementation("androidx.compose.runtime:runtime:1.2.1")
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-unit"))
-    implementation("androidx.compose.foundation:foundation:1.3.0-rc01")
-    implementation("androidx.compose.foundation:foundation-layout:1.3.0-rc01")
+    implementation("androidx.compose.foundation:foundation:1.3.1")
+    implementation("androidx.compose.foundation:foundation-layout:1.3.1")
 }
 
 androidx {
diff --git a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
index 84c3c67..3db6a8c 100644
--- a/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
+++ b/compose/ui/ui-unit/src/commonMain/kotlin/androidx/compose/ui/unit/ExperimentalUnitApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.ui.unit
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalUnitApi
\ No newline at end of file
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 732988f..3e2b1e6 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -828,6 +828,17 @@
     property public abstract int inputMode;
   }
 
+  public interface ScrollContainerInfo {
+    method public boolean canScrollHorizontally();
+    method public boolean canScrollVertically();
+  }
+
+  public final class ScrollContainerInfoKt {
+    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+  }
+
 }
 
 package androidx.compose.ui.input.key {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 1d491d6..3c86dfd 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -123,10 +123,10 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer, androidx.compose.ui.Modifier modifier);
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalComposeUiApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalComposeUiApi {
   }
 
-  @kotlin.RequiresOptIn(message="Unstable API for use only between compose-ui modules sharing the same exact version, " + "subject to change without notice in major, minor, or patch releases.") public @interface InternalComposeUiApi {
+  @kotlin.RequiresOptIn(message="Unstable API for use only between compose-ui modules sharing the same exact version, " + "subject to change without notice in major, minor, or patch releases.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface InternalComposeUiApi {
   }
 
   @androidx.compose.runtime.Stable @kotlin.jvm.JvmDefaultWithCompatibility public interface Modifier {
@@ -954,6 +954,17 @@
     property public abstract int inputMode;
   }
 
+  public interface ScrollContainerInfo {
+    method public boolean canScrollHorizontally();
+    method public boolean canScrollVertically();
+  }
+
+  public final class ScrollContainerInfoKt {
+    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+  }
+
 }
 
 package androidx.compose.ui.input.key {
@@ -2474,7 +2485,7 @@
     property public abstract long targetSize;
   }
 
-  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalCoreApi {
+  @kotlin.RequiresOptIn(message="This API is internal to library.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER}) public @interface InternalCoreApi {
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public interface LayoutAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index db61359..87d6adf 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -828,6 +828,17 @@
     property public abstract int inputMode;
   }
 
+  public interface ScrollContainerInfo {
+    method public boolean canScrollHorizontally();
+    method public boolean canScrollVertically();
+  }
+
+  public final class ScrollContainerInfoKt {
+    method public static boolean canScroll(androidx.compose.ui.input.ScrollContainerInfo);
+    method public static androidx.compose.ui.Modifier consumeScrollContainerInfo(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.input.ScrollContainerInfo,kotlin.Unit> consumer);
+    method public static androidx.compose.ui.Modifier provideScrollContainerInfo(androidx.compose.ui.Modifier, androidx.compose.ui.input.ScrollContainerInfo scrollContainerInfo);
+  }
+
 }
 
 package androidx.compose.ui.input.key {
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index a8403cb..4944ff2 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -72,13 +72,13 @@
         implementation(libs.kotlinCoroutinesAndroid)
 
         implementation("androidx.activity:activity-ktx:1.5.1")
-        implementation("androidx.core:core:1.5.0")
+        implementation("androidx.core:core:1.9.0")
         implementation('androidx.collection:collection:1.0.0')
         implementation("androidx.customview:customview-poolingcontainer:1.0.0")
         implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-        implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-        implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+        implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+        implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
         implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         testImplementation(libs.testRules)
@@ -116,8 +116,8 @@
         androidTestImplementation(project(":internal-testutils-runtime"))
         androidTestImplementation(project(":test:screenshot:screenshot"))
         androidTestImplementation("androidx.recyclerview:recyclerview:1.3.0-alpha02")
-        androidTestImplementation("androidx.core:core-ktx:1.2.0")
-        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+        androidTestImplementation("androidx.core:core-ktx:1.9.0")
+        androidTestImplementation("androidx.activity:activity-compose:1.5.1")
         androidTestImplementation("androidx.appcompat:appcompat:1.3.0")
         androidTestImplementation("androidx.fragment:fragment:1.3.0")
 
@@ -167,13 +167,13 @@
                 implementation(libs.kotlinCoroutinesAndroid)
 
                 implementation("androidx.activity:activity-ktx:1.5.1")
-                implementation("androidx.core:core:1.5.0")
+                implementation("androidx.core:core:1.9.0")
                 implementation('androidx.collection:collection:1.0.0')
                 implementation("androidx.customview:customview-poolingcontainer:1.0.0")
                 implementation("androidx.savedstate:savedstate-ktx:1.2.0")
-                implementation("androidx.lifecycle:lifecycle-common-java8:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-runtime:2.3.0")
-                implementation("androidx.lifecycle:lifecycle-viewmodel:2.3.0")
+                implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-runtime:2.5.1")
+                implementation("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
             }
 
             jvmMain.dependencies {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index ad704ea..7d0d0ff 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -228,6 +228,13 @@
     )
 )
 
+private val AccessibilityDemos = DemoCategory(
+    "Accessibility",
+    listOf(
+        ComposableDemo("Overlaid Nodes") { OverlaidNodeLayoutDemo() }
+    )
+)
+
 val CoreDemos = DemoCategory(
     "Framework",
     listOf(
@@ -243,6 +250,7 @@
         GestureDemos,
         ViewInteropDemos,
         ComposableDemo("Software Keyboard Controller") { SoftwareKeyboardControllerDemo() },
-        RecyclerViewDemos
+        RecyclerViewDemos,
+        AccessibilityDemos
     )
 )
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
new file mode 100644
index 0000000..6e36aee
--- /dev/null
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.demos
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun LastElementOverLaidColumn(
+    modifier: Modifier = Modifier,
+    content: @Composable () -> Unit,
+) {
+    var yPosition = 0
+
+    Layout(modifier = modifier, content = content) { measurables, constraints ->
+        val placeables = measurables.map { measurable ->
+            measurable.measure(constraints)
+        }
+
+        layout(constraints.maxWidth, constraints.maxHeight) {
+            placeables.forEach { placeable ->
+                if (placeable != placeables[placeables.lastIndex]) {
+                    placeable.placeRelative(x = 0, y = yPosition)
+                    yPosition += placeable.height
+                } else {
+                    // if the element is our last element (our overlaid node)
+                    // then we'll put it over the middle of our previous elements
+                    placeable.placeRelative(x = 0, y = yPosition / 2)
+                }
+            }
+        }
+    }
+}
+
+@Preview
+@Composable
+fun OverlaidNodeLayoutDemo() {
+    LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+        Row {
+            Column {
+                Row { Text("text1\n") }
+                Row { Text("text2\n") }
+                Row { Text("text3\n") }
+            }
+        }
+        Row {
+            Text("overlaid node")
+        }
+    }
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt
new file mode 100644
index 0000000..7146d12
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ScrollableContainerSample.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.samples
+
+import androidx.annotation.Sampled
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
+import androidx.compose.ui.input.pointer.pointerInput
+import java.util.concurrent.TimeoutException
+import kotlinx.coroutines.withTimeout
+
+@Sampled
+@Composable
+fun ScrollableContainerSample() {
+    var isParentScrollable by remember { mutableStateOf({ false }) }
+
+    Column(Modifier.verticalScroll(rememberScrollState())) {
+
+        Box(modifier = Modifier.consumeScrollContainerInfo {
+            isParentScrollable = { it?.canScroll() == true }
+        }) {
+            Box(Modifier.pointerInput(Unit) {
+                detectTapGestures(
+                    onPress = {
+                        // If there is an ancestor that handles drag events, this press might
+                        // become a drag so delay any work
+                        val doWork = !isParentScrollable() || try {
+                            withTimeout(100) { tryAwaitRelease() }
+                        } catch (e: TimeoutException) {
+                            true
+                        }
+                        if (doWork) println("Do work")
+                    })
+            })
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index f9428e5..1b57cbe 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -65,6 +65,8 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
 import androidx.compose.runtime.getValue
@@ -80,6 +82,7 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.ClassName
@@ -560,6 +563,116 @@
         @Suppress("DEPRECATION") accessibilityNodeInfo.recycle()
     }
 
+    @Composable
+    fun LastElementOverLaidColumn(
+        modifier: Modifier = Modifier,
+        content: @Composable () -> Unit,
+    ) {
+        var yPosition = 0
+
+        Layout(modifier = modifier, content = content) { measurables, constraints ->
+            val placeables = measurables.map { measurable ->
+                measurable.measure(constraints)
+            }
+
+            layout(constraints.maxWidth, constraints.maxHeight) {
+                placeables.forEach { placeable ->
+                    if (placeable != placeables[placeables.lastIndex]) {
+                        placeable.placeRelative(x = 0, y = yPosition)
+                        yPosition += placeable.height
+                    } else {
+                        // if the element is our last element (our overlaid node)
+                        // then we'll put it over the middle of our previous elements
+                        placeable.placeRelative(x = 0, y = yPosition / 2)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layout() {
+        val overlaidText = "Overlaid node text"
+        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        container.setContent {
+            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+                Row {
+                    Column {
+                        Row { Text(text1) }
+                        Row { Text(text2) }
+                        Row { Text(text3) }
+                    }
+                }
+                Row {
+                    Text(overlaidText)
+                }
+            }
+        }
+
+        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
+        val overlaidNode = rule.onNodeWithText(overlaidText).fetchSemanticsNode()
+
+        val ani3 = provider.createAccessibilityNodeInfo(node3.id)
+        val ani3TraversalBeforeVal = ani3?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
+        // comparison (like SemanticsSort), the third text node should come before the overlaid node
+        // — OverlaidNode should be read last
+        assertNotEquals(ani3TraversalBeforeVal, 0)
+        if (ani3TraversalBeforeVal != null) {
+            assertEquals(ani3TraversalBeforeVal, overlaidNode.id)
+        }
+    }
+
+    @Test
+    fun testCreateAccessibilityNodeInfo_forTraversalOrder_layoutTestTags() {
+        val overlaidText = "Overlaid node text"
+        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+        container.setContent {
+            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
+                Row(modifier = Modifier
+                    .semantics(true) { contentDescription = "Row1" }
+                    .testTag("Row1")
+                ) {
+                    Column(modifier = Modifier.testTag("Column1")) {
+                        Row(modifier = Modifier.testTag("Text1")) { Text(text1) }
+                        Row(modifier = Modifier.testTag("Text2")) { Text(text2) }
+                        Row(modifier = Modifier.testTag("Text3")) { Text(text3) }
+                    }
+                }
+                Row(modifier = Modifier
+                    .semantics(true) { contentDescription = "Row2" }
+                    .testTag("Row2")
+                ) {
+                    Text(overlaidText)
+                }
+            }
+        }
+
+        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
+        val row2 = rule.onNodeWithTag("Row2").fetchSemanticsNode()
+
+        val ani3 = provider.createAccessibilityNodeInfo(node3.id)
+        val ani3TraversalBeforeVal = ani3?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
+        // comparison (like SemanticsSort), the third text node should come before the overlaid node
+        // — OverlaidNode and its row should be read last
+        assertNotEquals(ani3TraversalBeforeVal, 0)
+        if (ani3TraversalBeforeVal != null) {
+            assertTrue(ani3TraversalBeforeVal < row2.id)
+        }
+    }
+
+    companion object {
+        private const val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
+            "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
+    }
+
     @Test
     fun testPerformAction_showOnScreen() {
         rule.mainClock.autoAdvance = false
@@ -3065,6 +3178,61 @@
         assertEquals(childInfo.isScreenReaderFocusable, false)
     }
 
+    @Test
+    fun accessibilityStateChangeListenerRemoved_onDetach() {
+        delegate.accessibilityForceEnabledForTesting = false
+
+        rule.runOnIdle {
+            assertTrue(androidComposeView.isAttachedToWindow)
+        }
+
+        rule.runOnUiThread {
+            container.removeView(androidComposeView)
+        }
+
+        rule.runOnIdle {
+            assertFalse(androidComposeView.isAttachedToWindow)
+
+            val removed = delegate.accessibilityManager.removeAccessibilityStateChangeListener(
+                delegate.enabledStateListener
+            )
+            assertFalse(removed)
+        }
+    }
+
+    @Test
+    fun touchExplorationChangeListenerRemoved_onDetach() {
+        delegate.accessibilityForceEnabledForTesting = false
+
+        rule.runOnIdle {
+            assertTrue(androidComposeView.isAttachedToWindow)
+        }
+
+        rule.runOnUiThread {
+            container.removeView(androidComposeView)
+        }
+
+        rule.runOnIdle {
+            assertFalse(androidComposeView.isAttachedToWindow)
+
+            val removed = delegate.accessibilityManager.removeTouchExplorationStateChangeListener(
+                delegate.touchExplorationStateListener
+            )
+            assertFalse(removed)
+        }
+    }
+
+    @Test
+    fun isEnabled_returnsFalse_whenUIAutomatorIsTheOnlyEnabledService() {
+        delegate.accessibilityForceEnabledForTesting = false
+
+        rule.runOnIdle {
+            // This test implies that UIAutomator is enabled and is the only enabled a11y service
+            assertTrue(delegate.accessibilityManager.isEnabled)
+            assertFalse(delegate.isEnabled)
+        }
+    }
+
     private fun eventIndex(list: List<AccessibilityEvent>, event: AccessibilityEvent): Int {
         for (i in list.indices) {
             if (ReflectionEquals(list[i], null).matches(event)) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 792181a..588bd86 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -111,7 +111,6 @@
 import androidx.compose.ui.unit.offset
 import androidx.compose.ui.unit.toOffset
 import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import org.junit.Assert.assertEquals
@@ -120,6 +119,7 @@
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -3099,7 +3099,7 @@
         validateSquareColors(outerColor = Color.Blue, innerColor = Color.White, size = 10)
     }
 
-    @FlakyTest
+    @Ignore // b/173806298
     @Test
     fun makingItemLarger() {
         var height by mutableStateOf(30)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
index de8dd50..a7d7eba 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.foundation.shape.GenericShape
 import androidx.compose.runtime.Composable
@@ -91,15 +92,15 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import kotlin.math.ceil
+import kotlin.math.roundToInt
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertTrue
 import org.junit.Assert.fail
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.math.roundToInt
-import org.junit.Assert.assertNotNull
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -1349,4 +1350,37 @@
         assertEquals(sizePx, drawScopeWidth)
         assertEquals(sizePx, drawScopeHeight)
     }
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun removingGraphicsLayerInvalidatesParentLayer() {
+        var toggle by mutableStateOf(true)
+        val size = 100
+        rule.setContent {
+            val sizeDp = with(LocalDensity.current) { size.toDp() }
+            LazyColumn(Modifier.testTag("lazy").background(Color.Blue)) {
+                items(4) {
+                    Box(
+                        Modifier
+                            .then(if (toggle) Modifier.graphicsLayer(alpha = 0f) else Modifier)
+                            .background(Color.Red)
+                            .size(sizeDp)
+                    )
+                }
+            }
+        }
+
+        rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+            assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+            assertEquals(Color.Blue.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+        }
+
+        rule.runOnIdle {
+            toggle = !toggle
+        }
+
+        rule.onNodeWithTag("lazy").captureToImage().asAndroidBitmap().apply {
+            assertEquals(Color.Red.toArgb(), getPixel(10, (size * 1.5f).roundToInt()))
+            assertEquals(Color.Red.toArgb(), getPixel(10, (size * 2.5f).roundToInt()))
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
index 16f6740..94300f6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/TextInputServiceAndroidOnStateUpdateTest.kt
@@ -70,8 +70,8 @@
             newValue = newValue
         )
 
-        verify(inputMethodManager, times(1)).restartInput(any())
-        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, times(1)).restartInput()
+        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
         assertThat(textInputService.state).isEqualTo(newValue)
@@ -85,8 +85,8 @@
             newValue = newValue
         )
 
-        verify(inputMethodManager, times(1)).restartInput(any())
-        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, times(1)).restartInput()
+        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
         assertThat(textInputService.state).isEqualTo(newValue)
@@ -100,8 +100,8 @@
             newValue = newValue
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
-        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, never()).restartInput()
+        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
         assertThat(textInputService.state).isEqualTo(newValue)
@@ -121,8 +121,8 @@
             newValue = value
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
-        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, never()).restartInput()
+        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(value)
         assertThat(textInputService.state).isEqualTo(value)
@@ -148,9 +148,9 @@
             newValue = value2
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
+        verify(inputMethodManager, never()).restartInput()
         verify(inputMethodManager, times(1)).updateSelection(
-            any(), eq(value2.selection.min), eq(value2.selection.max), eq(-1), eq(-1)
+            eq(value2.selection.min), eq(value2.selection.max), eq(-1), eq(-1)
         )
     }
 
@@ -162,8 +162,8 @@
             newValue = newValue
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
-        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, never()).restartInput()
+        verify(inputMethodManager, times(1)).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(newValue)
         assertThat(textInputService.state).isEqualTo(newValue)
@@ -177,8 +177,8 @@
             newValue = value
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
-        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, never()).restartInput()
+        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
 
         assertThat(inputConnection.mTextFieldValue).isEqualTo(value)
         assertThat(textInputService.state).isEqualTo(value)
@@ -192,8 +192,8 @@
             newValue = value
         )
 
-        verify(inputMethodManager, never()).restartInput(any())
-        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any(), any())
+        verify(inputMethodManager, never()).restartInput()
+        verify(inputMethodManager, never()).updateSelection(any(), any(), any(), any())
 
         // recreate the connection
         inputConnection = textInputService.createInputConnection(EditorInfo())
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.kt
new file mode 100644
index 0000000..4bf39eb
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/nestedscroll/ScrollContainerInfoTest.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.
+ */
+
+import androidx.compose.ui.input.ScrollContainerInfo
+import androidx.compose.ui.input.canScroll
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ScrollContainerInfoTest {
+
+    @Test
+    fun canScroll_horizontal() {
+        val subject = Subject(horizontal = true)
+
+        assertThat(subject.canScroll()).isTrue()
+    }
+
+    @Test
+    fun canScroll_vertical() {
+        val subject = Subject(vertical = true)
+
+        assertThat(subject.canScroll()).isTrue()
+    }
+
+    @Test
+    fun canScroll_both() {
+        val subject = Subject(horizontal = true, vertical = true)
+
+        assertThat(subject.canScroll()).isTrue()
+    }
+
+    @Test
+    fun canScroll_neither() {
+        val subject = Subject(horizontal = false, vertical = false)
+
+        assertThat(subject.canScroll()).isFalse()
+    }
+
+    class Subject(
+        private val horizontal: Boolean = false,
+        private val vertical: Boolean = false,
+    ) : ScrollContainerInfo {
+        override fun canScrollHorizontally(): Boolean = horizontal
+        override fun canScrollVertically(): Boolean = vertical
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
index a71891f..f3a63ae 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/SubcomposeLayoutTest.kt
@@ -64,6 +64,7 @@
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
@@ -1198,6 +1199,57 @@
     }
 
     @Test
+    fun staticCompositionLocalChangeInMainComposition_withNonStaticLocal_invalidatesComposition() {
+        var isDark by mutableStateOf(false)
+
+        val staticLocal = staticCompositionLocalOf<Boolean> { error("Not defined") }
+        val local = compositionLocalOf<Boolean> { error("Not defined") }
+        val innerLocal = staticCompositionLocalOf<Unit> { error("\not defined") }
+
+        val content = @Composable {
+            CompositionLocalProvider(innerLocal provides Unit) {
+                val value1 = staticLocal.current
+                val value2 = local.current
+                Box(
+                    Modifier
+                        .testTag(if (value1) "dark" else "light")
+                        .requiredSize(if (value2) 50.dp else 100.dp)
+                )
+            }
+        }
+
+        rule.setContent {
+            CompositionLocalProvider(
+                staticLocal provides isDark,
+            ) {
+                CompositionLocalProvider(
+                    local provides staticLocal.current
+                ) {
+                    SubcomposeLayout { constraints ->
+                        val measurables = subcompose(Unit, content)
+                        val placeables = measurables.map {
+                            it.measure(constraints)
+                        }
+                        layout(100, 100) {
+                            placeables.forEach { it.place(IntOffset.Zero) }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag("light")
+            .assertWidthIsEqualTo(100.dp)
+
+        isDark = true
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag("dark")
+            .assertWidthIsEqualTo(50.dp)
+    }
+
+    @Test
     fun derivedStateChangeInMainCompositionRecomposesSubcomposition() {
         var flag by mutableStateOf(true)
         var subcomposionValue: Boolean? = null
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
index 83a4a4f..1bfa2ff 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/ObserverNodeTest.kt
@@ -37,17 +37,12 @@
     @get:Rule
     val rule = createComposeRule()
 
-    var value by mutableStateOf(1)
-    var callbackInvoked = false
-
     @Test
     fun simplyObservingValue_doesNotTriggerCallback() {
         // Arrange.
-        val observerNode = object : ObserverNode, Modifier.Node() {
-            override fun onObservedReadsChanged() {
-                callbackInvoked = true
-            }
-        }
+        val value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
         rule.setContent {
             Box(Modifier.modifierElementOf { observerNode })
         }
@@ -67,11 +62,9 @@
     @Test
     fun changeInObservedValue_triggersCallback() {
         // Arrange.
-        val observerNode = object : ObserverNode, Modifier.Node() {
-            override fun onObservedReadsChanged() {
-                callbackInvoked = true
-            }
-        }
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
         rule.setContent {
             Box(Modifier.modifierElementOf { observerNode })
         }
@@ -91,10 +84,94 @@
         }
     }
 
+    @Test(expected = IllegalStateException::class)
+    fun unusedNodeDoesNotObserve() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+
+        // Act.
+        rule.runOnIdle {
+            // Read value to observe changes.
+            observerNode.observeReads { value.toString() }
+
+            // Write to the read value to trigger onObservedReadsChanged.
+            value = 3
+        }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isFalse()
+        }
+    }
+
+    @Test
+    fun detachedNodeCanObserveReads() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            Box(if (attached) modifierElementOf { observerNode } else Modifier)
+        }
+
+        // Act.
+        // Read value while not attached.
+        rule.runOnIdle { attached = false }
+        rule.runOnIdle { observerNode.observeReads { value.toString() } }
+        rule.runOnIdle { attached = true }
+        // Write to the read value to trigger onObservedReadsChanged.
+        rule.runOnIdle { value = 3 }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isTrue()
+        }
+    }
+
+    @Test
+    fun detachedNodeDoesNotCallOnObservedReadsChanged() {
+        // Arrange.
+        var value by mutableStateOf(1)
+        var callbackInvoked = false
+        val observerNode = TestObserverNode { callbackInvoked = true }
+        var attached by mutableStateOf(true)
+        rule.setContent {
+            Box(if (attached) modifierElementOf { observerNode } else Modifier)
+        }
+
+        // Act.
+        rule.runOnIdle {
+            // Read value to observe changes.
+            observerNode.observeReads { value.toString() }
+        }
+
+        rule.runOnIdle {
+            attached = false
+        }
+        // Write to the read value to trigger onObservedReadsChanged.
+        rule.runOnIdle { value = 3 }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(callbackInvoked).isFalse()
+        }
+    }
+
     @ExperimentalComposeUiApi
     private inline fun <reified T : Modifier.Node> Modifier.modifierElementOf(
-        crossinline create: () -> T
+        crossinline create: () -> T,
     ): Modifier {
         return this.then(modifierElementOf(create) { name = "testNode" })
     }
+
+    class TestObserverNode(
+        private val onObserveReadsChanged: () -> Unit,
+    ) : ObserverNode, Modifier.Node() {
+        override fun onObservedReadsChanged() {
+            this.onObserveReadsChanged()
+        }
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 0885058..7919713 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
 import android.widget.FrameLayout
+import android.widget.LinearLayout
 import android.widget.RelativeLayout
 import android.widget.TextView
 import androidx.compose.foundation.background
@@ -38,6 +39,8 @@
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
@@ -52,6 +55,8 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
@@ -86,6 +91,7 @@
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
+import kotlin.math.roundToInt
 import org.hamcrest.CoreMatchers.endsWith
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.CoreMatchers.instanceOf
@@ -93,7 +99,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.math.roundToInt
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -637,11 +642,19 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     fun androidView_noClip() {
         rule.setContent {
-            Box(Modifier.fillMaxSize().background(Color.White)) {
+            Box(
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.White)) {
                 with(LocalDensity.current) {
-                    Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+                    Box(
+                        Modifier
+                            .requiredSize(150.toDp())
+                            .testTag("box")) {
                         Box(
-                            Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+                            Modifier
+                                .size(100.toDp(), 100.toDp())
+                                .align(AbsoluteAlignment.TopLeft)
                         ) {
                             AndroidView(factory = { context ->
                                 object : View(context) {
@@ -667,6 +680,92 @@
         }
     }
 
+    @Test
+    fun scrollableViewGroup_propagates_shouldDelay() {
+        val scrollContainerInfo = mutableStateOf({ false })
+        rule.activityRule.scenario.onActivity { activity ->
+            val parentComposeView = ScrollingViewGroup(activity).apply {
+                addView(
+                    ComposeView(activity).apply {
+                        setContent {
+                            Box(modifier = Modifier.consumeScrollContainerInfo {
+                                scrollContainerInfo.value = { it?.canScroll() == true }
+                            })
+                        }
+                    })
+                }
+            activity.setContentView(parentComposeView)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollContainerInfo.value()).isTrue()
+        }
+    }
+
+    @Test
+    fun nonScrollableViewGroup_doesNotPropagate_shouldDelay() {
+        val scrollContainerInfo = mutableStateOf({ false })
+        rule.activityRule.scenario.onActivity { activity ->
+            val parentComposeView = FrameLayout(activity).apply {
+                addView(
+                    ComposeView(activity).apply {
+                        setContent {
+                            Box(modifier = Modifier.consumeScrollContainerInfo {
+                                scrollContainerInfo.value = { it?.canScroll() == true }
+                            })
+                        }
+                    })
+            }
+            activity.setContentView(parentComposeView)
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollContainerInfo.value()).isFalse()
+        }
+    }
+
+    @Test
+    fun viewGroup_propagates_shouldDelayTrue() {
+        lateinit var layout: View
+        rule.setContent {
+            Column(Modifier.verticalScroll(rememberScrollState())) {
+                AndroidView(
+                    factory = {
+                        layout = LinearLayout(it)
+                        layout
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            // View#isInScrollingContainer is hidden, check the parent manually.
+            val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
+            assertThat(shouldDelay).isTrue()
+        }
+    }
+
+    @Test
+    fun viewGroup_propagates_shouldDelayFalse() {
+        lateinit var layout: View
+        rule.setContent {
+            Column {
+                AndroidView(
+                    factory = {
+                        layout = LinearLayout(it)
+                        layout
+                    }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            // View#isInScrollingContainer is hidden, check the parent manually.
+            val shouldDelay = (layout.parent as ViewGroup).shouldDelayChildPressedState()
+            assertThat(shouldDelay).isFalse()
+        }
+    }
+
     private class StateSavingView(
         private val key: String,
         private val value: String,
@@ -698,4 +797,8 @@
             value,
             displayMetrics
         ).roundToInt()
+
+    class ScrollingViewGroup(context: Context) : FrameLayout(context) {
+        override fun shouldDelayChildPressedState() = true
+    }
 }
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 da9ee0f..02c2c59 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
@@ -73,6 +73,8 @@
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SmallTest
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import org.hamcrest.CoreMatchers.instanceOf
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -83,8 +85,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 import kotlin.math.roundToInt
 
 @MediumTest
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index 2eb61e2..fefb238 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -126,6 +126,9 @@
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.node.OwnerSnapshotObserver
 import androidx.compose.ui.node.RootForTest
+import androidx.compose.ui.input.ScrollContainerInfo
+import androidx.compose.ui.input.ModifierLocalScrollContainerInfo
+import androidx.compose.ui.modifier.ModifierLocalProvider
 import androidx.compose.ui.semantics.SemanticsModifierCore
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.semantics.SemanticsOwner
@@ -220,6 +223,34 @@
         false
     }
 
+    // We don't have a way to determine direction in Android, return true for both directions.
+    private val scrollContainerInfo = object : ModifierLocalProvider<ScrollContainerInfo?> {
+        override val key = ModifierLocalScrollContainerInfo
+        override val value = object : ScrollContainerInfo {
+            // Intentionally not using [View#canScrollHorizontally], to maintain semantics of
+            // View#isInScrollingContainer
+            override fun canScrollHorizontally(): Boolean =
+                view.isInScrollableViewGroup()
+
+            // Intentionally not using [View#canScrollVertically], to maintain semantics of
+            // View#isInScrollingContainer
+            override fun canScrollVertically(): Boolean =
+                view.isInScrollableViewGroup()
+
+            // Copied from View#isInScrollingContainer() which is @hide
+            private fun View.isInScrollableViewGroup(): Boolean {
+                var p = parent
+                while (p != null && p is ViewGroup) {
+                    if (p.shouldDelayChildPressedState()) {
+                        return true
+                    }
+                    p = p.parent
+                }
+                return false
+            }
+        }
+    }
+
     private val canvasHolder = CanvasHolder()
 
     override val root = LayoutNode().also {
@@ -231,6 +262,7 @@
             .then(rotaryInputModifier)
             .then(_focusManager.modifier)
             .then(keyInputModifier)
+            .then(scrollContainerInfo)
     }
 
     override val rootForTest: RootForTest = this
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 28504c3..aed28d7 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -30,6 +30,8 @@
 import android.view.View
 import android.view.accessibility.AccessibilityEvent
 import android.view.accessibility.AccessibilityManager
+import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener
+import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
 import android.view.accessibility.AccessibilityNodeInfo
 import android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH
 import android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX
@@ -50,6 +52,7 @@
 import androidx.compose.ui.graphics.toAndroidRect
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.boundsInParent
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.node.HitTestResult
 import androidx.compose.ui.node.LayoutNode
@@ -83,7 +86,6 @@
 import androidx.compose.ui.text.platform.toAccessibilitySpannableString
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.util.fastFirstOrNull
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastForEachIndexed
 import androidx.core.view.AccessibilityDelegateCompat
@@ -187,20 +189,41 @@
 
     /** Virtual view id for the currently hovered logical item. */
     internal var hoveredVirtualViewId = InvalidId
-    private val accessibilityManager: AccessibilityManager =
+
+    @VisibleForTesting
+    internal val accessibilityManager: AccessibilityManager =
         view.context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
     internal var accessibilityForceEnabledForTesting = false
 
+    @VisibleForTesting
+    internal val enabledStateListener: AccessibilityStateChangeListener =
+        AccessibilityStateChangeListener { enabled ->
+            enabledServices = if (enabled) {
+                accessibilityManager.getEnabledAccessibilityServiceList(
+                    AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+                )
+            } else {
+                emptyList()
+            }
+        }
+    @VisibleForTesting
+    internal val touchExplorationStateListener: TouchExplorationStateChangeListener =
+        TouchExplorationStateChangeListener {
+            enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
+                AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+            )
+        }
+    private var enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
+        AccessibilityServiceInfo.FEEDBACK_ALL_MASK
+    )
     /**
      * True if any accessibility service enabled in the system, except the UIAutomator (as it
      * doesn't appear in the list of enabled services)
      */
-    private val isEnabled: Boolean
+    @VisibleForTesting
+    internal val isEnabled: Boolean
         get() {
             // checking the list allows us to filter out the UIAutomator which doesn't appear in it
-            val enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
-                AccessibilityServiceInfo.FEEDBACK_ALL_MASK
-            )
             return accessibilityForceEnabledForTesting ||
                 accessibilityManager.isEnabled && enabledServices.isNotEmpty()
         }
@@ -214,20 +237,6 @@
         get() = accessibilityForceEnabledForTesting ||
                 accessibilityManager.isEnabled && accessibilityManager.isTouchExplorationEnabled
 
-    /** True if an accessibility service that listens for the event of type [eventType] is enabled
-     * in the system.
-     * Note that UIAutomator will always return false as it doesn't appear in the list of enabled
-     * services
-     */
-    private fun isEnabledForEvent(eventType: Int): Boolean {
-        val enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
-            AccessibilityServiceInfo.FEEDBACK_ALL_MASK
-        )
-        return enabledServices.fastFirstOrNull {
-            it.eventTypes.and(eventType) != 0
-        } != null || accessibilityForceEnabledForTesting
-    }
-
     private val handler = Handler(Looper.getMainLooper())
     private var nodeProvider: AccessibilityNodeProviderCompat =
         AccessibilityNodeProviderCompat(MyNodeProvider())
@@ -264,13 +273,17 @@
      */
     private var currentSemanticsNodes: Map<Int, SemanticsNodeWithAdjustedBounds> = mapOf()
         get() {
-            if (currentSemanticsNodesInvalidated) {
-                field = view.semanticsOwner.getAllUncoveredSemanticsNodesToMap()
+            if (currentSemanticsNodesInvalidated) { // first instance of retrieving all nodes
                 currentSemanticsNodesInvalidated = false
+                field = view.semanticsOwner.getAllUncoveredSemanticsNodesToMap()
+                setTraversalValues()
             }
             return field
         }
     private var paneDisplayed = ArraySet<Int>()
+    private var idToBeforeMap = HashMap<Int, Int>()
+    private val EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL =
+        "android.view.accessibility.extra.EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL"
 
     /**
      * A snapshot of the semantics node. The children here is fixed and are taken from the time
@@ -308,9 +321,19 @@
     init {
         // Remove callbacks that rely on view being attached to a window when we become detached.
         view.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
-            override fun onViewAttachedToWindow(view: View) {}
+            override fun onViewAttachedToWindow(view: View) {
+                accessibilityManager.addAccessibilityStateChangeListener(enabledStateListener)
+                accessibilityManager.addTouchExplorationStateChangeListener(
+                    touchExplorationStateListener
+                )
+            }
             override fun onViewDetachedFromWindow(view: View) {
                 handler.removeCallbacks(semanticsChangeChecker)
+
+                accessibilityManager.removeAccessibilityStateChangeListener(enabledStateListener)
+                accessibilityManager.removeTouchExplorationStateChangeListener(
+                    touchExplorationStateListener
+                )
             }
         })
     }
@@ -417,6 +440,44 @@
         return info.unwrap()
     }
 
+    private fun setTraversalValues() {
+        idToBeforeMap.clear()
+        var idToCoordinatesList = mutableListOf<Pair<Int, Rect>>()
+
+        fun depthFirstSearch(currNode: SemanticsNode) {
+            if (currNode.parent?.layoutNode?.innerCoordinator?.isAttached == true &&
+                currNode.layoutNode.innerCoordinator.isAttached
+            ) {
+                idToCoordinatesList.add(
+                    Pair(
+                        currNode.id,
+                        currNode.layoutNode.coordinates.boundsInWindow()
+                    )
+                )
+            }
+            // This retrieves the children in the order that we want (respecting child/parent
+            // hierarchies)
+            currNode.replacedChildrenSortedByBounds.fastForEach { child ->
+                depthFirstSearch(child)
+            }
+        }
+
+        currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]?.semanticsNode
+            ?.replacedChildrenSortedByBounds?.fastForEach { node ->
+                depthFirstSearch(node)
+            }
+
+        // Iterate through our ordered list, and creating a mapping of current node to next node ID
+        // We'll later read through this and set traversal order with IdToBeforeMap
+        for (i in 1..idToCoordinatesList.lastIndex) {
+            val prevId = idToCoordinatesList[i - 1].first
+            val currId = idToCoordinatesList[i].first
+            idToBeforeMap[prevId] = currId
+        }
+
+        return
+    }
+
     @VisibleForTesting
     @OptIn(ExperimentalComposeUiApi::class)
     fun populateAccessibilityNodeInfoProperties(
@@ -476,7 +537,7 @@
         // "important".
         info.isImportantForAccessibility = true
 
-        semanticsNode.replacedChildrenSortedByBounds.fastForEach { child ->
+        semanticsNode.replacedChildren.fastForEach { child ->
             if (currentSemanticsNodes.contains(child.id)) {
                 val holder = view.androidViewsHandler.layoutNodeToHolder[child.layoutNode]
                 if (holder != null) {
@@ -961,6 +1022,12 @@
         info.isScreenReaderFocusable =
             semanticsNode.unmergedConfig.isMergingSemanticsOfDescendants ||
             isUnmergedLeafNode && isSpeakingNode
+
+        if (idToBeforeMap[virtualViewId] != null) {
+            idToBeforeMap[virtualViewId]?.let { info.setTraversalBefore(view, it) }
+            addExtraDataToAccessibilityNodeInfoHelper(
+                virtualViewId, info.unwrap(), EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL, null)
+        }
     }
 
     /** Set the error text for this node */
@@ -1065,7 +1132,7 @@
         contentChangeType: Int? = null,
         contentDescription: List<String>? = null
     ): Boolean {
-        if (virtualViewId == InvalidId || !isEnabledForEvent(eventType)) {
+        if (virtualViewId == InvalidId || !isEnabled) {
             return false
         }
 
@@ -1088,7 +1155,7 @@
      */
     private fun sendEvent(event: AccessibilityEvent): Boolean {
         // only send an event if there's an enabled service listening for events of this type
-        if (!isEnabledForEvent(event.eventType)) {
+        if (!isEnabled) {
             return false
         }
 
@@ -1441,7 +1508,14 @@
     ) {
         val node = currentSemanticsNodes[virtualViewId]?.semanticsNode ?: return
         val text = getIterableTextForAccessibility(node)
-        if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
+
+        // This extra is just for testing: needed a way to retrieve `traversalBefore` from
+        // non-sealed instance of ANI
+        if (extraDataKey == EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL) {
+            idToBeforeMap[virtualViewId]?.let {
+                info.extras.putInt(extraDataKey, it)
+            }
+        } else if (node.unmergedConfig.contains(SemanticsActions.GetTextLayoutResult) &&
             arguments != null && extraDataKey == EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY
         ) {
             val positionInfoStartIndex = arguments.getInt(
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
index 39071a16..50d67c6 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/InputMethodManager.kt
@@ -16,26 +16,33 @@
 
 package androidx.compose.ui.text.input
 
+import android.app.Activity
 import android.content.Context
-import android.os.IBinder
+import android.content.ContextWrapper
+import android.os.Build
+import android.util.Log
 import android.view.View
+import android.view.Window
 import android.view.inputmethod.ExtractedText
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.window.DialogWindowProvider
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat
 
 internal interface InputMethodManager {
-    fun restartInput(view: View)
+    fun restartInput()
 
-    fun showSoftInput(view: View)
+    fun showSoftInput()
 
-    fun hideSoftInputFromWindow(windowToken: IBinder?)
+    fun hideSoftInput()
 
     fun updateExtractedText(
-        view: View,
         token: Int,
         extractedText: ExtractedText
     )
 
     fun updateSelection(
-        view: View,
         selectionStart: Int,
         selectionEnd: Int,
         compositionStart: Int,
@@ -47,27 +54,36 @@
  * Wrapper class to prevent depending on getSystemService and final InputMethodManager.
  * Let's us test TextInputServiceAndroid class.
  */
-internal class InputMethodManagerImpl(context: Context) : InputMethodManager {
+internal class InputMethodManagerImpl(private val view: View) : InputMethodManager {
 
     private val imm by lazy(LazyThreadSafetyMode.NONE) {
-        context.getSystemService(Context.INPUT_METHOD_SERVICE)
+        view.context.getSystemService(Context.INPUT_METHOD_SERVICE)
             as android.view.inputmethod.InputMethodManager
     }
 
-    override fun restartInput(view: View) {
+    private val helper = if (Build.VERSION.SDK_INT < 30) {
+        ImmHelper21(view)
+    } else {
+        ImmHelper30(view)
+    }
+
+    override fun restartInput() {
         imm.restartInput(view)
     }
 
-    override fun showSoftInput(view: View) {
-        imm.showSoftInput(view, 0)
+    override fun showSoftInput() {
+        if (DEBUG && !view.hasWindowFocus()) {
+            Log.d(TAG, "InputMethodManagerImpl: requesting soft input on non focused field")
+        }
+
+        helper.showSoftInput(imm)
     }
 
-    override fun hideSoftInputFromWindow(windowToken: IBinder?) {
-        imm.hideSoftInputFromWindow(windowToken, 0)
+    override fun hideSoftInput() {
+        helper.hideSoftInput(imm)
     }
 
     override fun updateExtractedText(
-        view: View,
         token: Int,
         extractedText: ExtractedText
     ) {
@@ -75,7 +91,6 @@
     }
 
     override fun updateSelection(
-        view: View,
         selectionStart: Int,
         selectionEnd: Int,
         compositionStart: Int,
@@ -83,4 +98,71 @@
     ) {
         imm.updateSelection(view, selectionStart, selectionEnd, compositionStart, compositionEnd)
     }
-}
\ No newline at end of file
+}
+
+private interface ImmHelper {
+    fun showSoftInput(imm: android.view.inputmethod.InputMethodManager)
+    fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager)
+}
+
+private class ImmHelper21(private val view: View) : ImmHelper {
+
+    @DoNotInline
+    override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+        view.post {
+            imm.showSoftInput(view, 0)
+        }
+    }
+
+    @DoNotInline
+    override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+        imm.hideSoftInputFromWindow(view.windowToken, 0)
+    }
+}
+
+@RequiresApi(30)
+private class ImmHelper30(private val view: View) : ImmHelper {
+
+    /**
+     * Get a [WindowInsetsControllerCompat] for the view. This returns a new instance every time,
+     * since the view may return null or not null at different times depending on window attach
+     * state.
+     */
+    private val insetsControllerCompat
+        // This can return null when, for example, the view is not attached to a window.
+        get() = view.findWindow()?.let { WindowInsetsControllerCompat(it, view) }
+
+    /**
+     * This class falls back to the legacy implementation when the window insets controller isn't
+     * available.
+     */
+    private val immHelper21: ImmHelper21
+        get() = _immHelper21 ?: ImmHelper21(view).also { _immHelper21 = it }
+    private var _immHelper21: ImmHelper21? = null
+
+    @DoNotInline
+    override fun showSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+        insetsControllerCompat?.apply {
+            show(WindowInsetsCompat.Type.ime())
+        } ?: immHelper21.showSoftInput(imm)
+    }
+
+    @DoNotInline
+    override fun hideSoftInput(imm: android.view.inputmethod.InputMethodManager) {
+        insetsControllerCompat?.apply {
+            hide(WindowInsetsCompat.Type.ime())
+        } ?: immHelper21.hideSoftInput(imm)
+    }
+
+    // TODO(b/221889664) Replace with composition local when available.
+    private fun View.findWindow(): Window? =
+        (parent as? DialogWindowProvider)?.window
+            ?: context.findWindow()
+
+    private tailrec fun Context.findWindow(): Window? =
+        when (this) {
+            is Activity -> window
+            is ContextWrapper -> baseContext.findWindow()
+            else -> null
+        }
+}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
index 97ee595..f234d53 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/RecordingInputConnection.android.kt
@@ -21,7 +21,6 @@
 import android.text.TextUtils
 import android.util.Log
 import android.view.KeyEvent
-import android.view.View
 import android.view.inputmethod.CompletionInfo
 import android.view.inputmethod.CorrectionInfo
 import android.view.inputmethod.EditorInfo
@@ -94,7 +93,6 @@
     fun updateInputState(
         state: TextFieldValue,
         inputMethodManager: InputMethodManager,
-        view: View
     ) {
         if (!isActive) return
 
@@ -104,7 +102,6 @@
 
         if (extractedTextMonitorMode) {
             inputMethodManager.updateExtractedText(
-                view,
                 currentExtractedTextRequestToken,
                 state.toExtractedText()
             )
@@ -121,7 +118,7 @@
             )
         }
         inputMethodManager.updateSelection(
-            view, state.selection.min, state.selection.max, compositionStart, compositionEnd
+            state.selection.min, state.selection.max, compositionStart, compositionEnd
         )
     }
 
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
index 1dbb467..14cff951 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroid.android.kt
@@ -95,10 +95,12 @@
      */
     private val textInputCommandChannel = Channel<TextInputCommand>(Channel.UNLIMITED)
 
-    internal constructor(view: View) : this(view, InputMethodManagerImpl(view.context))
+    internal constructor(view: View) : this(view, InputMethodManagerImpl(view))
 
     init {
-        if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.create") }
+        if (DEBUG) {
+            Log.d(TAG, "$DEBUG_CLASS.create")
+        }
     }
 
     /**
@@ -138,7 +140,9 @@
             }
         ).also {
             ics.add(WeakReference(it))
-            if (DEBUG) { Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics") }
+            if (DEBUG) {
+                Log.d(TAG, "$DEBUG_CLASS.createInputConnection: $ics")
+            }
         }
     }
 
@@ -324,7 +328,6 @@
             if (needUpdateSelection) {
                 // updateSelection API requires -1 if there is no composition
                 inputMethodManager.updateSelection(
-                    view = view,
                     selectionStart = newValue.selection.min,
                     selectionEnd = newValue.selection.max,
                     compositionStart = state.composition?.min ?: -1,
@@ -348,7 +351,7 @@
             restartInputImmediately()
         } else {
             for (i in 0 until ics.size) {
-                ics[i].get()?.updateInputState(this.state, inputMethodManager, view)
+                ics[i].get()?.updateInputState(this.state, inputMethodManager)
             }
         }
     }
@@ -380,16 +383,16 @@
     /** Immediately restart the IME connection, bypassing the [textInputCommandChannel]. */
     private fun restartInputImmediately() {
         if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.restartInputImmediately")
-        inputMethodManager.restartInput(view)
+        inputMethodManager.restartInput()
     }
 
     /** Immediately show or hide the keyboard, bypassing the [textInputCommandChannel]. */
     private fun setKeyboardVisibleImmediately(visible: Boolean) {
         if (DEBUG) Log.d(TAG, "$DEBUG_CLASS.setKeyboardVisibleImmediately(visible=$visible)")
         if (visible) {
-            inputMethodManager.showSoftInput(view)
+            inputMethodManager.showSoftInput()
         } else {
-            inputMethodManager.hideSoftInputFromWindow(view.windowToken)
+            inputMethodManager.hideSoftInput()
         }
     }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index 9b725ee..6032a96 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -61,9 +61,9 @@
  * Compose and there is no corresponding Compose API. Common examples for the moment are
  * WebView, SurfaceView, AdView, etc.
  *
- * [AndroidView] will clip its content to the layout bounds, as being clipped is a common
- * assumption made by [View]s - keeping clipping disabled might lead to unexpected drawing behavior.
- * Note this deviates from Compose's practice of keeping clipping opt-in, disabled by default.
+ * [AndroidView] will not clip its content to the layout bounds. Use [View.setClipToOutline] on
+ * the child View to clip the contents, if desired. Developers will likely want to do this with
+ * all subclasses of SurfaceView to keep its contents contained.
  *
  * [AndroidView] has nested scroll interop capabilities if the containing view has nested scroll
  * enabled. This means this Composable can dispatch scroll deltas if it is placed inside a
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
index 7638528..4694d96 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidViewHolder.android.kt
@@ -35,6 +35,8 @@
 import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.pointer.pointerInteropFilter
+import androidx.compose.ui.input.canScroll
+import androidx.compose.ui.input.consumeScrollContainerInfo
 import androidx.compose.ui.layout.IntrinsicMeasurable
 import androidx.compose.ui.layout.IntrinsicMeasureScope
 import androidx.compose.ui.layout.Measurable
@@ -181,6 +183,8 @@
     private val nestedScrollingParentHelper: NestedScrollingParentHelper =
         NestedScrollingParentHelper(this)
 
+    private var isInScrollContainer: () -> Boolean = { true }
+
     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
         view?.measure(widthMeasureSpec, heightMeasureSpec)
         setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
@@ -280,11 +284,15 @@
                     (layoutNode.owner as? AndroidComposeView)
                         ?.drawAndroidView(this@AndroidViewHolder, canvas.nativeCanvas)
                 }
-            }.onGloballyPositioned {
+            }
+            .onGloballyPositioned {
                 // The global position of this LayoutNode can change with it being replaced. For
                 // these cases, we need to inform the View.
                 layoutAccordingTo(layoutNode)
             }
+            .consumeScrollContainerInfo { scrollContainerInfo ->
+                isInScrollContainer = { scrollContainerInfo?.canScroll() == true }
+            }
         layoutNode.modifier = modifier.then(coreModifier)
         onModifierChanged = { layoutNode.modifier = it.then(coreModifier) }
 
@@ -398,9 +406,7 @@
         }
     }
 
-    // TODO: b/203141462 - consume whether the AndroidView() is inside a scrollable container, and
-    //  use that to set this. In the meantime set true as the defensive default.
-    override fun shouldDelayChildPressedState(): Boolean = true
+    override fun shouldDelayChildPressedState(): Boolean = isInScrollContainer()
 
     // NestedScrollingParent3
     override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
index 84259b4..00116b9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ExperimentalComposeUiApi.kt
@@ -17,4 +17,5 @@
 package androidx.compose.ui
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalComposeUiApi
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
index d2977ca..476d2aa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/InternalComposeUiApi.kt
@@ -24,4 +24,5 @@
     "Unstable API for use only between compose-ui modules sharing the same exact version, " +
         "subject to change without notice in major, minor, or patch releases."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalComposeUiApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
index 6dea801..2c43580 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt
@@ -191,7 +191,7 @@
             level = DeprecationLevel.HIDDEN
         )
         override val isValid: Boolean
-            get() = TODO("Not yet implemented")
+            get() = isAttached
 
         internal open fun updateCoordinator(coordinator: NodeCoordinator?) {
             this.coordinator = coordinator
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
index 81c1629a..5b0c82b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/OneDimensionalFocusSearch.kt
@@ -210,8 +210,11 @@
         requireNotNull(focusModifier2)
 
         // Ignore focus modifiers that won't be considered during focus search.
-        if (!focusModifier1.isEligibleForFocusSearch) return 0
-        if (!focusModifier2.isEligibleForFocusSearch) return 0
+        if (!focusModifier1.isEligibleForFocusSearch || !focusModifier2.isEligibleForFocusSearch) {
+            if (focusModifier1.isEligibleForFocusSearch) return -1
+            if (focusModifier2.isEligibleForFocusSearch) return 1
+            return 0
+        }
 
         val layoutNode1 = checkNotNull(focusModifier1.coordinator?.layoutNode)
         val layoutNode2 = checkNotNull(focusModifier2.coordinator?.layoutNode)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt
new file mode 100644
index 0000000..2e8afd7
--- /dev/null
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/ScrollContainerInfo.kt
@@ -0,0 +1,111 @@
+/*
+ * 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.input
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.modifier.ModifierLocalConsumer
+import androidx.compose.ui.modifier.ModifierLocalProvider
+import androidx.compose.ui.modifier.ModifierLocalReadScope
+import androidx.compose.ui.modifier.ProvidableModifierLocal
+import androidx.compose.ui.modifier.modifierLocalConsumer
+import androidx.compose.ui.modifier.modifierLocalOf
+import androidx.compose.ui.platform.debugInspectorInfo
+
+/**
+ * Represents a component that handles scroll events, so that other components in the hierarchy
+ * can adjust their behaviour.
+ * @See [provideScrollContainerInfo] and [consumeScrollContainerInfo]
+ */
+interface ScrollContainerInfo {
+    /** @return whether this component handles horizontal scroll events */
+    fun canScrollHorizontally(): Boolean
+    /** @return whether this component handles vertical scroll events */
+    fun canScrollVertically(): Boolean
+}
+
+/** @return whether this container handles either horizontal or vertical scroll events */
+fun ScrollContainerInfo.canScroll() = canScrollVertically() || canScrollHorizontally()
+
+/**
+ * A modifier to query whether there are any parents in the hierarchy that handle scroll events.
+ * The [ScrollContainerInfo] provided in [consumer] will recursively look for ancestors if the
+ * nearest parent does not handle scroll events in the queried direction.
+ * This can be used to delay UI changes in cases where a pointer event may later become a scroll,
+ * cancelling any existing press or other gesture.
+ *
+ * @sample androidx.compose.ui.samples.ScrollableContainerSample
+ */
+@OptIn(ExperimentalComposeUiApi::class)
+fun Modifier.consumeScrollContainerInfo(consumer: (ScrollContainerInfo?) -> Unit): Modifier =
+    modifierLocalConsumer {
+        consumer(ModifierLocalScrollContainerInfo.current)
+    }
+
+/**
+ * A Modifier that indicates that this component handles scroll events. Use
+ * [consumeScrollContainerInfo] to query whether there is a parent in the hierarchy that is
+ * a [ScrollContainerInfo].
+ */
+fun Modifier.provideScrollContainerInfo(scrollContainerInfo: ScrollContainerInfo): Modifier =
+    composed(
+        inspectorInfo = debugInspectorInfo {
+            name = "provideScrollContainerInfo"
+            value = scrollContainerInfo
+        }) {
+    remember(scrollContainerInfo) {
+        ScrollContainerInfoModifierLocal(scrollContainerInfo)
+    }
+}
+
+/**
+ * ModifierLocal to propagate ScrollableContainer throughout the hierarchy.
+ * This Modifier will recursively check for ancestor ScrollableContainers,
+ * if the current ScrollableContainer does not handle scroll events in a particular direction.
+ */
+private class ScrollContainerInfoModifierLocal(
+    private val scrollContainerInfo: ScrollContainerInfo,
+) : ScrollContainerInfo, ModifierLocalProvider<ScrollContainerInfo?>, ModifierLocalConsumer {
+
+    private var parent: ScrollContainerInfo? by mutableStateOf(null)
+
+    override val key: ProvidableModifierLocal<ScrollContainerInfo?> =
+        ModifierLocalScrollContainerInfo
+    override val value: ScrollContainerInfoModifierLocal = this
+
+    override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) = with(scope) {
+        parent = ModifierLocalScrollContainerInfo.current
+    }
+
+    override fun canScrollHorizontally(): Boolean {
+        return scrollContainerInfo.canScrollHorizontally() ||
+            parent?.canScrollHorizontally() == true
+    }
+
+    override fun canScrollVertically(): Boolean {
+        return scrollContainerInfo.canScrollVertically() || parent?.canScrollVertically() == true
+    }
+}
+
+internal val ModifierLocalScrollContainerInfo = modifierLocalOf<ScrollContainerInfo?> {
+    null
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
index dc8372e..4edb9c4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/util/VelocityTracker.kt
@@ -37,9 +37,9 @@
  *
  * The input data is provided by calling [addPosition]. Adding data is cheap.
  *
- * To obtain a velocity, call [calculateVelocity] or [getVelocityEstimate]. This will
- * compute the velocity based on the data added so far. Only call these when
- * you need to use the velocity, as they are comparatively expensive.
+ * To obtain a velocity, call [calculateVelocity]. This will compute the velocity
+ * based on the data added so far. Only call this when you need to use the velocity,
+ * as it is comparatively expensive.
  *
  * The quality of the velocity estimation will be better if more data points
  * have been received.
@@ -52,9 +52,9 @@
     internal var currentPointerPositionAccumulator = Offset.Zero
 
     /**
-     * Adds a position as the given time to the tracker.
+     * Adds a position at the given time to the tracker.
      *
-     * Call resetTracking to remove added Offsets.
+     * Call [resetTracking] to remove added [Offset]s.
      *
      * @see resetTracking
      */
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 215dd4d..2b4b7ac 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
@@ -1,4 +1,3 @@
-
 /*
  * Copyright 2022 The Android Open Source Project
  *
@@ -302,10 +301,10 @@
 }
 
 @ExperimentalComposeUiApi
-internal fun DelegatableNode.requireLayoutNode(): LayoutNode = node.coordinator!!.layoutNode
+internal fun DelegatableNode.requireLayoutNode() = checkNotNull(node.coordinator).layoutNode
 
 @ExperimentalComposeUiApi
-internal fun DelegatableNode.requireOwner(): Owner = requireLayoutNode().owner!!
+internal fun DelegatableNode.requireOwner(): Owner = checkNotNull(requireLayoutNode().owner)
 
 /**
  * Invalidates the subtree of this layout, including layout, drawing, parent data, etc.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
index 3da8a82..57e2bfb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InternalCoreApi.kt
@@ -21,4 +21,5 @@
     AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY,
     AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalCoreApi
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
index 9d0bed1..c56b027 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeChain.kt
@@ -234,7 +234,7 @@
         headToTail {
             if (!it.isAttached) {
                 it.attach()
-                if (performInvalidations) autoInvalidateNode(it)
+                if (performInvalidations) autoInvalidateInsertedNode(it)
             }
         }
     }
@@ -432,7 +432,7 @@
             // for removing nodes, we always do the autoInvalidateNode call,
             // regardless of whether or not it was a ModifierNodeElement with autoInvalidate
             // true, or a BackwardsCompatNode, etc.
-            autoInvalidateNode(node)
+            autoInvalidateRemovedNode(node)
             node.detach()
         }
         return removeNode(node)
@@ -510,24 +510,26 @@
             check(node is BackwardsCompatNode)
             node.element = next
             // we always autoInvalidate BackwardsCompatNode
-            autoInvalidateNode(node)
+            autoInvalidateUpdatedNode(node)
             return node
         }
         val updated = next.updateUnsafe(node)
-        val result = if (updated !== node) {
+        if (updated !== node) {
             // if a new instance is returned, we want to detach the old one
+            autoInvalidateRemovedNode(node)
             node.detach()
-            replaceNode(node, updated)
+            val result = replaceNode(node, updated)
+            autoInvalidateInsertedNode(updated)
+            return result
         } else {
             // the node was updated. we are done.
-            updated
+            if (next.autoInvalidate) {
+                // the modifier element is labeled as "auto invalidate", which means that since the
+                // node was updated, we need to invalidate everything relevant to it
+                autoInvalidateUpdatedNode(updated)
+            }
+            return updated
         }
-        if (next.autoInvalidate) {
-            // the modifier element is labeled as "auto invalidate", which means that since the
-            // node was updated, we need to invalidate everything relevant to it
-            autoInvalidateNode(result)
-        }
-        return result
     }
 
     // TRAVERSAL
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
index 4f8022f..04fef20 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeKind.kt
@@ -156,10 +156,29 @@
     return mask
 }
 
+private const val Updated = 0
+private const val Inserted = 1
+private const val Removed = 2
+
 @OptIn(ExperimentalComposeUiApi::class)
-internal fun autoInvalidateNode(node: Modifier.Node) {
+internal fun autoInvalidateRemovedNode(node: Modifier.Node) = autoInvalidateNode(node, Removed)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateInsertedNode(node: Modifier.Node) = autoInvalidateNode(node, Inserted)
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateUpdatedNode(node: Modifier.Node) = autoInvalidateNode(node, Updated)
+@OptIn(ExperimentalComposeUiApi::class)
+private fun autoInvalidateNode(node: Modifier.Node, phase: Int) {
     if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
         node.invalidateMeasurements()
+        if (phase == Removed) {
+            val coordinator = node.requireCoordinator(Nodes.Layout)
+            val layer = coordinator.layer
+            if (layer != null) {
+                coordinator.onLayerBlockUpdated(null)
+            }
+        }
     }
     if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
         node.requireLayoutNode().invalidateMeasurements()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
index 5b4aa14..2450ada 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ObserverNode.kt
@@ -36,7 +36,7 @@
     @ExperimentalComposeUiApi
     companion object {
         internal val OnObserveReadsChanged: (ObserverNode) -> Unit = {
-            it.onObservedReadsChanged()
+            if (it.node.isAttached) it.onObservedReadsChanged()
         }
     }
 }
@@ -47,8 +47,8 @@
 @ExperimentalComposeUiApi
 fun <T> T.observeReads(block: () -> Unit) where T : Modifier.Node, T : ObserverNode {
     requireOwner().snapshotObserver.observeReads(
-        this,
-        ObserverNode.OnObserveReadsChanged,
-        block
+        target = this,
+        onChanged = ObserverNode.OnObserveReadsChanged,
+        block = block
     )
 }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
index 7a9486c..4774c9f 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/input/RecordingInputConnectionUpdateTextFieldValueTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.ui.input
 
-import android.view.View
 import android.view.inputmethod.ExtractedText
 import android.view.inputmethod.InputConnection
 import androidx.compose.ui.text.TextRange
@@ -25,6 +24,10 @@
 import androidx.compose.ui.text.input.RecordingInputConnection
 import androidx.compose.ui.text.input.TextFieldValue
 import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
 import org.mockito.kotlin.any
 import org.mockito.kotlin.argumentCaptor
 import org.mockito.kotlin.eq
@@ -32,10 +35,6 @@
 import org.mockito.kotlin.never
 import org.mockito.kotlin.times
 import org.mockito.kotlin.verify
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @RunWith(JUnit4::class)
 class RecordingInputConnectionUpdateTextFieldValueTest {
@@ -56,48 +55,45 @@
     @Test
     fun test_update_input_state() {
         val imm: InputMethodManager = mock()
-        val view: View = mock()
 
         val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
 
-        ic.updateInputState(inputState, imm, view)
+        ic.updateInputState(inputState, imm)
 
-        verify(imm, times(1)).updateSelection(eq(view), eq(0), eq(0), eq(-1), eq(-1))
-        verify(imm, never()).updateExtractedText(any(), any(), any())
+        verify(imm, times(1)).updateSelection(eq(0), eq(0), eq(-1), eq(-1))
+        verify(imm, never()).updateExtractedText(any(), any())
     }
 
     @Test
     fun test_update_input_state_inactive() {
         val imm: InputMethodManager = mock()
-        val view: View = mock()
 
         val previousTextFieldValue = ic.mTextFieldValue
         ic.closeConnection()
 
         val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
-        ic.updateInputState(inputState, imm, view)
+        ic.updateInputState(inputState, imm)
 
         assertThat(ic.mTextFieldValue).isEqualTo(previousTextFieldValue)
-        verify(imm, never()).updateSelection(any(), any(), any(), any(), any())
-        verify(imm, never()).updateExtractedText(any(), any(), any())
+        verify(imm, never()).updateSelection(any(), any(), any(), any())
+        verify(imm, never()).updateExtractedText(any(), any())
     }
 
     @Test
     fun test_update_input_state_extracted_text_monitor() {
         val imm: InputMethodManager = mock()
-        val view: View = mock()
 
         ic.getExtractedText(null, InputConnection.GET_EXTRACTED_TEXT_MONITOR)
 
         val inputState = TextFieldValue(text = "Hello, World.", selection = TextRange.Zero)
 
-        ic.updateInputState(inputState, imm, view)
+        ic.updateInputState(inputState, imm)
 
-        verify(imm, times(1)).updateSelection(eq(view), eq(0), eq(0), eq(-1), eq(-1))
+        verify(imm, times(1)).updateSelection(eq(0), eq(0), eq(-1), eq(-1))
 
         val captor = argumentCaptor<ExtractedText>()
 
-        verify(imm, times(1)).updateExtractedText(any(), any(), captor.capture())
+        verify(imm, times(1)).updateExtractedText(any(), captor.capture())
 
         assertThat(captor.allValues.size).isEqualTo(1)
         assertThat(captor.firstValue.text).isEqualTo("Hello, World.")
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
index 32854e2..9de574d 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/text/input/TextInputServiceAndroidCommandDebouncingTest.kt
@@ -16,12 +16,9 @@
 
 package androidx.compose.ui.text.input
 
-import android.os.IBinder
 import android.view.View
 import android.view.inputmethod.ExtractedText
 import com.google.common.truth.Truth.assertThat
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.whenever
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.cancel
@@ -32,6 +29,8 @@
 import org.junit.After
 import org.junit.Before
 import org.junit.Test
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 class TextInputServiceAndroidCommandDebouncingTest {
@@ -59,9 +58,9 @@
         service.showSoftwareKeyboard()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test
@@ -69,9 +68,9 @@
         service.hideSoftwareKeyboard()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
     }
 
     @Test
@@ -79,7 +78,7 @@
         service.startInput()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.restartCalls).hasSize(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(1)
     }
 
     @Test
@@ -87,7 +86,7 @@
         service.startInput()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -95,7 +94,7 @@
         service.stopInput()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.restartCalls).hasSize(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(1)
     }
 
     @Test
@@ -103,7 +102,7 @@
         service.stopInput()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -116,7 +115,7 @@
         // in either order, should debounce to a single restart call. If they aren't de-duped, the
         // keyboard may flicker if one of the calls configures the IME in a non-default way (e.g.
         // number input).
-        assertThat(inputMethodManager.restartCalls).hasSize(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(1)
     }
 
     @Test
@@ -129,7 +128,7 @@
         // in either order, should debounce to a single restart call. If they aren't de-duped, the
         // keyboard may flicker if one of the calls configures the IME in a non-default way (e.g.
         // number input).
-        assertThat(inputMethodManager.restartCalls).hasSize(1)
+        assertThat(inputMethodManager.restartCalls).isEqualTo(1)
     }
 
     @Test
@@ -140,7 +139,7 @@
 
         // After stopInput, there's no input connection, so any calls to show the keyboard should
         // be ignored until the next call to startInput.
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
     }
 
     @Test
@@ -152,7 +151,7 @@
         // stopInput will hide the keyboard implicitly, so both stopInput and hideSoftwareKeyboard
         // have the effect "hide the keyboard". These two effects should be debounced and the IMM
         // should only get a single hide call instead of two redundant calls.
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -162,7 +161,7 @@
         }
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -172,7 +171,7 @@
         }
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -181,8 +180,8 @@
         service.hideSoftwareKeyboard()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).hasSize(0)
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(1)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(1)
     }
 
     @Test
@@ -191,40 +190,40 @@
         service.showSoftwareKeyboard()
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).hasSize(1)
-        assertThat(inputMethodManager.hideSoftInputCalls).hasSize(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(1)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun stopInput_isNotProcessedImmediately() {
         service.stopInput()
 
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
-        assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun startInput_isNotProcessedImmediately() {
         service.startInput()
 
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
-        assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun showSoftwareKeyboard_isNotProcessedImmediately() {
         service.showSoftwareKeyboard()
 
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
-        assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun hideSoftwareKeyboard_isNotProcessedImmediately() {
         service.hideSoftwareKeyboard()
 
-        assertThat(inputMethodManager.restartCalls).isEmpty()
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
-        assertThat(inputMethodManager.hideSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.restartCalls).isEqualTo(0)
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
+        assertThat(inputMethodManager.hideSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun commandsAreIgnored_ifFocusLostBeforeProcessing() {
@@ -235,7 +234,7 @@
         // Process the queued commands.
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
     }
 
     @Test fun commandsAreDrained_whenProcessedWithoutFocus() {
@@ -246,7 +245,7 @@
         whenever(view.isFocused).thenReturn(true)
         scope.advanceUntilIdle()
 
-        assertThat(inputMethodManager.showSoftInputCalls).isEmpty()
+        assertThat(inputMethodManager.showSoftInputCalls).isEqualTo(0)
     }
 
     private fun TextInputServiceAndroid.startInput() {
@@ -259,27 +258,26 @@
     }
 
     private class TestInputMethodManager : InputMethodManager {
-        val restartCalls = mutableListOf<View>()
-        val showSoftInputCalls = mutableListOf<View>()
-        val hideSoftInputCalls = mutableListOf<IBinder?>()
+        var restartCalls = 0
+        var showSoftInputCalls = 0
+        var hideSoftInputCalls = 0
 
-        override fun restartInput(view: View) {
-            restartCalls += view
+        override fun restartInput() {
+            restartCalls++
         }
 
-        override fun showSoftInput(view: View) {
-            showSoftInputCalls += view
+        override fun showSoftInput() {
+            showSoftInputCalls++
         }
 
-        override fun hideSoftInputFromWindow(windowToken: IBinder?) {
-            hideSoftInputCalls += windowToken
+        override fun hideSoftInput() {
+            hideSoftInputCalls++
         }
 
-        override fun updateExtractedText(view: View, token: Int, extractedText: ExtractedText) {
+        override fun updateExtractedText(token: Int, extractedText: ExtractedText) {
         }
 
         override fun updateSelection(
-            view: View,
             selectionStart: Int,
             selectionEnd: Int,
             compositionStart: Int,
diff --git a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
index fd40d95..96daef8 100644
--- a/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
+++ b/constraintlayout/constraintlayout-compose/api/public_plus_experimental_current.txt
@@ -362,7 +362,7 @@
     property public final androidx.constraintlayout.compose.Easing Standard;
   }
 
-  @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") public @interface ExperimentalMotionApi {
+  @kotlin.RequiresOptIn(message="MotionLayout API is experimental and it is likely to change.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalMotionApi {
   }
 
   @androidx.compose.runtime.Immutable public final class FlowStyle {
diff --git a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
index 2e50d98..efa4195 100644
--- a/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
+++ b/constraintlayout/constraintlayout-compose/src/androidMain/kotlin/androidx/constraintlayout/compose/ExperimentalMotionApi.kt
@@ -17,4 +17,5 @@
 package androidx.constraintlayout.compose
 
 @RequiresOptIn("MotionLayout API is experimental and it is likely to change.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalMotionApi
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
index a20071d..27587e6 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintLayout.java
@@ -18,6 +18,7 @@
 
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_SPREAD;
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.MATCH_CONSTRAINT_WRAP;
 import static androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID;
@@ -2376,7 +2377,7 @@
         public static final int START = 6;
 
         /**
-         * The right side of a view in right to left languages.
+         * The right side of a view in left to right languages.
          * In right to left languages it corresponds to the left side of the view
          */
         public static final int END = 7;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
index 55c8c2d..d2c94b9 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintProperties.java
@@ -62,7 +62,7 @@
     public static final int START = ConstraintLayout.LayoutParams.START;
 
     /**
-     * The right side of a view in right to left languages.
+     * The right side of a view in left to right languages.
      * In right to left languages it corresponds to the left side of the view
      */
     public static final int END = ConstraintLayout.LayoutParams.END;
diff --git a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
index 59a0a66..63d4f6c 100644
--- a/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
+++ b/constraintlayout/constraintlayout/src/main/java/androidx/constraintlayout/widget/ConstraintSet.java
@@ -252,7 +252,7 @@
     public static final int START = ConstraintLayout.LayoutParams.START;
 
     /**
-     * The right side of a view in right to left languages.
+     * The right side of a view in left to right languages.
      * In right to left languages it corresponds to the left side of the view
      */
     public static final int END = ConstraintLayout.LayoutParams.END;
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index 4821c84..1c7216f 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -34,7 +34,7 @@
     field public static final androidx.datastore.core.DataStoreFactory INSTANCE;
   }
 
-  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface ExperimentalMultiProcessDataStore {
+  @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.WARNING, message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets=kotlin.annotation.AnnotationTarget.FUNCTION) public @interface ExperimentalMultiProcessDataStore {
   }
 
   public final class MultiProcessDataStoreFactory {
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
index 5546222..49a2d2d 100644
--- a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
@@ -21,4 +21,5 @@
     message = "This API is experimental and is likely to change in the future."
 )
 @Target(AnnotationTarget.FUNCTION)
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalMultiProcessDataStore
\ No newline at end of file
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
index 0ec2645..5b88fd9 100644
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
@@ -20,7 +20,6 @@
 import android.os.Bundle
 import androidx.datastore.core.handlers.NoOpCorruptionHandler
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.filters.FlakyTest
 import androidx.testing.TestMessageProto.FooProto
 import com.google.common.truth.Truth.assertThat
 import com.google.protobuf.ExtensionRegistryLite
@@ -43,6 +42,7 @@
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -265,7 +265,7 @@
         }
     }
 
-    @FlakyTest(bugId = 242765757)
+    @Ignore // b/242765757
     @Test
     fun testInterleavedUpdateDataWithLocalRead() = runTest(UnconfinedTestDispatcher()) {
         val testData: Bundle = createDataStoreBundle(testFile.absolutePath)
diff --git a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
index f735ace..2082a7a 100644
--- a/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
@@ -58,7 +58,6 @@
 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,7 +243,6 @@
         }
     }
 
-    @Ignore("b/244352517")
     @Test
     fun testReadFromNonExistentFile() = runTest {
         val nonExistentFile = tempFolder.newFile()
@@ -372,7 +370,7 @@
             dataStore.updateData { awaitCancellation() }
         }
 
-        dsScope.launch(Dispatchers.Unconfined) {
+        val started = dsScope.async(Dispatchers.Unconfined) {
             dataStore.updateData {
                 latch.await()
                 it.inc()
@@ -387,6 +385,11 @@
 
         assertThat(awaitingCancellation.isCancelled).isTrue()
         assertThat(notStarted.isCancelled).isTrue()
+
+        // wait for coroutine to complete to prevent it from outliving the test, which is flaky
+        latch.complete(Unit)
+        started.await()
+        assertThat(dataStore.data.first()).isEqualTo(1)
     }
 
     @Test
diff --git a/datastore/datastore/build.gradle b/datastore/datastore/build.gradle
index dd9b2bb..305a0d6 100644
--- a/datastore/datastore/build.gradle
+++ b/datastore/datastore/build.gradle
@@ -14,48 +14,72 @@
  * limitations under the License.
  */
 
-import androidx.build.Publish
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import androidx.build.KmpPlatformsKt
+import androidx.build.LibraryType
 
 plugins {
     id("AndroidXPlugin")
     id("com.android.library")
-    id("kotlin-android")
 }
 
+def enableNative = KmpPlatformsKt.enableNative(project)
+
 android {
-    sourceSets {
-        androidTest {
-            // Shared TestingSerializer between test and androidTest
-            kotlin.srcDirs += test.kotlin.srcDirs
-        }
-    }
     namespace "androidx.datastore.datastore"
 }
 
-dependencies {
-    api(libs.kotlinStdlib)
-    api(libs.kotlinCoroutinesCore)
-    api("androidx.annotation:annotation:1.2.0")
-    api(project(":datastore:datastore-core"))
-    api(project(":datastore:datastore-core-okio"))
+androidXMultiplatform {
+    android()
 
-    testImplementation(libs.junit)
-    testImplementation(libs.kotlinCoroutinesTest)
-    testImplementation(libs.truth)
-    testImplementation(project(":internal-testutils-truth"))
+    sourceSets {
+        androidMain {
+            dependencies {
+                api(libs.kotlinStdlib)
+                api(libs.kotlinCoroutinesCore)
+                api("androidx.annotation:annotation:1.2.0")
+                api(project(":datastore:datastore-core"))
+                api(project(":datastore:datastore-core-okio"))
+            }
 
-    androidTestImplementation(libs.junit)
-    androidTestImplementation(libs.kotlinCoroutinesTest)
-    androidTestImplementation(libs.truth)
-    androidTestImplementation(project(":internal-testutils-truth"))
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testCore)
+        }
+        androidAndroidTest {
+            dependencies {
+                implementation(libs.junit)
+                implementation(libs.kotlinCoroutinesTest)
+                implementation(libs.truth)
+                implementation(project(":internal-testutils-truth"))
+
+                implementation(libs.junit)
+                implementation(libs.kotlinCoroutinesTest)
+                implementation(libs.truth)
+                implementation(project(":internal-testutils-truth"))
+                implementation(libs.testRunner)
+                implementation(libs.testCore)
+            }
+
+        }
+        targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget).configureEach {
+            binaries.all {
+                binaryOptions["memoryModel"] = "experimental"
+            }
+        }
+        targets.all { target ->
+            if (target.platformType == org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.native) {
+                target.compilations["main"].defaultSourceSet {
+                    dependsOn(nativeMain)
+                }
+                target.compilations["test"].defaultSourceSet {
+                    dependsOn(nativeTest)
+                }
+            }
+        }
+    }
 }
 
 androidx {
     name = "Android DataStore"
-    publish = Publish.SNAPSHOT_AND_RELEASE
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.DATASTORE
     inceptionYear = "2020"
     description = "Android DataStore - contains the underlying store used by each serialization " +
diff --git a/datastore/datastore/src/androidTest/AndroidManifest.xml b/datastore/datastore/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore/src/androidTest/AndroidManifest.xml
rename to datastore/datastore/src/androidAndroidTest/AndroidManifest.xml
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreDelegateTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreDelegateTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreDelegateTest.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreFileTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/DataStoreFileTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/DataStoreFileTest.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/TestingSerializer.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/TestingSerializer.kt
diff --git a/datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt b/datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
similarity index 100%
rename from datastore/datastore/src/androidTest/java/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
rename to datastore/datastore/src/androidAndroidTest/kotlin/androidx/datastore/migrations/SharedPreferencesMigrationTest.kt
diff --git a/datastore/datastore/src/main/AndroidManifest.xml b/datastore/datastore/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from datastore/datastore/src/main/AndroidManifest.xml
rename to datastore/datastore/src/androidMain/AndroidManifest.xml
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreDelegate.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/DataStoreDelegate.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreDelegate.kt
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFile.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreFile.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/DataStoreFile.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/DataStoreFile.kt
diff --git a/datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt b/datastore/datastore/src/androidMain/kotlin/androidx/datastore/migrations/SharedPreferencesMigration.kt
similarity index 100%
rename from datastore/datastore/src/main/java/androidx/datastore/migrations/SharedPreferencesMigration.kt
rename to datastore/datastore/src/androidMain/kotlin/androidx/datastore/migrations/SharedPreferencesMigration.kt
diff --git a/development/auto-version-updater/test_update_versions_for_release.py b/development/auto-version-updater/test_update_versions_for_release.py
index fc27d84..e6e9a9f 100755
--- a/development/auto-version-updater/test_update_versions_for_release.py
+++ b/development/auto-version-updater/test_update_versions_for_release.py
@@ -20,6 +20,11 @@
 from update_versions_for_release import *
 from shutil import rmtree
 
+# Import functions from the parent directory
+sys.path.append("..")
+from update_tracing_perfetto import single
+from update_tracing_perfetto import sed
+
 class TestVersionUpdates(unittest.TestCase):
 
     def test_increment_version(self):
diff --git a/development/auto-version-updater/update_tracing_perfetto.sh b/development/auto-version-updater/update_tracing_perfetto.sh
deleted file mode 100755
index 349dd4b..0000000
--- a/development/auto-version-updater/update_tracing_perfetto.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-
-#
-# Usage:
-# ./update_perfetto.sh ANDROIDX_CHECKOUT CURRENT_VERSION NEW_VERSION
-#
-# Example:
-# ./update_perfetto.sh /Volumes/android/androidx-main/frameworks/support 1.0.0-alpha04 1.0.0-alpha05
-#
-
-set -euo pipefail
-
-ANDROIDX_CHECKOUT="$(cd "$1"; pwd -P .)" # gets absolute path of root dir
-CURRENT_VERSION="$2"
-NEW_VERSION="$3"
-
-/usr/bin/env python3 <<EOF
-from update_versions_for_release import update_tracing_perfetto
-update_tracing_perfetto('$CURRENT_VERSION', '$NEW_VERSION', "$ANDROIDX_CHECKOUT")
-EOF
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
index 98c9091..0cd1cd8 100755
--- a/development/auto-version-updater/update_versions_for_release.py
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -21,12 +21,14 @@
 import glob
 import pathlib
 import re
+import shutil
 import subprocess
 import toml
 
 # Import the JetpadClient from the parent directory
 sys.path.append("..")
 from JetpadClient import *
+from update_tracing_perfetto import update_tracing_perfetto
 
 # cd into directory of script
 os.chdir(os.path.dirname(os.path.abspath(__file__)))
@@ -79,42 +81,6 @@
         print("Please respond with y/n")
 
 
-def sed(pattern, replacement, file):
-    """ Performs an in-place string replacement of pattern in a target file
-
-    Args:
-        pattern: pattern to replace
-        replacement: replacement for the pattern matches
-        file: target file
-
-    Returns:
-        Nothing
-    """
-
-    with open(file) as f:
-        file_contents = f.read()
-    new_file_contents = re.sub(pattern, replacement, file_contents)
-    with open(file, "w") as f:
-        f.write(new_file_contents)
-
-
-def single(list):
-    """ Returns the only item from a list of just one item
-
-    Raises a ValueError if the list does not contain exactly one element
-
-    Args:
-        list: a list of one item
-
-    Returns:
-        The only item from a single-item-list
-    """
-
-    if len(list) != 1:
-        raise ValueError('Expected a list of size 1. Found: %s' % list)
-    return list[0]
-
-
 def run_update_api():
     """Runs updateApi ignoreApiChanges from the frameworks/support root.
     """
@@ -495,81 +461,6 @@
     return
 
 
-def update_tracing_perfetto(old_version, new_version=None, core_path=FRAMEWORKS_SUPPORT_FP):
-    """Updates tracing-perfetto version and artifacts (including building new binaries)
-
-    Args:
-        old_version: old version of the existing library
-        new_version: new version of the library; defaults to incrementing the old_version
-        core_path: path to frameworks/support directory
-    Returns:
-        Nothing
-    """
-
-    print("Updating tracing-perfetto, this can take a while...")
-
-    # update version in code
-    if not new_version:
-        new_version = increment_version(old_version)
-
-    sed('tracingPerfettoVersion = "%s"' % old_version,
-        'tracingPerfettoVersion = "%s"' % new_version,
-        os.path.join(core_path, 'benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/'
-                                'macro/perfetto/PerfettoSdkHandshakeTest.kt'))
-    sed('TRACING_PERFETTO = "%s"' % old_version,
-        'TRACING_PERFETTO = "%s"' % new_version,
-        os.path.join(core_path, 'libraryversions.toml'))
-    sed('#define VERSION "%s"' % old_version,
-        '#define VERSION "%s"' % new_version,
-        os.path.join(core_path, 'tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc'))
-    sed('const val libraryVersion = "%s"' % old_version,
-        'const val libraryVersion = "%s"' % new_version,
-        os.path.join(core_path, 'tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/'
-                                'perfetto/jni/test/PerfettoNativeTest.kt'))
-    sed('const val version = "%s"' % old_version,
-        'const val version = "%s"' % new_version,
-        os.path.join(core_path, 'tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/'
-                                'jni/PerfettoNative.kt'))
-
-    # build new binaries
-    subprocess.check_call(["./gradlew",
-                           ":tracing:tracing-perfetto-binary:createProjectZip",
-                           "-DTRACING_PERFETTO_REUSE_PREBUILTS_AAR=false"],
-                          cwd=core_path)
-
-    # copy binaries to prebuilts
-    project_zip_dir = os.path.join(core_path, '../../out/dist/per-project-zips')
-    project_zip_file = os.path.join(
-        project_zip_dir,
-        single(glob.glob('%s/*tracing*perfetto*binary*%s*.zip' % (project_zip_dir, new_version))))
-    dst_dir = pathlib.Path(os.path.join(
-        core_path,
-        "../../prebuilts/androidx/internal/androidx/tracing/tracing-perfetto-binary",
-        new_version))
-    if dst_dir.exists():
-        shutil.rmtree(dst_dir)
-    dst_dir.mkdir()
-    subprocess.check_call(
-        ["unzip", "-xjqq", project_zip_file, '**/%s/**' % new_version, "-d", dst_dir])
-
-    # update SHA
-    for arch in ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']:
-        checksum = subprocess.check_output(
-            'unzip -cxqq "*tracing*binary*%s*.aar" "**/%s/libtracing_perfetto.so" | shasum -a256 |'
-            ' awk \'{print $1}\' | tr -d "\n"' % (new_version, arch),
-            cwd=dst_dir,
-            shell=True
-        ).decode()
-        if not re.fullmatch('^[0-9a-z]{64}$', checksum):
-            raise ValueError('Expecting a sha256 sum. Got: %s' % checksum)
-        sed(
-            '"%s" to "[0-9a-z]{64}"' % arch,
-            '"%s" to "%s"' % (arch, checksum),
-            os.path.join(core_path, 'tracing/tracing-perfetto/src/main/java/androidx/tracing/'
-                                    'perfetto/jni/PerfettoNative.kt'))
-
-    print("Updated tracing-perfetto.")
-
 def commit_updates(release_date):
     for dir in [FRAMEWORKS_SUPPORT_FP, PREBUILTS_ANDROIDX_INTERNAL_FP]:
         subprocess.check_call(["git", "add", "."], cwd=dir, stderr=subprocess.STDOUT)
@@ -611,7 +502,9 @@
                 if tracing_perfetto_updated:
                     updated = True
                 else:
-                    update_tracing_perfetto(artifact["version"])
+                    current_version = artifact["version"]
+                    target_version = increment_version(current_version)
+                    update_tracing_perfetto(current_version, target_version, FRAMEWORKS_SUPPORT_FP)
                     tracing_perfetto_updated = True
 
             if not updated:
diff --git a/development/referenceDocs/switcher.py b/development/referenceDocs/switcher.py
index 0bb494d..63195ae 100755
--- a/development/referenceDocs/switcher.py
+++ b/development/referenceDocs/switcher.py
@@ -79,10 +79,9 @@
     if (java):
       file_path = doc[len(java_ref_root)+1:]
       stub = doc.replace(java_source_abs_path, kotlin_source_abs_path)
-      if (both):
-        slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% setvar page_path %}_page_path_{% endsetvar %}\\n{% setvar can_switch %}1{% endsetvar %}\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
-      else:
-        slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
+      # Always add the switcher for java files, switch to the package summary if
+      # the page itself doesn't exist in kotlin
+      slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% setvar page_path %}_page_path_{% endsetvar %}\\n{% setvar can_switch %}1{% endsetvar %}\\n{% include \"reference\/_java_switcher2.md\" %}",doc)
     else:
       file_path = doc[len(kotlin_ref_root)+1:]
       stub = doc.replace(kotlin_source_abs_path, java_source_abs_path)
@@ -92,8 +91,13 @@
         slug1 = "sed -i 's/<\/h1>/{}/' {}".format("<\/h1>\\n{% include \"reference\/_kotlin_switcher2.md\" %}",doc)
 
     os.system(slug1)
-    if (both):
-      page_path_slug = "sed -i 's/_page_path_/{}/' {}".format(file_path.replace("/","\/"),doc)
+    if both or java:
+      if both:
+        page_path = file_path
+      else:
+        page_path = os.path.join(os.path.dirname(file_path), "package-summary.html")
+
+      page_path_slug = "sed -i 's/_page_path_/{}/' {}".format(page_path.replace("/","\/"),doc)
       os.system(page_path_slug)
 
 
diff --git a/development/update_studio.sh b/development/update_studio.sh
index 292d6fbb..187a9d4 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -1,7 +1,7 @@
 #!/bin/bash
 # Get versions
-AGP_VERSION=${1:-8.0.0-alpha05}
-STUDIO_VERSION_STRING=${2:-"Android Studio Flamingo (2022.2.1) Canary 5"}
+AGP_VERSION=${1:-8.0.0-alpha07}
+STUDIO_VERSION_STRING=${2:-"Android Studio Flamingo (2022.2.1) Canary 7"}
 STUDIO_IFRAME_LINK=`curl "https://developer.android.com/studio/archive.html" | grep iframe | sed "s/.*src=\"\([a-zA-Z0-9\/\._]*\)\".*/https:\/\/android-dot-devsite-v2-prod.appspot.com\1/g"`
 STUDIO_LINK=`curl -s $STUDIO_IFRAME_LINK | grep -C30 "$STUDIO_VERSION_STRING" | grep Linux | tail -n 1 | sed 's/.*a href="\(.*\).*"/\1/g'`
 STUDIO_VERSION=`echo $STUDIO_LINK | sed "s/.*ide-zips\/\(.*\)\/android-studio-.*/\1/g"`
@@ -20,6 +20,7 @@
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint:$LINT_VERSION,"
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint-tests:$LINT_VERSION,"
 ARTIFACTS_TO_DOWNLOAD+="com.android.tools.lint:lint-gradle:$LINT_VERSION,"
+ARTIFACTS_TO_DOWNLOAD+="com.android.tools:ninepatch:$LINT_VERSION,"
 
 # Update studio_versions.properties
 sed -i "s/androidGradlePlugin = .*/androidGradlePlugin = \"$AGP_VERSION\"/g" gradle/libs.versions.toml
diff --git a/development/update_tracing_perfetto.py b/development/update_tracing_perfetto.py
new file mode 100755
index 0000000..333ca7b
--- /dev/null
+++ b/development/update_tracing_perfetto.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python3
+
+# 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 os
+import glob
+import argparse
+import pathlib
+import shutil
+import subprocess
+import re
+
+
+def sed(pattern, replacement, file):
+    """ Performs an in-place string replacement of pattern in a target file
+
+    Args:
+        pattern: pattern to replace
+        replacement: replacement for the pattern matches
+        file: target file
+
+    Returns:
+        Nothing
+    """
+
+    with open(file) as reader:
+        file_contents = reader.read()
+    new_file_contents = re.sub(pattern, replacement, file_contents)
+    with open(file, "w") as writer:
+        writer.write(new_file_contents)
+
+
+def single(items):
+    """ Returns the only item from a list of just one item
+
+    Raises a ValueError if the list does not contain exactly one element
+
+    Args:
+        items: a list of one item
+
+    Returns:
+        The only item from a single-item-list
+    """
+
+    if len(items) != 1:
+        raise ValueError('Expected a list of size 1. Found: %s' % items)
+    return items[0]
+
+
+def update_tracing_perfetto(old_version, new_version, core_path, force_unstripped_binaries=False):
+    """Updates tracing-perfetto version and artifacts (including building new binaries)
+
+    Args:
+        old_version: old version of the existing library
+        new_version: new version of the library; defaults to incrementing the old_version
+        core_path: path to frameworks/support directory
+        force_unstripped_binaries: flag allowing to force unstripped variant of binaries
+    Returns:
+        Nothing
+    """
+
+    print("Updating tracing-perfetto, this can take a while...")
+
+    # update version in code
+    sed('tracingPerfettoVersion = "%s"' % old_version,
+        'tracingPerfettoVersion = "%s"' % new_version,
+        os.path.join(core_path, 'benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/'
+                                'macro/perfetto/PerfettoSdkHandshakeTest.kt'))
+    sed('TRACING_PERFETTO = "%s"' % old_version,
+        'TRACING_PERFETTO = "%s"' % new_version,
+        os.path.join(core_path, 'libraryversions.toml'))
+    sed('#define VERSION "%s"' % old_version,
+        '#define VERSION "%s"' % new_version,
+        os.path.join(core_path, 'tracing/tracing-perfetto-binary/src/main/cpp/tracing_perfetto.cc'))
+    sed('const val libraryVersion = "%s"' % old_version,
+        'const val libraryVersion = "%s"' % new_version,
+        os.path.join(core_path, 'tracing/tracing-perfetto/src/androidTest/java/androidx/tracing/'
+                                'perfetto/jni/test/PerfettoNativeTest.kt'))
+    sed('const val version = "%s"' % old_version,
+        'const val version = "%s"' % new_version,
+        os.path.join(core_path, 'tracing/tracing-perfetto/src/main/java/androidx/tracing/perfetto/'
+                                'jni/PerfettoNative.kt'))
+
+    # build new binaries
+    subprocess.check_call(["./gradlew",
+                           ":tracing:tracing-perfetto-binary:createProjectZip",
+                           "-DTRACING_PERFETTO_REUSE_PREBUILTS_AAR=false"],
+                          cwd=core_path)
+
+    # copy binaries to prebuilts
+    project_zip_dir = os.path.join(core_path, '../../out/dist/per-project-zips')
+    project_zip_file = os.path.join(
+        project_zip_dir,
+        single(glob.glob('%s/*tracing*perfetto*binary*%s*.zip' % (project_zip_dir, new_version))))
+    dst_dir = pathlib.Path(os.path.join(
+        core_path,
+        "../../prebuilts/androidx/internal/androidx/tracing/tracing-perfetto-binary",
+        new_version))
+    if dst_dir.exists():
+        shutil.rmtree(dst_dir)
+    dst_dir.mkdir()
+    subprocess.check_call(
+        ["unzip", "-xjqq", project_zip_file, '**/%s/**' % new_version, "-d", dst_dir])
+
+    # force unstripped binaries if the flag is enabled
+    if force_unstripped_binaries:
+        # locate unstripped binaries
+        out_dir = pathlib.Path(core_path, "../../out")
+        arm64_lib_file = out_dir.joinpath(single(subprocess.check_output(
+            'find . -type f -name "libtracing_perfetto.so"'
+            ' -and -path "*RelWithDebInfo/*/obj/arm64*"'
+            ' -exec stat -c "%Y %n" {} \\; |'
+            ' sort | tail -1 | cut -d " " -f2-',
+            cwd=out_dir,
+            shell=True).splitlines()).decode())
+        base_dir = arm64_lib_file.parent.parent.parent
+        obj_dir = base_dir.joinpath('obj')
+        if not obj_dir.exists():
+            raise RuntimeError('Expected path %s to exist' % repr(obj_dir))
+        jni_dir = base_dir.joinpath('jni')
+
+        # prepare a jni folder to inject into the destination aar
+        if jni_dir.exists():
+            shutil.rmtree(jni_dir)
+        shutil.copytree(obj_dir, jni_dir)
+
+        # inject the jni folder into the aar
+        dst_aar = os.path.join(dst_dir, 'tracing-perfetto-binary-%s.aar' % new_version)
+        subprocess.check_call(['zip', '-r', dst_aar, 'jni'], cwd=base_dir)
+
+        # clean up
+        if jni_dir.exists():
+            shutil.rmtree(jni_dir)
+
+    # update SHA
+    for arch in ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64']:
+        checksum = subprocess.check_output(
+            'unzip -cxqq "*tracing*binary*%s*.aar" "**/%s/libtracing_perfetto.so" | shasum -a256 |'
+            ' awk \'{print $1}\' | tr -d "\n"' % (new_version, arch),
+            cwd=dst_dir,
+            shell=True
+        ).decode()
+        if not re.fullmatch('^[0-9a-z]{64}$', checksum):
+            raise ValueError('Expecting a sha256 sum. Got: %s' % checksum)
+        sed(
+            '"%s" to "[0-9a-z]{64}"' % arch,
+            '"%s" to "%s"' % (arch, checksum),
+            os.path.join(core_path, 'tracing/tracing-perfetto/src/main/java/androidx/tracing/'
+                                    'perfetto/jni/PerfettoNative.kt'))
+
+    print("Updated tracing-perfetto.")
+
+
+if __name__ == '__main__':
+    parser = argparse.ArgumentParser(
+        description='Updates tracing-perfetto in the source code, which involves:'
+                    ' 1) updating hardcoded version references in code'
+                    ' 2) building binaries and updating them in the prebuilts folder'
+                    ' 3) updating SHA checksums hardcoded in code.')
+    parser.add_argument('-f', '--frameworks-support-dir',
+                        required=True,
+                        help='Path to frameworks/support directory')
+    parser.add_argument('-c', '--current-version',
+                        required=True,
+                        help='Current version, e.g. 1.0.0-alpha07')
+    parser.add_argument('-t', '--target-version',
+                        required=True,
+                        help='Target version, e.g. 1.0.0-alpha08')
+    parser.add_argument('-k', '--keep-binary-debug-symbols',
+                        required=False,
+                        default=False,
+                        action='store_true',
+                        help='Keeps debug symbols in the built binaries. Useful when profiling '
+                             'performance of the library. ')
+    args = parser.parse_args()
+    core_path_abs = pathlib.Path(args.frameworks_support_dir).resolve()
+    update_tracing_perfetto(args.current_version, args.target_version, core_path_abs,
+                            args.keep_binary_debug_symbols)
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 0ad099d..5df412d 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -29,10 +29,10 @@
     docs("androidx.asynclayoutinflater:asynclayoutinflater:1.1.0-alpha01")
     docs("androidx.asynclayoutinflater:asynclayoutinflater-appcompat:1.1.0-alpha01")
     docs("androidx.autofill:autofill:1.2.0-beta01")
-    docs("androidx.benchmark:benchmark-common:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha06")
-    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha06")
+    docs("androidx.benchmark:benchmark-common:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-junit4:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-macro:1.2.0-alpha07")
+    docs("androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha07")
     docs("androidx.biometric:biometric:1.2.0-alpha05")
     docs("androidx.biometric:biometric-ktx:1.2.0-alpha05")
     samples("androidx.biometric:biometric-ktx-samples:1.2.0-alpha05")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 90f7a93..5d5e448 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -18,6 +18,7 @@
     docs(project(":ads:ads-identifier-testing"))
     docs(project(":annotation:annotation"))
     docs(project(":annotation:annotation-experimental"))
+    docs(project(":appactions:interaction:interaction-proto"))
     docs(project(":appcompat:appcompat"))
     docs(project(":appcompat:appcompat-resources"))
     docs(project(":appsearch:appsearch"))
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
index f9e16bc..5f967ca 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/BundledEmojiListLoaderTest.kt
@@ -57,7 +57,7 @@
         BundledEmojiListLoader.load(context)
 
         val cacheFileName = fileCache.emojiPickerCacheDir.listFiles()!![0].listFiles()!![0].name
-        val emptyDefaultValue = listOf<BundledEmojiListLoader.EmojiData>()
+        val emptyDefaultValue = listOf<EmojiViewItem>()
         // Read from cache instead of using default value
         var output = fileCache.getOrPut(cacheFileName) { emptyDefaultValue }
         assertTrue(output.isNotEmpty())
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
index 4f4d9c1..3a4e7ed 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/BundledEmojiListLoader.kt
@@ -85,14 +85,14 @@
     private fun loadSingleCategory(
         context: Context,
         resId: Int,
-    ): List<EmojiData> =
+    ): List<EmojiViewItem> =
         context.resources
             .openRawResource(resId)
             .bufferedReader()
             .useLines { it.toList() }
             .map { filterRenderableEmojis(it.split(",")) }
             .filter { it.isNotEmpty() }
-            .map { EmojiData(it.first(), it.drop(1)) }
+            .map { EmojiViewItem(it.first(), it.drop(1)) }
 
     private fun getCacheFileName(categoryIndex: Int) =
         StringBuilder().append("emoji.v1.")
@@ -112,10 +112,8 @@
             UnicodeRenderableManager.isEmojiRenderable(it)
         }.toList()
 
-    internal data class EmojiData(val primary: String, val variants: List<String>)
-
     internal data class EmojiDataCategory(
         val categoryName: String,
-        val emojiDataList: List<EmojiData>
+        val emojiDataList: List<EmojiViewItem>
     )
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.kt
new file mode 100644
index 0000000..2db67d7
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/CategorySeparatorViewData.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.emoji2.emojipicker
+
+/**
+ * Separator for each category.
+ *
+ *
+ * CategorySeparatorViewData: A 0-width `Space` at the beginning of each category. The
+ * `Space` works an anchor (prevention from unexpected scrolling) when the contents of the
+ * RecyclerView is updated.
+ */
+internal class CategorySeparatorViewData
+/**
+ * Instantiates a CategorySeparatorViewData.
+ *
+ * @param categoryIndex Used to compute the id.
+ * @param idInCategory Used to compute the id.
+ * @param categoryName The category name showing in the text view, e.g. "CUSTOM EMOJIS". If empty,
+ * will look up the corresponding category name based on `categoryIndex`
+ * in [EmojiPickerBodyAdapter.onBindViewHolder]
+ */(
+    categoryIndex: Int,
+    idInCategory: Int,
+    /** The name of this category.  */
+    val categoryName: String
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, /* idInCategory= */idInCategory)) {
+
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE = CategorySeparatorViewData::class.java.name.hashCode()
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.kt
new file mode 100644
index 0000000..1218fef
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/DummyViewData.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.emoji2.emojipicker
+
+/**
+ * Placeholder entry, filled at the end of each category if there are room to be filled.
+ *
+ * This behaves like NaN. Nothing is equal to Placeholder entry.
+ */
+internal class DummyViewData(id: Long) : ItemViewData(id) {
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE: Int = DummyViewData::class.java.name.hashCode()
+        val INSTANCE = DummyViewData(TYPE.toLong())
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 43c70e4..9679844 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -17,13 +17,16 @@
 package androidx.emoji2.emojipicker
 
 import android.content.Context
+import android.util.Log
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewGroup.LayoutParams
+import androidx.annotation.IntRange
 import androidx.annotation.UiThread
+import androidx.annotation.VisibleForTesting
 import androidx.appcompat.widget.AppCompatTextView
-import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.Adapter
 import androidx.recyclerview.widget.RecyclerView.ViewHolder
 import androidx.tracing.Trace
 
@@ -31,37 +34,150 @@
 internal class EmojiPickerBodyAdapter(
     context: Context,
     private val emojiGridColumns: Int,
-    private val emojiGridRows: Float
-) : RecyclerView.Adapter<ViewHolder>() {
+    private val emojiGridRows: Float,
+    private val categoryNames: Array<String>
+) : Adapter<ViewHolder>() {
     private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
     private val context = context
 
+    private var flattenSource: ItemViewDataFlatList
+
+    init {
+        val categorizedEmojis: MutableList<MutableList<ItemViewData>> = mutableListOf()
+        for (i in categoryNames.indices) {
+            categorizedEmojis.add(mutableListOf())
+        }
+        flattenSource = ItemViewDataFlatList(
+            categorizedEmojis,
+            emojiGridColumns
+        )
+    }
+
     @UiThread
     override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
         Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
-        try {
-            // TODO: Load real emoji data in the next change
-            val view: View = layoutInflater.inflate(
-                R.layout.emoji_picker_empty_category_text_view, parent,
-                /*attachToRoot= */ false
-            )
-            view.layoutParams = LayoutParams(
-                parent.width / emojiGridColumns, (parent.measuredHeight / emojiGridRows).toInt()
-            )
-            view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
-            return object : ViewHolder(view) {}
+        return try {
+            val view: View
+            if (viewType == CategorySeparatorViewData.TYPE) {
+                view = layoutInflater.inflate(
+                    R.layout.category_text_view,
+                    parent,
+                    /* attachToRoot= */ false
+                )
+                view.layoutParams =
+                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+            } else if (viewType == EmptyCategoryViewData.TYPE) {
+                view = layoutInflater.inflate(
+                    R.layout.emoji_picker_empty_category_text_view,
+                    parent,
+                    /* attachToRoot= */ false
+                )
+                view.layoutParams =
+                    LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
+                view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
+            } else if (viewType == EmojiViewData.TYPE) {
+                return EmojiViewHolder(
+                    parent,
+                    layoutInflater,
+                    getParentWidth(parent) / emojiGridColumns,
+                    (parent.measuredHeight / emojiGridRows).toInt(),
+                )
+            } else if (viewType == DummyViewData.TYPE) {
+                view = View(context)
+                view.layoutParams = LayoutParams(
+                    getParentWidth(parent) / emojiGridColumns,
+                    (parent.measuredHeight / emojiGridRows).toInt()
+                )
+            } else {
+                Log.e(
+                    "EmojiPickerBodyAdapter",
+                    "EmojiPickerBodyAdapter gets unsupported view type."
+                )
+                view = View(context)
+                view.layoutParams =
+                    LayoutParams(
+                        getParentWidth(parent) / emojiGridColumns,
+                        (parent.measuredHeight / emojiGridRows).toInt()
+                    )
+            }
+            object : ViewHolder(view) {}
         } finally {
             Trace.endSection()
         }
     }
 
     override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
-        val emptyCategoryView: AppCompatTextView =
-            viewHolder.itemView.findViewById(R.id.emoji_picker_empty_category_view)
-        emptyCategoryView.setText(R.string.emoji_empty_non_recent_category)
+        val viewType = viewHolder.itemViewType
+        val view = viewHolder.itemView
+        if (viewType == CategorySeparatorViewData.TYPE) {
+            val categoryIndex = flattenSource.getCategoryIndex(position)
+            val item = flattenSource[position] as CategorySeparatorViewData
+            var categoryName = item.categoryName
+            if (categoryName.isEmpty()) {
+                categoryName = categoryNames[categoryIndex]
+            }
+            // Show category label.
+            val categoryLabel = view.findViewById<AppCompatTextView>(R.id.category_name)
+            if (categoryName.isEmpty()) {
+                categoryLabel.visibility = View.GONE
+            } else {
+                categoryLabel.text = categoryName
+                categoryLabel.visibility = View.VISIBLE
+            }
+        } else if (viewType == EmptyCategoryViewData.TYPE) {
+            // Show empty category description.
+            val emptyCategoryView =
+                view.findViewById<AppCompatTextView>(R.id.emoji_picker_empty_category_view)
+            val item = flattenSource[position] as EmptyCategoryViewData
+            var content = item.description
+            if (content.isEmpty()) {
+                val categoryIndex: Int = getCategoryIndex(position)
+                content = context.getString(
+                    if (categoryIndex == EmojiPickerConstants.RECENT_CATEGORY_INDEX)
+                        R.string.emoji_empty_recent_category
+                    else R.string.emoji_empty_non_recent_category
+                )
+            }
+            emptyCategoryView.text = content
+        } else if (viewType == EmojiViewData.TYPE) {
+            val item = flattenSource[position] as EmojiViewData
+            val emojiViewHolder = viewHolder as EmojiViewHolder
+            emojiViewHolder.bindEmoji(
+                EmojiViewItem(
+                    item.primary,
+                    item.secondaries.toList()
+                )
+            )
+        }
     }
 
     override fun getItemCount(): Int {
-        return (emojiGridColumns * emojiGridRows).toInt()
+        return flattenSource.size
+    }
+
+    override fun getItemViewType(position: Int): Int {
+        return flattenSource[position].type
+    }
+
+    @IntRange(from = 0)
+    fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
+        return getFlattenSource().getCategoryIndex(position)
+    }
+
+    @VisibleForTesting
+    fun getFlattenSource(): ItemViewDataFlatList {
+        return flattenSource
+    }
+
+    private fun getParentWidth(parent: ViewGroup): Int {
+        return parent.measuredWidth - parent.paddingLeft - parent.paddingRight
+    }
+
+    fun updateEmojis(emojis: List<List<ItemViewData>>) {
+        flattenSource = ItemViewDataFlatList(
+            emojis,
+            emojiGridColumns
+        )
+        notifyDataSetChanged()
     }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.kt
new file mode 100644
index 0000000..ffe1390
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyView.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.emoji2.emojipicker
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+
+/** Body view contains all emojis.  */
+internal class EmojiPickerBodyView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) : RecyclerView(context, attrs) {
+
+    init {
+        val layoutManager = GridLayoutManager(
+            getContext(),
+            EmojiPickerConstants.DEFAULT_BODY_COLUMNS,
+            LinearLayoutManager.VERTICAL,
+            /* reverseLayout = */ false
+        )
+        layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
+            override fun getSpanSize(position: Int): Int {
+                val adapter = adapter ?: return 1
+                val viewType = adapter.getItemViewType(position)
+                // The following viewTypes occupy entire row.
+                return if (
+                    viewType == CategorySeparatorViewData.TYPE ||
+                    viewType == EmptyCategoryViewData.TYPE
+                ) EmojiPickerConstants.DEFAULT_BODY_COLUMNS else 1
+            }
+        }
+        setLayoutManager(layoutManager)
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 7108b68..e579631 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -24,4 +24,10 @@
 
     // The default number of body rows.
     const val DEFAULT_BODY_ROWS = 7.5f
+
+    // The default minimal number of each body row.
+    const val MIN_ROWS_PER_CATEGORY = 1
+
+    // The default recent category index number.
+    const val RECENT_CATEGORY_INDEX = 0
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index baaac54..178c0cb 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -18,15 +18,15 @@
 
 import android.content.Context
 import android.content.res.TypedArray
+import android.os.Trace
 import android.util.AttributeSet
 import android.widget.FrameLayout
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
-import androidx.recyclerview.widget.GridLayoutManager
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
 
 /**
  * The emoji picker view that provides up-to-date emojis in a vertical scrollable view with a
@@ -84,24 +84,75 @@
         }
     }
 
-    private suspend fun showEmojiPickerView(context: Context) {
-        BundledEmojiListLoader.getCategorizedEmojiData()
+    private fun getEmojiPickerBodyAdapter(
+        context: Context,
+        emojiGridColumns: Int,
+        emojiGridRows: Float,
+        categorizedEmojiData: List<BundledEmojiListLoader.EmojiDataCategory>
+    ): EmojiPickerBodyAdapter {
+        val categoryNames = mutableListOf<String>()
+        val categorizedEmojis = mutableListOf<MutableList<EmojiViewItem>>()
+        for (i in categorizedEmojiData.indices) {
+            categoryNames.add(categorizedEmojiData[i].categoryName)
+            categorizedEmojis.add(
+                categorizedEmojiData[i].emojiDataList.toMutableList()
+            )
+        }
+        val adapter = EmojiPickerBodyAdapter(
+            context,
+            emojiGridColumns,
+            emojiGridRows,
+            categoryNames.toTypedArray()
+        )
+        adapter.updateEmojis(createEmojiViewData(categorizedEmojis))
 
+        return adapter
+    }
+
+    private fun createEmojiViewData(categorizedEmojis: MutableList<MutableList<EmojiViewItem>>):
+        List<List<ItemViewData>> {
+        Trace.beginSection("createEmojiViewData")
+        return try {
+            val listBuilder = mutableListOf<List<ItemViewData>>()
+            for ((categoryIndex, sameType) in categorizedEmojis.withIndex()) {
+                val builder = mutableListOf<ItemViewData>()
+                for ((idInCategory, eachEmoji) in sameType.withIndex()) {
+                    builder.add(
+                        EmojiViewData(
+                            categoryIndex,
+                            idInCategory,
+                            eachEmoji.primary,
+                            eachEmoji.variants.toTypedArray()
+                        )
+                    )
+                }
+                listBuilder.add(builder.toList())
+            }
+            listBuilder.toList()
+        } finally {
+            Trace.endSection()
+        }
+    }
+
+    private suspend fun showEmojiPickerView(context: Context) {
         // get emoji picker
         val emojiPicker = inflate(context, R.layout.emoji_picker, this)
 
         // set headerView
         headerView = emojiPicker.findViewById(R.id.emoji_picker_header)
         headerView.layoutManager =
-            LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, /* reverseLayout= */false)
+            LinearLayoutManager(
+                context,
+                LinearLayoutManager.HORIZONTAL,
+                /* reverseLayout= */ false
+            )
         headerView.adapter = EmojiPickerHeaderAdapter(context)
 
         // set bodyView
         bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
-        bodyView.layoutManager = GridLayoutManager(
-            context, emojiGridColumns, LinearLayoutManager.VERTICAL, /* reverseLayout= */
-            false
+        val categorizedEmojiData = BundledEmojiListLoader.getCategorizedEmojiData()
+        bodyView.adapter = getEmojiPickerBodyAdapter(
+            context, emojiGridColumns, emojiGridRows, categorizedEmojiData
         )
-        bodyView.adapter = EmojiPickerBodyAdapter(context, emojiGridColumns, emojiGridRows)
     }
 }
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
new file mode 100644
index 0000000..aace300
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewData.kt
@@ -0,0 +1,60 @@
+/*
+ * 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
+
+/** Concrete entry which contains emoji view data.  */
+internal class EmojiViewData(
+    categoryIndex: Int,
+    idInCategory: Int,
+    primary: String,
+    secondaries: Array<String>
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
+    /** The index of category where the emoji view located in.  */
+    private val categoryIndex: Int
+
+    /** The id of this emoji view in the category, usually is the position of the emoji.  */
+    private val idInCategory: Int
+
+    /** Primary key which is used for labeling and for PRESS action.  */
+    val primary: String
+
+    /** Secondary keys which are used for LONG_PRESS action.  */
+    val secondaries: Array<String>
+
+    /**
+     * Instantiates a EmojiViewData.
+     *
+     * @param categoryIndex Used to compute the id.
+     * @param idInCategory Used to compute the id.
+     * @param primary The default base variant of the given emoji (no skin tone or gender modifier)
+     * @param secondaries Array of variants associated to primary
+     */
+    init {
+        this.categoryIndex = categoryIndex
+        this.idInCategory = idInCategory
+        this.primary = primary
+        this.secondaries = secondaries
+    }
+
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE: Int = EmojiViewData::class.java.name.hashCode()
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
new file mode 100644
index 0000000..4dc5cd7
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -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.emoji2.emojipicker
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+/** A [ViewHolder] containing an emoji view and emoji data.  */
+internal class EmojiViewHolder(
+    parent: ViewGroup,
+    layoutInflater: LayoutInflater,
+    width: Int,
+    height: Int
+) : ViewHolder(
+    layoutInflater
+        .inflate(R.layout.emoji_view_holder, parent, /* attachToRoot= */false)
+) {
+    private val emojiView: EmojiView
+
+    init {
+        itemView.layoutParams = LayoutParams(width, height)
+        emojiView = itemView.findViewById(R.id.emoji_view)
+        emojiView.isClickable = true
+    }
+
+    fun bindEmoji(
+        emojiViewItem: EmojiViewItem
+    ) {
+        emojiView.emoji = emojiViewItem.primary
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
similarity index 74%
rename from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
rename to emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
index dfb2685..2f79955 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewItem.kt
@@ -14,13 +14,6 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.emoji2.emojipicker
 
-import androidx.compose.ui.graphics.Color
-
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+internal class EmojiViewItem(val primary: String, val variants: List<String>)
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
new file mode 100644
index 0000000..9b96673
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmptyCategoryViewData.kt
@@ -0,0 +1,57 @@
+/*
+ * 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
+
+/**
+ * Indicator to show "You haven't used any emojis yet"-like label for empty category.
+ *
+ * EmptyCategoryViewData: A full-width `Space` at the beginning of each empty category, to
+ * show description and indicate that there is no items in the category.
+ */
+internal class EmptyCategoryViewData
+/**
+ * Instantiates an EmptyCategoryViewData.
+ *
+ * @param categoryIndex Used to compute the id.
+ * @param idInCategory Used to compute the id.
+ * @param description The description showing in the text view, e.g. "You haven't used any emojis
+ * yet". If empty, will look up the corresponding description based on `categoryIndex`
+ * in [EmojiPickerBodyAdapter.onBindViewHolder].
+ */(
+    categoryIndex: Int,
+    idInCategory: Int,
+    /** The description to indicate the category is empty.  */
+    val description: String
+) :
+    ItemViewData(calculateId(TYPE, categoryIndex, idInCategory)) {
+    override val type: Int
+        get() = TYPE
+
+    companion object {
+        val TYPE = EmptyCategoryViewData::class.java.name.hashCode()
+
+        /**
+         * Use -1 as categoryIndex and idInCategory and empty string as description for the default
+         * instance. The categoryIndex and idInCategory are just used to compute the id for the instance.
+         * Make the description empty to look up the corresponding description based on category index in
+         * [EmojiPickerBodyAdapter.onBindViewHolder].
+         */
+        val INSTANCE = EmptyCategoryViewData( /* categoryIndex= */
+            -1, /* idInCategory= */-1, /* description= */""
+        )
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
new file mode 100644
index 0000000..6df7bae
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 androidx.annotation.IntRange
+
+/** Value (immutable) classes for Emoji Picker.*/
+internal abstract class ItemViewData(val id: Long) {
+    abstract val type: Int
+
+    override fun hashCode(): Int {
+        return (id xor (id ushr 32)).toInt()
+    }
+
+    companion object {
+        fun calculateId(
+            type: Int,
+            @IntRange(from = 0, to = 256) categoryIndex: Int,
+            @IntRange(from = 0) idInCategory: Int
+        ): Long {
+            return type.toLong() shl 60 or (categoryIndex.toLong() shl 32) or idInCategory.toLong()
+        }
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
new file mode 100644
index 0000000..45edc3c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewDataFlatList.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.util.Log
+import androidx.annotation.IntRange
+
+/**
+ * Flattened list of categorized `ItemViewData` (`List<List<ItemViewData>>`) with placeholder
+ * entries and category separators.
+ *
+ * Keyword "position" is defined in `RecyclerView`.
+ */
+internal class ItemViewDataFlatList(
+    categorizedSources: List<List<ItemViewData>>,
+    @IntRange(from = 1) columns: Int
+) : AbstractList<ItemViewData>() {
+
+    companion object {
+        const val LOG_TAG = "ItemViewDataFlatList"
+    }
+
+    override val size: Int
+        get() = totalSize
+
+    /** Returns number of categories  */
+    /** # of categories.  */
+    @get:IntRange(from = 0)
+    val numberOfCategories: Int
+    private val categorizedSources: MutableList<List<ItemViewData>>
+    private val categorySizes: IntArray
+    private val categoryStartPositions: IntArray
+    private val columns: Int
+
+    /** == `size()`, including all types of `ItemViewData`s.  */
+    private var totalSize = 0
+
+    init {
+        this.categorizedSources = ArrayList(categorizedSources)
+        this.columns = columns
+        numberOfCategories = this.categorizedSources.size
+        categorySizes = IntArray(numberOfCategories)
+        categoryStartPositions = IntArray(numberOfCategories)
+        updateIndex()
+        if (categorizedSources.isEmpty()) {
+            Log.wtf(LOG_TAG, "Initialized with empty categorized sources")
+        }
+    }
+
+    private fun updateIndex() {
+        var categoryStartPosition = 0
+        for (currentCategoryIndex in 0 until numberOfCategories) {
+            val sources: List<ItemViewData> = categorizedSources[currentCategoryIndex]
+            val sourcesSize: Int = sources.size
+            categoryStartPositions[currentCategoryIndex] = categoryStartPosition
+            var sourcesSizeIncludingEmpty: Int
+            var rowsInCategory = Math.ceil(sourcesSize / columns.toDouble()).toInt()
+            // Guarantee showing at least `minRowsPerCategory` rows for each category.
+            rowsInCategory = Math.max(rowsInCategory, EmojiPickerConstants.MIN_ROWS_PER_CATEGORY)
+            sourcesSizeIncludingEmpty =
+                if (sourcesSize <= 0 || sourcesSize == 1 && sources[0] is EmptyCategoryViewData) {
+                    // category separator(occupy entire row) + empty category indicator(occupy entire row)
+                    // + placeholder view items
+                    1 + 1 + if (rowsInCategory >= 1) (rowsInCategory - 1) * columns else 0
+                } else {
+                    rowsInCategory * columns + 1 // +1 for category separator
+                }
+            categorySizes[currentCategoryIndex] = sourcesSizeIncludingEmpty
+            categoryStartPosition += sourcesSizeIncludingEmpty
+        }
+        totalSize = categoryStartPosition
+    }
+
+    override fun get(@IntRange(from = 0) index: Int): ItemViewData {
+        val currentCategoryIndex = getCategoryIndex(index)
+        val indexInCategory = index - categoryStartPositions[currentCategoryIndex]
+        return if (indexInCategory < 0) {
+            Log.wtf(
+                LOG_TAG,
+                String.format(
+                    "position (%d) for category (%d) is invalid",
+                    index,
+                    currentCategoryIndex
+                )
+
+            )
+            DummyViewData.INSTANCE
+        } else if (indexInCategory == 0) {
+            // Category separator occupies first place.
+            CategorySeparatorViewData(
+                currentCategoryIndex, indexInCategory, /* categoryName= */""
+            )
+        } else if (indexInCategory < categorizedSources[currentCategoryIndex].size + 1) {
+            // Concrete ItemViewData.
+            categorizedSources[currentCategoryIndex][indexInCategory - 1]
+        } else if (indexInCategory == 1 && categorizedSources[currentCategoryIndex].isEmpty()) {
+            // Empty category indicator.
+            EmptyCategoryViewData.INSTANCE
+        } else {
+            // Placeholder entries located at the end of category.
+            DummyViewData.INSTANCE
+        }
+    }
+
+    /** Returns category index for given `position`  */
+    @IntRange(from = 0)
+    fun getCategoryIndex(@IntRange(from = 0) position: Int): Int {
+        var currentCategoryIndex = 0
+        while (currentCategoryIndex + 1 < numberOfCategories &&
+            position >= categoryStartPositions[currentCategoryIndex + 1]
+        ) {
+            currentCategoryIndex++
+        }
+        return currentCategoryIndex
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
index 6c1d57d..7b1bdaf 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/utils/FileCache.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.VisibleForTesting
 import androidx.core.content.ContextCompat
 import androidx.emoji2.emojipicker.BundledEmojiListLoader
+import androidx.emoji2.emojipicker.EmojiViewItem
 import java.io.File
 
 /**
@@ -52,8 +53,8 @@
     /** Get cache for a given file name, or write to a new file using the [defaultValue] factory. */
     internal fun getOrPut(
         key: String,
-        defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
-    ): List<BundledEmojiListLoader.EmojiData> {
+        defaultValue: () -> List<EmojiViewItem>
+    ): List<EmojiViewItem> {
         val targetDir = File(emojiPickerCacheDir, currentProperty)
         // No matching cache folder for current property, clear stale cache directory if any
         if (!targetDir.exists()) {
@@ -65,19 +66,19 @@
         return readFrom(targetFile) ?: writeTo(targetFile, defaultValue)
     }
 
-    private fun readFrom(targetFile: File): List<BundledEmojiListLoader.EmojiData>? {
+    private fun readFrom(targetFile: File): List<EmojiViewItem>? {
         if (!targetFile.isFile)
             return null
         return targetFile.bufferedReader()
             .useLines { it.toList() }
             .map { it.split(",") }
-            .map { BundledEmojiListLoader.EmojiData(it.first(), it.drop(1)) }
+            .map { EmojiViewItem(it.first(), it.drop(1)) }
     }
 
     private fun writeTo(
         targetFile: File,
-        defaultValue: () -> List<BundledEmojiListLoader.EmojiData>
-    ): List<BundledEmojiListLoader.EmojiData> {
+        defaultValue: () -> List<EmojiViewItem>
+    ): List<EmojiViewItem> {
         val data = defaultValue.invoke()
         targetFile.bufferedWriter()
             .use { out ->
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.xml
new file mode 100644
index 0000000..3bae1dd
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/ripple_emoji_view.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.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="?android:colorControlHighlight">
+    <item android:id="@android:id/mask">
+        <shape android:shape="rectangle">
+            <corners android:radius="12dp" />
+            <solid android:color="@android:color/white" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
new file mode 100644
index 0000000..b5453f1
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/category_text_view.xml
@@ -0,0 +1,37 @@
+<?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.
+  -->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:importantForAccessibility="no">
+    <androidx.appcompat.widget.AppCompatTextView
+        android:id="@+id/category_name"
+        android:layout_width="wrap_content"
+        android:layout_height="24dp"
+        android:layout_alignParentStart="true"
+        android:layout_alignParentTop="true"
+        android:paddingTop="4dp"
+        android:paddingLeft="7dp"
+        android:paddingRight="7dp"
+        android:gravity="center_vertical|start"
+        android:letterSpacing="0.1"
+        android:importantForAccessibility="yes"
+        style="?attr/EmojiPickerStyleCategoryLabelText"
+        android:textSize="12dp" />
+</RelativeLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
index 0753bdd..f1bb622 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -22,10 +22,12 @@
     <androidx.recyclerview.widget.RecyclerView
         android:id="@+id/emoji_picker_header"
         android:layout_width="wrap_content"
-        android:layout_height="@dimen/emoji_picker_header_height"/>
+        android:layout_height="@dimen/emoji_picker_header_height"
+        android:paddingEnd="@dimen/emoji_picker_header_padding"
+        android:paddingStart="@dimen/emoji_picker_header_padding" />
 
-    <androidx.recyclerview.widget.RecyclerView
+    <androidx.emoji2.emojipicker.EmojiPickerBodyView
         android:id="@+id/emoji_picker_body"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"/>
+        android:layout_height="match_parent" />
 </LinearLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
new file mode 100644
index 0000000..0fa8c9c
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_view_holder.xml
@@ -0,0 +1,31 @@
+<?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"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="0dp"
+    android:layout_height="0dp">
+
+    <androidx.emoji2.emojipicker.EmojiView
+        android:id="@+id/emoji_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/ripple_emoji_view"
+        android:importantForAccessibility="yes"
+        android:textSize="30dp"
+        tools:ignore="SpUsage" />
+</FrameLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
index 3948f9f..0a2d01f2 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
@@ -15,9 +15,11 @@
   -->
 
 <resources>
-    <attr name="EmojiPickerColorCategoryEmptyHintText" format="color"/>
-    <attr name="EmojiPickerColorHeaderIcon" format="color"/>
-    <attr name="EmojiPickerColorHeaderIconSelected" format="color"/>
-    <attr name="EmojiPickerStyleHeaderIcon" format="reference"/>
-    <attr name="EmojiPickerStyleCategoryEmptyHintText" format="reference"/>
+    <attr name="EmojiPickerColorCategoryEmptyHintText" format="color" />
+    <attr name="EmojiPickerColorCategoryLabelText" format="color" />
+    <attr name="EmojiPickerColorHeaderIcon" format="color" />
+    <attr name="EmojiPickerColorHeaderIconSelected" format="color" />
+    <attr name="EmojiPickerStyleCategoryEmptyHintText" format="reference" />
+    <attr name="EmojiPickerStyleCategoryLabelText" format="reference" />
+    <attr name="EmojiPickerStyleHeaderIcon" format="reference" />
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
index 5ab47ec..cc79c54 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -23,4 +23,5 @@
     <dimen name="emoji_picker_header_icon_underline_width">28dp</dimen>
     <dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
     <dimen name="emoji_picker_header_height">50dp</dimen>
+    <dimen name="emoji_picker_header_padding">8dp</dimen>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index e2ffab3..472702f 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -1,5 +1,4 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
+<?xml version="1.0" encoding="UTF-8"?><!--
   Copyright 2022 The Android Open Source Project
 
   Licensed under the Apache License, Version 2.0 (the "License");
@@ -37,4 +36,6 @@
 
     <!-- Shown in emoji keyboard (non-Recent category) when the category is empty. -->
     <string name="emoji_empty_non_recent_category">No emojis available</string>
+    <!-- Shown in emoji keyboard when Recent emoji is empty. -->
+    <string name="emoji_empty_recent_category">You haven\'t used any emojis yet</string>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
index fa44e69..0e6d24e 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -27,4 +27,8 @@
     <style name="EmojiPickerCategoryContainer">
         <item name="android:layout_marginBottom">8dp</item>
     </style>
+
+    <style name="EmojiPickerStyleCategoryLabelText">
+        <item name="android:textColor">?attr/EmojiPickerColorCategoryLabelText</item>
+    </style>
 </resources>
diff --git a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
index fe48524..bdf635b 100644
--- a/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
+++ b/external/paparazzi/paparazzi/src/main/java/app/cash/paparazzi/Paparazzi.kt
@@ -568,11 +568,11 @@
       val compositionInvalidations = recomposer.javaClass
         .getDeclaredField("compositionInvalidations")
         .apply { isAccessible = true }
-        .get(recomposer) as MutableList<*>
+        .get(recomposer) as MutableCollection<*>
       val snapshotInvalidations = recomposer.javaClass
         .getDeclaredField("snapshotInvalidations")
         .apply { isAccessible = true }
-        .get(recomposer) as MutableList<*>
+        .get(recomposer) as MutableCollection<*>
       compositionInvalidations.clear()
       snapshotInvalidations.clear()
       applyObservers.clear()
diff --git a/fragment/fragment/api/current.txt b/fragment/fragment/api/current.txt
index d10b6de..ee8ccae 100644
--- a/fragment/fragment/api/current.txt
+++ b/fragment/fragment/api/current.txt
@@ -489,6 +489,7 @@
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -539,5 +540,12 @@
     property public final android.view.ViewGroup container;
   }
 
+  public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+    method public int getContainerId();
+    method public androidx.fragment.app.Fragment getParentFragment();
+    property public final int containerId;
+    property public final androidx.fragment.app.Fragment parentFragment;
+  }
+
 }
 
diff --git a/fragment/fragment/api/public_plus_experimental_current.txt b/fragment/fragment/api/public_plus_experimental_current.txt
index d10b6de..ee8ccae 100644
--- a/fragment/fragment/api/public_plus_experimental_current.txt
+++ b/fragment/fragment/api/public_plus_experimental_current.txt
@@ -489,6 +489,7 @@
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -539,5 +540,12 @@
     property public final android.view.ViewGroup container;
   }
 
+  public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+    method public int getContainerId();
+    method public androidx.fragment.app.Fragment getParentFragment();
+    property public final int containerId;
+    property public final androidx.fragment.app.Fragment parentFragment;
+  }
+
 }
 
diff --git a/fragment/fragment/api/restricted_current.txt b/fragment/fragment/api/restricted_current.txt
index 44528a7..29eb3b1 100644
--- a/fragment/fragment/api/restricted_current.txt
+++ b/fragment/fragment/api/restricted_current.txt
@@ -518,6 +518,7 @@
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectSetUserVisibleHint();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectTargetFragmentUsage();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongFragmentContainer();
+    method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder detectWrongNestedHierarchy();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyDeath();
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyListener(androidx.fragment.app.strictmode.FragmentStrictMode.OnViolationListener listener);
     method public androidx.fragment.app.strictmode.FragmentStrictMode.Policy.Builder penaltyLog();
@@ -568,5 +569,12 @@
     property public final android.view.ViewGroup container;
   }
 
+  public final class WrongNestedHierarchyViolation extends androidx.fragment.app.strictmode.Violation {
+    method public int getContainerId();
+    method public androidx.fragment.app.Fragment getParentFragment();
+    property public final int containerId;
+    property public final androidx.fragment.app.Fragment parentFragment;
+  }
+
 }
 
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 8debe9d..6611e65 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/DialogFragmentDismissTest.kt
@@ -19,6 +19,7 @@
 import android.app.AlertDialog
 import android.app.Dialog
 import android.content.DialogInterface
+import android.os.Build
 import android.os.Bundle
 import android.os.Looper
 import androidx.fragment.app.test.EmptyFragmentTestActivity
@@ -111,6 +112,11 @@
 
     @Test
     fun testDialogFragmentDismiss() {
+        // Due to b/157955883, we need to early return if API == 30.
+        // Otherwise, this test flakes.
+        if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+            return
+        }
         val fragment = TestDialogFragment()
         activityTestRule.runOnUiThread {
             fragment.showNow(activityTestRule.activity.supportFragmentManager, null)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
index 5ce2fab..c68e779 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleOwnerTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.app.test.TestViewModel
+import androidx.fragment.app.test.ViewModelActivity
 import androidx.fragment.test.R
 import androidx.lifecycle.HasDefaultViewModelProviderFactory
 import androidx.lifecycle.ViewModel
@@ -129,6 +130,48 @@
         }
     }
 
+    @Test
+    fun testCreateViewModelViaExtrasSavedState() {
+        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                setContentView(R.layout.simple_container)
+                supportFragmentManager
+            }
+            val fragment = StrictViewFragment()
+
+            fm.beginTransaction()
+                .add(R.id.fragmentContainer, fragment, "fragment")
+                .commit()
+            executePendingTransactions()
+
+            val viewLifecycleOwner = (fragment.viewLifecycleOwner as FragmentViewLifecycleOwner)
+
+            val creationViewModel = ViewModelProvider(
+                viewLifecycleOwner.viewModelStore,
+                viewLifecycleOwner.defaultViewModelProviderFactory,
+                viewLifecycleOwner.defaultViewModelCreationExtras
+            )["test", ViewModelActivity.TestSavedStateViewModel::class.java]
+
+            creationViewModel.savedStateHandle["key"] = "value"
+
+            recreate()
+
+            val recreatedViewLifecycleOwner = withActivity {
+                supportFragmentManager.findFragmentByTag("fragment")?.viewLifecycleOwner
+                    as FragmentViewLifecycleOwner
+            }
+
+            val recreateViewModel = ViewModelProvider(recreatedViewLifecycleOwner)[
+                "test", ViewModelActivity.TestSavedStateViewModel::class.java
+            ]
+
+            assertThat(recreateViewModel).isSameInstanceAs(creationViewModel)
+
+            val value: String? = recreateViewModel.savedStateHandle["key"]
+            assertThat(value).isEqualTo("value")
+        }
+    }
+
     class FakeViewModelProviderFactory : ViewModelProvider.Factory {
         private var createCalled: Boolean = false
         override fun <T : ViewModel> create(modelClass: Class<T>): T {
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentTest.kt
index 448fea2..5004ac0 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/NestedFragmentTest.kt
@@ -26,29 +26,28 @@
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
-import org.junit.Before
 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.rules.RuleChain
 
 @Suppress("DEPRECATION")
 @RunWith(AndroidJUnit4::class)
 @LargeTest
 class NestedFragmentTest {
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
-    private lateinit var instrumentation: Instrumentation
-    private lateinit var parentFragment: ParentFragment
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
 
-    @Before
-    fun setup() {
-        instrumentation = InstrumentationRegistry.getInstrumentation()
+    private fun setupParentFragment(parentFragment: ParentFragment) {
         val fragmentManager = activityRule.activity.supportFragmentManager
-        parentFragment = ParentFragment()
         fragmentManager.beginTransaction().add(parentFragment, "parent").commit()
         val latch = CountDownLatch(1)
         activityRule.runOnUiThread {
@@ -60,6 +59,10 @@
 
     @Test
     fun testUsingUpper16BitRequestCode() {
+        val parentFragment = ParentFragment()
+        setupParentFragment(parentFragment)
+
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
         val activityResult = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())
 
         val activityMonitor = instrumentation.addMonitor(
@@ -89,6 +92,10 @@
 
     @Test
     fun testNestedFragmentStartActivityForResult() {
+        val parentFragment = ParentFragment()
+        setupParentFragment(parentFragment)
+
+        val instrumentation = InstrumentationRegistry.getInstrumentation()
         val activityResult = Instrumentation.ActivityResult(Activity.RESULT_OK, Intent())
 
         val activityMonitor = instrumentation.addMonitor(
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 241a570d..abe5b23 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
@@ -31,7 +31,6 @@
 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
 
@@ -45,7 +44,8 @@
 
     @Test
     fun testBackPressFinishesActivity() {
-       withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+        // Since this activity finishes manually, we do not want to use withUse here
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
             val countDownLatch = withActivity {
                 onBackPressed()
                 finishCountDownLatch
@@ -173,10 +173,10 @@
         }
     }
 
-    @Ignore // b/250870927
     @Test
     fun testBackPressFinishesActivityAfterFragmentPop() {
-       withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+        // Since this activity finishes manually, we do not want to use withUse here
+       with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
             val fragmentManager = withActivity { supportFragmentManager }
             val fragment = StrictFragment()
             fragmentManager.beginTransaction()
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 83a2f21..9ea6d81 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
@@ -18,6 +18,7 @@
 
 import android.os.Looper
 import androidx.fragment.app.StrictFragment
+import androidx.fragment.app.StrictViewFragment
 import androidx.fragment.app.executePendingTransactions
 import androidx.fragment.app.test.FragmentTestActivity
 import androidx.fragment.test.R
@@ -234,6 +235,87 @@
         }
     }
 
+    @Test
+    public fun detectWrongNestedHierarchyNoParent() {
+        var violation: Violation? = null
+        val policy = FragmentStrictMode.Policy.Builder()
+            .detectWrongNestedHierarchy()
+            .penaltyListener { violation = it }
+            .build()
+        FragmentStrictMode.defaultPolicy = policy
+
+        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                setContentView(R.layout.simple_container)
+                supportFragmentManager
+            }
+            val outerFragment = StrictViewFragment(R.layout.scene1)
+            val innerFragment = StrictViewFragment(R.layout.fragment_a)
+
+            fm.beginTransaction()
+                .add(R.id.fragmentContainer, outerFragment)
+                .setReorderingAllowed(false)
+                .commit()
+            // Here we add childFragment to a layout within parentFragment, but we
+            // specifically don't use parentFragment.childFragmentManager
+            fm.beginTransaction()
+                .add(R.id.squareContainer, innerFragment)
+                .setReorderingAllowed(false)
+                .commit()
+            executePendingTransactions()
+
+            assertThat(violation).isInstanceOf(WrongNestedHierarchyViolation::class.java)
+            assertThat(violation).hasMessageThat().contains(
+                "Attempting to nest fragment $innerFragment within the view " +
+                    "of parent fragment $outerFragment via container with ID " +
+                    "${R.id.squareContainer} without using parent's childFragmentManager"
+            )
+        }
+    }
+
+    @Test
+    public fun detectWrongNestedHierarchyWrongParent() {
+        var violation: Violation? = null
+        val policy = FragmentStrictMode.Policy.Builder()
+            .detectWrongNestedHierarchy()
+            .penaltyListener { violation = it }
+            .build()
+        FragmentStrictMode.defaultPolicy = policy
+
+        withUse(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                setContentView(R.layout.simple_container)
+                supportFragmentManager
+            }
+            val grandParent = StrictViewFragment(R.layout.scene1)
+            val parentFragment = StrictViewFragment(R.layout.scene5)
+            val childFragment = StrictViewFragment(R.layout.fragment_a)
+            fm.beginTransaction()
+                .add(R.id.fragmentContainer, grandParent)
+                .setReorderingAllowed(false)
+                .commit()
+            executePendingTransactions()
+            grandParent.childFragmentManager.beginTransaction()
+                .add(R.id.squareContainer, parentFragment)
+                .setReorderingAllowed(false)
+                .commit()
+            executePendingTransactions()
+            // Here we use the grandParent.childFragmentManager for the child
+            // fragment, though we should actually be using parentFragment.childFragmentManager
+            grandParent.childFragmentManager.beginTransaction()
+                .add(R.id.sharedElementContainer, childFragment)
+                .setReorderingAllowed(false)
+                .commit()
+            executePendingTransactions(parentFragment.childFragmentManager)
+            assertThat(violation).isInstanceOf(WrongNestedHierarchyViolation::class.java)
+            assertThat(violation).hasMessageThat().contains(
+                "Attempting to nest fragment $childFragment within the view " +
+                    "of parent fragment $parentFragment via container with ID " +
+                    "${R.id.sharedElementContainer} without using parent's childFragmentManager"
+            )
+        }
+    }
+
     @Suppress("DEPRECATION")
     @Test
     public fun detectRetainInstanceUsage() {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
index 22f4c63..1e0f070 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java
@@ -1065,7 +1065,7 @@
      * @return the locally scoped {@link Fragment} to the given view, if found
      */
     @Nullable
-    private static Fragment findViewFragment(@NonNull View view) {
+    static Fragment findViewFragment(@NonNull View view) {
         while (view != null) {
             Fragment fragment = getViewFragment(view);
             if (fragment != null) {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
index 0b54927..859045b 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentStateManager.java
@@ -860,6 +860,16 @@
     }
 
     void addViewToContainer() {
+        Fragment expectedParent = FragmentManager.findViewFragment(mFragment.mContainer);
+        Fragment actualParent = mFragment.getParentFragment();
+        // onFindViewById prevents any wrong nested hierarchies when expectedParent is null already
+        if (expectedParent != null) {
+            if (!expectedParent.equals(actualParent)) {
+                FragmentStrictMode.onWrongNestedHierarchy(mFragment, expectedParent,
+                        mFragment.mContainerId);
+            }
+        }
+
         // Ensure that our new Fragment is placed in the right index
         // based on its relative position to Fragments already in the
         // same container
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
index 6f416d8..7c5330a 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
@@ -16,8 +16,6 @@
 
 package androidx.fragment.app;
 
-import static androidx.lifecycle.SavedStateHandleSupport.enableSavedStateHandles;
-
 import android.app.Application;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -76,7 +74,6 @@
             mLifecycleRegistry = new LifecycleRegistry(this);
             mSavedStateRegistryController = SavedStateRegistryController.create(this);
             mSavedStateRegistryController.performAttach();
-            enableSavedStateHandles(this);
             mRestoreViewSavedStateRunnable.run();
         }
     }
@@ -134,7 +131,7 @@
 
             mDefaultFactory = new SavedStateViewModelFactory(
                     application,
-                    this,
+                    mFragment,
                     mFragment.getArguments());
         }
 
@@ -158,7 +155,7 @@
         if (application != null) {
             extras.set(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY, application);
         }
-        extras.set(SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY, this);
+        extras.set(SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY, mFragment);
         extras.set(SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY, this);
         if (mFragment.getArguments() != null) {
             extras.set(SavedStateHandleSupport.DEFAULT_ARGS_KEY, mFragment.getArguments());
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
index 1137313..04ef240 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/FragmentStrictMode.kt
@@ -95,6 +95,27 @@
      */
     @JvmStatic
     @RestrictTo(RestrictTo.Scope.LIBRARY)
+    fun onWrongNestedHierarchy(
+        fragment: Fragment,
+        parentFragment: Fragment,
+        containerId: Int
+    ) {
+        val violation: Violation =
+            WrongNestedHierarchyViolation(fragment, parentFragment, containerId)
+        logIfDebuggingEnabled(violation)
+        val policy = getNearestPolicy(fragment)
+        if (policy.flags.contains(Flag.DETECT_WRONG_NESTED_HIERARCHY) &&
+            shouldHandlePolicyViolation(policy, fragment.javaClass, violation.javaClass)
+        ) {
+            handlePolicyViolation(policy, violation)
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @JvmStatic
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     fun onSetRetainInstanceUsage(fragment: Fragment) {
         val violation: Violation = SetRetainInstanceUsageViolation(fragment)
         logIfDebuggingEnabled(violation)
@@ -284,6 +305,7 @@
         PENALTY_DEATH,
         DETECT_FRAGMENT_REUSE,
         DETECT_FRAGMENT_TAG_USAGE,
+        DETECT_WRONG_NESTED_HIERARCHY,
         DETECT_RETAIN_INSTANCE_USAGE,
         DETECT_SET_USER_VISIBLE_HINT,
         DETECT_TARGET_FRAGMENT_USAGE,
@@ -378,6 +400,13 @@
                 return this
             }
 
+            /** Detects nested fragments that do not use the parent's childFragmentManager.  */
+            @SuppressLint("BuilderSetStyle")
+            fun detectWrongNestedHierarchy(): Builder {
+                flags.add(Flag.DETECT_WRONG_NESTED_HIERARCHY)
+                return this
+            }
+
             /**
              * Detects calls to [Fragment.setRetainInstance] and [Fragment.getRetainInstance].
              */
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.kt b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.kt
new file mode 100644
index 0000000..3ec71c8
--- /dev/null
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/strictmode/WrongNestedHierarchyViolation.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.
+ */
+
+package androidx.fragment.app.strictmode
+
+import androidx.fragment.app.Fragment
+
+/**
+ * See [FragmentStrictMode.Policy.Builder.detectWrongNestedHierarchy].
+ */
+class WrongNestedHierarchyViolation internal constructor(
+    fragment: Fragment,
+    /**
+     * Gets the expected parent [Fragment] of the fragment causing the Violation.
+     */
+    val parentFragment: Fragment,
+    /**
+     * Gets the unique ID of the container that the [Fragment] causing
+     * the Violation would have been added to.
+     */
+    val containerId: Int
+) : Violation(
+    fragment,
+    "Attempting to nest fragment $fragment within the view " +
+        "of parent fragment $parentFragment via container with ID $containerId " +
+        "without using parent's childFragmentManager"
+)
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index b163bae..cd1190b 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -57,7 +57,7 @@
   public final class CoroutineBroadcastReceiverKt {
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlanceRemoteViewsApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlanceRemoteViewsApi {
   }
 
   public final class GeneratedLayoutsKt {
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index c8aa9d9..1ad5b3b6 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -43,9 +43,9 @@
 
     api(project(":glance:glance"))
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.compose.runtime:runtime:1.1.0-beta01")
-    api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
-    api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+    api("androidx.compose.runtime:runtime:1.1.1")
+    api("androidx.compose.ui:ui-graphics:1.1.1")
+    api("androidx.compose.ui:ui-unit:1.1.1")
 
     implementation('androidx.core:core-ktx:1.7.0')
     implementation("androidx.datastore:datastore:1.0.0")
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
index c9c7d36..009e306 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/ExperimentalGlanceRemoteViewsApi.kt
@@ -17,4 +17,5 @@
 package androidx.glance.appwidget
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalGlanceRemoteViewsApi
diff --git a/glance/glance-preview/api/public_plus_experimental_current.txt b/glance/glance-preview/api/public_plus_experimental_current.txt
index c536700..1fc4543 100644
--- a/glance/glance-preview/api/public_plus_experimental_current.txt
+++ b/glance/glance-preview/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.glance.preview {
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlancePreviewApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlancePreviewApi {
   }
 
   @androidx.glance.preview.ExperimentalGlancePreviewApi @kotlin.annotation.Repeatable @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface GlancePreview {
diff --git a/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt b/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
index b21e9d3..0a5ac68 100644
--- a/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
+++ b/glance/glance-preview/src/androidMain/kotlin/androidx/glance/preview/ExperimentalGlancePreviewApi.kt
@@ -17,4 +17,5 @@
 package androidx.glance.preview
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalGlancePreviewApi
\ No newline at end of file
diff --git a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
index 117a296..d6febbf 100644
--- a/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
+++ b/glance/glance-wear-tiles/api/public_plus_experimental_current.txt
@@ -18,7 +18,7 @@
   public final class ErrorUiLayoutKt {
   }
 
-  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") public @interface ExperimentalGlanceWearTilesApi {
+  @kotlin.RequiresOptIn(message="This API is experimental and is likely to change in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalGlanceWearTilesApi {
   }
 
   public abstract class GlanceTileService extends androidx.wear.tiles.TileService {
diff --git a/glance/glance-wear-tiles/build.gradle b/glance/glance-wear-tiles/build.gradle
index 7c09cd6..eccfcf3 100644
--- a/glance/glance-wear-tiles/build.gradle
+++ b/glance/glance-wear-tiles/build.gradle
@@ -30,9 +30,9 @@
 dependencies {
 
     api(project(":glance:glance"))
-    api("androidx.compose.runtime:runtime:1.1.0-beta01")
-    api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
-    api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+    api("androidx.compose.runtime:runtime:1.1.1")
+    api("androidx.compose.ui:ui-graphics:1.1.1")
+    api("androidx.compose.ui:ui-unit:1.1.1")
     api("androidx.wear.tiles:tiles:1.0.0")
 
     implementation(libs.kotlinStdlib)
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
index 2587a4b..8966090 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/ExperimentalGlanceWearTilesApi.kt
@@ -17,4 +17,5 @@
 package androidx.glance.wear.tiles
 
 @RequiresOptIn("This API is experimental and is likely to change in the future.")
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalGlanceWearTilesApi
\ No newline at end of file
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -580,14 +580,12 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
     method public androidx.glance.template.ActionBlock? getActionBlock();
-    method public boolean getDisplayHeader();
     method public androidx.glance.template.HeaderBlock? getHeaderBlock();
     method public androidx.glance.template.ImageBlock? getImageBlock();
     method public androidx.glance.template.TextBlock? getTextBlock();
     property public final androidx.glance.template.ActionBlock? actionBlock;
-    property public final boolean displayHeader;
     property public final androidx.glance.template.HeaderBlock? headerBlock;
     property public final androidx.glance.template.ImageBlock? imageBlock;
     property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -580,14 +580,12 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
     method public androidx.glance.template.ActionBlock? getActionBlock();
-    method public boolean getDisplayHeader();
     method public androidx.glance.template.HeaderBlock? getHeaderBlock();
     method public androidx.glance.template.ImageBlock? getImageBlock();
     method public androidx.glance.template.TextBlock? getTextBlock();
     property public final androidx.glance.template.ActionBlock? actionBlock;
-    property public final boolean displayHeader;
     property public final androidx.glance.template.HeaderBlock? headerBlock;
     property public final androidx.glance.template.ImageBlock? imageBlock;
     property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index ad6cf59..222ec6d 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -580,14 +580,12 @@
   }
 
   public final class SingleEntityTemplateData {
-    ctor public SingleEntityTemplateData(optional boolean displayHeader, optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
+    ctor public SingleEntityTemplateData(optional androidx.glance.template.HeaderBlock? headerBlock, optional androidx.glance.template.TextBlock? textBlock, optional androidx.glance.template.ImageBlock? imageBlock, optional androidx.glance.template.ActionBlock? actionBlock);
     method public androidx.glance.template.ActionBlock? getActionBlock();
-    method public boolean getDisplayHeader();
     method public androidx.glance.template.HeaderBlock? getHeaderBlock();
     method public androidx.glance.template.ImageBlock? getImageBlock();
     method public androidx.glance.template.TextBlock? getTextBlock();
     property public final androidx.glance.template.ActionBlock? actionBlock;
-    property public final boolean displayHeader;
     property public final androidx.glance.template.HeaderBlock? headerBlock;
     property public final androidx.glance.template.ImageBlock? imageBlock;
     property public final androidx.glance.template.TextBlock? textBlock;
diff --git a/glance/glance/build.gradle b/glance/glance/build.gradle
index 3d96ae5..ba94622 100644
--- a/glance/glance/build.gradle
+++ b/glance/glance/build.gradle
@@ -30,9 +30,9 @@
 dependencies {
 
     api("androidx.annotation:annotation:1.2.0")
-    api("androidx.compose.runtime:runtime:1.1.0-beta01")
-    api("androidx.compose.ui:ui-graphics:1.1.0-beta01")
-    api("androidx.compose.ui:ui-unit:1.1.0-beta01")
+    api("androidx.compose.runtime:runtime:1.1.1")
+    api("androidx.compose.ui:ui-graphics:1.1.1")
+    api("androidx.compose.ui:ui-unit:1.1.1")
     api("androidx.datastore:datastore-core:1.0.0")
     api("androidx.datastore:datastore-preferences-core:1.0.0")
     api("androidx.datastore:datastore-preferences:1.0.0")
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
index c4a2b5f..a0a5308 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/template/SingleEntityTemplateData.kt
@@ -20,7 +20,6 @@
  * The semantic data required to build Single Entity Template layouts. The template allows for
  * a header, text section with up to three text items, main image, and single action button.
  *
- * @param displayHeader True if the glanceable header should be displayed
  * @param headerBlock The header block of the entity by [HeaderBlock].
  * @param textBlock The text block for up to three types of texts for the entity.
  * @param imageBlock The image block for the entity main image by [ImageBlock].
@@ -28,16 +27,13 @@
  */
 
 class SingleEntityTemplateData(
-    val displayHeader: Boolean = true,
     val headerBlock: HeaderBlock? = null,
     val textBlock: TextBlock? = null,
     val imageBlock: ImageBlock? = null,
     val actionBlock: ActionBlock? = null,
 ) {
-
     override fun hashCode(): Int {
-        var result = displayHeader.hashCode()
-        result = 31 * result + (headerBlock?.hashCode() ?: 0)
+        var result = headerBlock?.hashCode() ?: 0
         result = 31 * result + (textBlock?.hashCode() ?: 0)
         result = 31 * result + (imageBlock?.hashCode() ?: 0)
         result = 31 * result + (actionBlock?.hashCode() ?: 0)
@@ -50,7 +46,6 @@
 
         other as SingleEntityTemplateData
 
-        if (displayHeader != other.displayHeader) return false
         if (headerBlock != other.headerBlock) return false
         if (textBlock != other.textBlock) return false
         if (imageBlock != other.imageBlock) return false
diff --git a/gradle/README.md b/gradle/README.md
index 232393b..e945e4e 100644
--- a/gradle/README.md
+++ b/gradle/README.md
@@ -4,7 +4,7 @@
 
 ## libs.versions.toml
 
-Keeps track of library and plugin dependencies used by androidx. Adding or updating a library there requires running `./development/importMaven/import_maven_artifacts.py -n myartifact:here:1.0.0`
+Keeps track of library and plugin dependencies used by androidx. Adding or updating a library there requires running `./development/importMaven/importMaven.sh myartifact:here:1.0.0`
 
 ## verification-keyring.keys
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 58e943f..0546b9e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,24 +2,24 @@
 # -----------------------------------------------------------------------------
 # All of the following should be updated in sync.
 # -----------------------------------------------------------------------------
-androidGradlePlugin = "8.0.0-alpha05"
+androidGradlePlugin = "8.0.0-alpha07"
 # NOTE: When updating the lint version we also need to update the `api` version
 # supported by `IssueRegistry`'s.' For e.g. r.android.com/1331903
-androidLint = "31.0.0-alpha05"
+androidLint = "31.0.0-alpha07"
 # Once you have a chosen version of AGP to upgrade to, go to
 # https://developer.android.com/studio/archive and find the matching version of Studio.
-androidStudio = "2022.2.1.5"
+androidStudio = "2022.2.1.7"
 # -----------------------------------------------------------------------------
 
 androidGradlePluginMin = "7.0.4"
 androidLintMin = "30.0.4"
 androidLintMinCompose = "30.0.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"
+androidxTestRunner = "1.5.1"
+androidxTestRules = "1.5.0"
+androidxTestMonitor = "1.6.0"
+androidxTestCore = "1.5.0"
+androidxTestExtJunit = "1.1.4"
+androidxTestExtTruth = "1.5.0"
 atomicFu = "0.17.0"
 autoService = "1.0-rc6"
 autoValue = "1.6.3"
@@ -29,18 +29,18 @@
 dagger = "2.44"
 dexmaker = "2.28.3"
 dokka = "1.7.20"
-espresso = "3.5.0-rc01"
+espresso = "3.5.0"
 guavaJre = "31.1-jre"
 hilt = "2.44"
 incap = "0.2"
 jcodec = "0.2.5"
-kotlin = "1.7.20"
+kotlin = "1.7.21"
 kotlinBenchmark = "0.4.5"
-kotlinNative = "1.7.20"
+kotlinNative = "1.7.21"
 kotlinCompileTesting = "1.4.9"
 kotlinCoroutines = "1.6.4"
 kotlinSerialization = "1.3.3"
-ksp = "1.7.20-1.0.6"
+ksp = "1.7.21-1.0.8"
 ktlint = "0.46.0-20220520.192227-74"
 leakcanary = "2.8.1"
 metalava = "1.0.0-alpha06"
@@ -91,7 +91,7 @@
 checkerframework = { module = "org.checkerframework:checker-qual", version = "2.5.3" }
 checkmark = { module = "net.saff.checkmark:checkmark", version = "0.1.6" }
 constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version = "2.0.1"}
-dackka = { module = "com.google.devsite:dackka", version = "1.0.4" }
+dackka = { module = "com.google.devsite:dackka", version = "1.0.5" }
 dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" }
 daggerCompiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" }
 dexmakerMockito = { module = "com.linkedin.dexmaker:dexmaker-mockito", version.ref = "dexmaker" }
@@ -208,6 +208,7 @@
 protobufCompiler = { module = "com.google.protobuf:protoc", version.ref = "protobuf" }
 protobufGradlePluginz = { module = "com.google.protobuf:protobuf-gradle-plugin", version = "0.9.0" }
 protobufLite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protobuf" }
+protobufKotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" }
 reactiveStreams = { module = "org.reactivestreams:reactive-streams", version = "1.0.0" }
 retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
 retrofitConverterWire = { module = "com.squareup.retrofit2:converter-wire", version.ref = "retrofit" }
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index f9a0caf..764b8b2e 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -910,21 +910,21 @@
             <sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.20" androidx:reason="Unsigned, b/227204920">
-         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz">
-            <sha256 value="2b82114ad226276f88a321bd2c47ed162214c5c579ac899dd1f9ccdac4c739b9" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.21" androidx:reason="Unsigned, b/227204920">
+         <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz">
+            <sha256 value="b6a4aef343c029ac1b7d3e70101c45356a02a30b10fdd0813fb085b29cc714f4" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.7.20">
-         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.7.20.tar.gz">
-            <sha256 value="efdc6e5c1e5b15aa63989b725a2acfd5ffa6682824d7963e3b4b0987e5aecd3b" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-aarch64" version="1.7.21">
+         <artifact name="kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz">
+            <sha256 value="6953ddaa5ed2466ac606bd81338475af8064dce6a932aa51baaa0d3bff64bcc2" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-aarch64-1.7.21.tar.gz"/>
          </artifact>
       </component>
 
-      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.7.20">
-         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz">
-            <sha256 value="59ff4942f783def4c2293bccfbe2fd5fd7b3a8a6b878cfe95bb83921c75fe0cb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.20.tar.gz"/>
+      <component group="" name="kotlin-native-prebuilt-macos-x86_64" version="1.7.21">
+         <artifact name="kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz">
+            <sha256 value="ee01ebb7f44f8acc0aad87b61219f292dd766a06acb6ecba9fb21c5834c747eb" origin="Hand-built using sha256sum kotlin-native-prebuilt-macos-x86_64-1.7.21.tar.gz"/>
          </artifact>
       </component>
    </components>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 9650598..8f62713 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -30,7 +30,6 @@
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.opengl.egl.EGLVersion
 import androidx.graphics.opengl.egl.supportsNativeAndroidFence
-import androidx.hardware.SyncFence
 import androidx.lifecycle.Lifecycle
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -381,13 +380,6 @@
     }
 
     @Test
-    fun testExtractSyncFenceFd() {
-        val fileDescriptor = 7
-        val syncFence = SyncFence(7)
-        assertEquals(fileDescriptor, JniBindings.nExtractFenceFd(syncFence))
-    }
-
-    @Test
     fun testTransactionSetBuffer_nullFence() {
         val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
             .moveToState(
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
index c29a456..530c753 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlWrapperTest.kt
@@ -25,7 +25,6 @@
 import android.view.Surface
 import android.view.SurfaceControl
 import android.view.SurfaceHolder
-import androidx.hardware.SyncFence
 import androidx.lifecycle.Lifecycle
 import androidx.test.core.app.ActivityScenario
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -410,13 +409,6 @@
     }
 
     @Test
-    fun testExtractSyncFenceFd() {
-        val fileDescriptor = 7
-        val syncFence = SyncFence(7)
-        assertEquals(fileDescriptor, JniBindings.nExtractFenceFd(syncFence))
-    }
-
-    @Test
     fun testTransactionSetVisibility_show() {
         val listener = TransactionOnCompleteListener()
         val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
index 05b449d..4023ed0 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/hardware/SyncFenceTest.kt
@@ -17,6 +17,7 @@
 package androidx.hardware
 
 import android.os.Build
+import androidx.graphics.surface.JniBindings
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
@@ -30,6 +31,23 @@
 @SmallTest
 class SyncFenceTest {
 
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    @Test
+    fun testDupSyncFenceFd() {
+        val fileDescriptor = 7
+        val syncFence = SyncFence(7)
+        // If the file descriptor is valid dup'ing it should return a different fd
+        Assert.assertNotEquals(fileDescriptor, JniBindings.nDupFenceFd(syncFence))
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.KITKAT)
+    @Test
+    fun testDupSyncFenceFdWhenInvalid() {
+        // If the fence is invalid there should be no attempt to dup the fd it and -1
+        // should be returned
+        Assert.assertEquals(-1, JniBindings.nDupFenceFd(SyncFence(-1)))
+    }
+
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
     @Test
     fun testSignalTimeInvalid() {
diff --git a/graphics/graphics-core/src/main/cpp/graphics-core.cpp b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
index ed2243e..98bc848 100644
--- a/graphics/graphics-core/src/main/cpp/graphics-core.cpp
+++ b/graphics/graphics-core/src/main/cpp/graphics-core.cpp
@@ -155,6 +155,12 @@
     jmethodID onCommit{};
 } gTransactionCommittedListenerClassInfo;
 
+static struct {
+    bool CLASS_INFO_INITIALIZED = false;
+    jclass clazz{};
+    jmethodID dupeFileDescriptor{};
+} gSyncFenceClassInfo;
+
 #define NANO_SECONDS 1000000000LL
 
 int64_t getSystemTime() {
@@ -297,20 +303,29 @@
     }
 }
 
-int extract_fence_fd(JNIEnv *env, jobject syncFence) {
-    jclass sfClass = env->GetObjectClass(syncFence);
-    jfieldID fid = env->GetFieldID(sfClass, "fd", "I");
-    return env->GetIntField(syncFence, fid);
+void setupSyncFenceClassInfo(JNIEnv *env) {
+    if (!gSyncFenceClassInfo.CLASS_INFO_INITIALIZED) {
+        jclass syncFenceClazz = env->FindClass("androidx/hardware/SyncFence");
+        gSyncFenceClassInfo.clazz = static_cast<jclass>(env->NewGlobalRef(syncFenceClazz));
+        gSyncFenceClassInfo.dupeFileDescriptor =
+                env->GetMethodID(gSyncFenceClassInfo.clazz, "dupeFileDescriptor", "()I");
+        gSyncFenceClassInfo.CLASS_INFO_INITIALIZED = true;
+    }
+}
+
+int dup_fence_fd(JNIEnv *env, jobject syncFence) {
+    setupSyncFenceClassInfo(env);
+    return env->CallIntMethod(syncFence, gSyncFenceClassInfo.dupeFileDescriptor);
 }
 
 /* Helper method to extract the SyncFence file descriptor
  */
 extern "C"
 JNIEXPORT jint JNICALL
-Java_androidx_graphics_surface_JniBindings_00024Companion_nExtractFenceFd(JNIEnv *env,
-                                                                          jobject thiz,
-                                                                          jobject syncFence) {
-    return extract_fence_fd(env, syncFence);
+Java_androidx_graphics_surface_JniBindings_00024Companion_nDupFenceFd(JNIEnv *env,
+                                                                      jobject thiz,
+                                                                      jobject syncFence) {
+    return dup_fence_fd(env, syncFence);
 }
 
 extern "C"
@@ -325,7 +340,7 @@
         auto transaction = reinterpret_cast<ASurfaceTransaction *>(surfaceTransaction);
         auto sc = reinterpret_cast<ASurfaceControl *>(surfaceControl);
         auto hardwareBuffer = AHardwareBuffer_fromHardwareBuffer(env, hBuffer);
-        auto fence_fd = extract_fence_fd(env, syncFence);
+        auto fence_fd = dup_fence_fd(env, syncFence);
         ASurfaceTransaction_setBuffer(transaction, sc, hardwareBuffer, fence_fd);
     }
 }
@@ -500,4 +515,10 @@
     auto src = ARect{0, 0, bufferWidth, bufferHeight};
     auto dest = ARect{0, 0, dstWidth, dstHeight};
     ASurfaceTransaction_setGeometry(st, sc, src, dest, transformation);
+}
+
+extern "C"
+JNIEXPORT jint JNICALL
+Java_androidx_hardware_SyncFence_nDup(JNIEnv *env, jobject thiz, jint fd) {
+    return static_cast<jint>(dup(static_cast<int>(fd)));
 }
\ No newline at end of file
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 6c30b4c..b191913 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
@@ -560,6 +560,7 @@
                         transaction
                     )
                     transaction.commit()
+                    syncFenceCompat?.close()
                 }
             },
             mFrontBufferSyncStrategy
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
index b9af9e7..27dbd7b 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlWrapper.kt
@@ -52,7 +52,7 @@
             listener: SurfaceControlCompat.TransactionCommittedListener
         )
 
-        external fun nExtractFenceFd(
+        external fun nDupFenceFd(
             syncFence: SyncFence
         ): Int
 
diff --git a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
index 3963976..97c6b74 100644
--- a/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
+++ b/graphics/graphics-core/src/main/java/androidx/hardware/SyncFence.kt
@@ -19,6 +19,8 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.concurrent.withLock
 
 /**
  * A SyncFence represents a synchronization primitive which signals when hardware buffers have
@@ -33,11 +35,15 @@
 @RequiresApi(Build.VERSION_CODES.KITKAT)
 class SyncFence(private var fd: Int) : AutoCloseable {
 
+    private val fenceLock = ReentrantLock()
+
     /**
      * Checks if the SyncFence object is valid.
      * @return `true` if it is valid, `false` otherwise
      */
-    fun isValid(): Boolean = fd != -1
+    fun isValid(): Boolean = fenceLock.withLock {
+        fd != -1
+    }
 
     /**
      * Returns the time that the fence signaled in the [CLOCK_MONOTONIC] time domain.
@@ -45,12 +51,22 @@
      */
     // Relies on NDK APIs sync_file_info/sync_file_info_free which were introduced in API level 26
     @RequiresApi(Build.VERSION_CODES.O)
-    fun getSignalTime(): Long =
+    fun getSignalTime(): Long = fenceLock.withLock {
         if (isValid()) {
             nGetSignalTime(fd)
         } else {
             SIGNAL_TIME_INVALID
         }
+    }
+
+    // Accessed through JNI to obtain the dup'ed file descriptor in a thread safe manner
+    private fun dupeFileDescriptor(): Int = fenceLock.withLock {
+        return if (isValid()) {
+            nDup(fd)
+        } else {
+            -1
+        }
+    }
 
     /**
      * Waits for a SyncFence to signal for up to the [timeoutNanos] duration. An invalid SyncFence,
@@ -62,17 +78,19 @@
      * @return `true` if the fence signaled or is not valid, `false` otherwise
      */
     fun await(timeoutNanos: Long): Boolean {
-        if (isValid()) {
-            val timeout: Int
-            if (timeoutNanos < 0) {
-                timeout = -1
+        fenceLock.withLock {
+            if (isValid()) {
+                val timeout: Int
+                if (timeoutNanos < 0) {
+                    timeout = -1
+                } else {
+                    timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
+                }
+                return nWait(fd, timeout)
             } else {
-                timeout = TimeUnit.NANOSECONDS.toMillis(timeoutNanos).toInt()
+                // invalid file descriptors will always return true
+                return true
             }
-            return nWait(fd, timeout)
-        } else {
-            // invalid file descriptors will always return true
-            return true
         }
     }
 
@@ -89,8 +107,12 @@
      * is subsequent calls to [isValid] will return `false`
      */
     override fun close() {
-        nClose(fd)
-        fd = -1
+        fenceLock.withLock {
+            if (isValid()) {
+                nClose(fd)
+                fd = -1
+            }
+        }
     }
 
     protected fun finalize() {
@@ -104,6 +126,12 @@
     private external fun nGetSignalTime(fd: Int): Long
     private external fun nClose(fd: Int)
 
+    /**
+     * Dup the provided file descriptor, this method requires the caller to acquire the corresponding
+     * [fenceLock] before invoking
+     */
+    private external fun nDup(fd: Int): Int
+
     companion object {
 
         /**
diff --git a/health/health-services-client/api/1.0.0-beta02.txt b/health/health-services-client/api/1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/1.0.0-beta02.txt
+++ b/health/health-services-client/api/1.0.0-beta02.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/api_lint.ignore b/health/health-services-client/api/api_lint.ignore
index 4b2826d..844c583 100644
--- a/health/health-services-client/api/api_lint.ignore
+++ b/health/health-services-client/api/api_lint.ignore
@@ -1,4 +1,8 @@
 // Baseline format: 1.0
+DocumentExceptions: androidx.health.services.client.ExerciseClient#updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig):
+    Method ExerciseClient.updateExerciseTypeConfigAsync appears to be throwing kotlin.NotImplementedError; this should be listed in the documentation; see https://android.github.io/kotlin-guides/interop.html#document-exceptions
+
+
 ExecutorRegistration: androidx.health.services.client.ExerciseClient#clearUpdateCallbackAsync(androidx.health.services.client.ExerciseUpdateCallback):
     Registration methods should have overload that accepts delivery Executor: `clearUpdateCallbackAsync`
 
@@ -7,8 +11,6 @@
     Invalid nullability on method `onBind` return. Overrides of unannotated super method cannot be Nullable.
 InvalidNullabilityOverride: androidx.health.services.client.PassiveListenerService#onBind(android.content.Intent) parameter #0:
     Invalid nullability on parameter `intent` in method `onBind`. Parameters of overrides cannot be NonNull if the super parameter is unannotated.
-InvalidNullabilityOverride: androidx.health.services.client.VersionApiService#onBind(android.content.Intent):
-    Invalid nullability on method `onBind` return. Overrides of unannotated super method cannot be Nullable.
 
 
 PairedRegistration: androidx.health.services.client.MeasureClient#registerMeasureCallback(androidx.health.services.client.data.DeltaDataType<?,?>, androidx.health.services.client.MeasureCallback):
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta02.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/restricted_1.0.0-beta02.txt b/health/health-services-client/api/restricted_1.0.0-beta02.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta02.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta02.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index 590e802..d65cfd6 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -17,6 +17,7 @@
     method public void setUpdateCallback(androidx.health.services.client.ExerciseUpdateCallback callback);
     method public void setUpdateCallback(java.util.concurrent.Executor executor, androidx.health.services.client.ExerciseUpdateCallback callback);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> startExerciseAsync(androidx.health.services.client.data.ExerciseConfig configuration);
+    method public default com.google.common.util.concurrent.ListenableFuture<java.lang.Void> updateExerciseTypeConfigAsync(androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig);
   }
 
   public final class ExerciseClientExtensionKt {
@@ -33,6 +34,7 @@
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? removeGoalFromActiveExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseGoal<?> exerciseGoal, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? resumeExercise(androidx.health.services.client.ExerciseClient, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
     method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? startExercise(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseConfig configuration, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=android.os.RemoteException::class) public static suspend Object? updateExerciseTypeConfig(androidx.health.services.client.ExerciseClient, androidx.health.services.client.data.ExerciseTypeConfig exerciseTypeConfig, kotlin.coroutines.Continuation<? super java.lang.Void>) throws android.os.RemoteException;
   }
 
   public interface ExerciseUpdateCallback {
@@ -308,12 +310,17 @@
   }
 
   public final class ExerciseConfig {
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters, optional androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams, optional @FloatRange(from=0.0) float swimmingPoolLengthMeters);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals, optional android.os.Bundle exerciseParams);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled, optional java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
+    ctor public ExerciseConfig(androidx.health.services.client.data.ExerciseType exerciseType, java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes, boolean isAutoPauseAndResumeEnabled, boolean isGpsEnabled);
     method public static androidx.health.services.client.data.ExerciseConfig.Builder builder(androidx.health.services.client.data.ExerciseType exerciseType);
     method public java.util.Set<androidx.health.services.client.data.DataType<?,?>> getDataTypes();
     method public java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> getExerciseGoals();
     method public android.os.Bundle getExerciseParams();
     method public androidx.health.services.client.data.ExerciseType getExerciseType();
+    method public androidx.health.services.client.data.ExerciseTypeConfig? getExerciseTypeConfig();
     method public float getSwimmingPoolLengthMeters();
     method public boolean isAutoPauseAndResumeEnabled();
     method public boolean isGpsEnabled();
@@ -321,6 +328,7 @@
     property public final java.util.List<androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals;
     property public final android.os.Bundle exerciseParams;
     property public final androidx.health.services.client.data.ExerciseType exerciseType;
+    property public final androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig;
     property public final boolean isAutoPauseAndResumeEnabled;
     property public final boolean isGpsEnabled;
     property public final float swimmingPoolLengthMeters;
@@ -334,6 +342,7 @@
     method public androidx.health.services.client.data.ExerciseConfig.Builder setDataTypes(java.util.Set<? extends androidx.health.services.client.data.DataType<?,?>> dataTypes);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseGoals(java.util.List<? extends androidx.health.services.client.data.ExerciseGoal<?>> exerciseGoals);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseParams(android.os.Bundle exerciseParams);
+    method public androidx.health.services.client.data.ExerciseConfig.Builder setExerciseTypeConfig(androidx.health.services.client.data.ExerciseTypeConfig? exerciseTypeConfig);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsAutoPauseAndResumeEnabled(boolean isAutoPauseAndResumeEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setIsGpsEnabled(boolean isGpsEnabled);
     method public androidx.health.services.client.data.ExerciseConfig.Builder setSwimmingPoolLengthMeters(float swimmingPoolLength);
@@ -556,6 +565,17 @@
     property public final boolean supportsAutoPauseAndResume;
   }
 
+  public final class ExerciseTypeConfig {
+    method public static androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+    method public int getGolfShotTrackingPlaceInfo();
+    property public final int golfShotTrackingPlaceInfo;
+    field public static final androidx.health.services.client.data.ExerciseTypeConfig.Companion Companion;
+  }
+
+  public static final class ExerciseTypeConfig.Companion {
+    method public androidx.health.services.client.data.ExerciseTypeConfig createGolfExerciseTypeConfig(int golfShotTrackingPlaceInfo);
+  }
+
   public final class ExerciseUpdate {
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.IntervalDataPoint<?> dataPoint);
     method public java.time.Duration getActiveDurationAtDataPoint(androidx.health.services.client.data.SampleDataPoint<?> dataPoint);
diff --git a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
index c4f225e..87f3b3e 100644
--- a/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/IExerciseApiService.aidl
@@ -25,12 +25,13 @@
 import androidx.health.services.client.impl.request.ExerciseGoalRequest;
 import androidx.health.services.client.impl.request.PrepareExerciseRequest;
 import androidx.health.services.client.impl.request.StartExerciseRequest;
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest;
 import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse;
 
 /**
  * Interface to make ipc calls for health services exercise api.
  *
- * The next method added to the interface should use ID: 15
+ * The next method added to the interface should use ID: 17
  * (this id needs to be incremented for each added method)
  *
  * @hide
@@ -41,7 +42,7 @@
      * method is added.
      *
      */
-    const int API_VERSION = 1;
+    const int API_VERSION = 3;
 
     /**
      * Returns version of this AIDL interface.
@@ -127,4 +128,11 @@
 
     /** Method to flush data metrics. */
     void flushExercise(in FlushRequest request, in IStatusCallback statusCallback) = 12;
+
+    /**
+     * Handles a given request to update an exercise.
+
+     * <p>Added in API version 3.
+     */
+    void updateExerciseTypeConfigForActiveExercise(in UpdateExerciseTypeConfigRequest updateExerciseTypeConfigRequest, IStatusCallback statuscallback) = 16;
 }
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
similarity index 68%
copy from tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
copy to health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
index dfb2685..3b3a016 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/Media.kt
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * 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.
@@ -14,13 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.tv.tvmaterial.samples
+package androidx.health.services.client.impl.request;
 
-import androidx.compose.ui.graphics.Color
-
-data class Media(
-    val id: String,
-    val title: String,
-    val description: String,
-    val backgroundColor: Color
-)
+/** @hide */
+parcelable UpdateExerciseTypeConfigRequest;
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
index fd572f3..311cdcd 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClient.kt
@@ -25,6 +25,7 @@
 import androidx.health.services.client.data.ExerciseState
 import androidx.health.services.client.data.ExerciseEndReason
 import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeConfig
 import androidx.health.services.client.data.ExerciseUpdate
 import androidx.health.services.client.data.WarmUpConfig
 import com.google.common.util.concurrent.ListenableFuture
@@ -281,4 +282,24 @@
      * @return a [ListenableFuture] containing the [ExerciseCapabilities] for this device
      */
     public fun getCapabilitiesAsync(): ListenableFuture<ExerciseCapabilities>
+
+    /**
+     * Updates the configurable exercise type attributes for the current exercise.
+     *
+     * This can be used to update the configurable attributes for the ongoing exercise, as defined
+     * in [ExerciseTypeConfig]. Minimum Exercise API version for this function is 3.
+     *
+     * @param exerciseTypeConfig a configuration containing the new values for the configurable
+     * attributes
+     * @return a [ListenableFuture] that completes when the configuration has been updated.
+     * @throws [NotImplementedError] if there is an existing [ExerciseClient] that has not
+     * implemented this method. Developers should use [ServiceBackedExerciseClient], which is
+     * guaranteed to have this method implemented.
+     * @see ServiceBackedExerciseClient
+     */
+    public fun updateExerciseTypeConfigAsync(
+        exerciseTypeConfig: ExerciseTypeConfig
+    ): ListenableFuture<Void> {
+        throw NotImplementedError()
+    }
 }
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
index 2e54b70..13106ac 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/ExerciseClientExtension.kt
@@ -25,6 +25,7 @@
 import androidx.health.services.client.data.ExerciseInfo
 import androidx.health.services.client.data.ExerciseState
 import androidx.health.services.client.data.ExerciseType
+import androidx.health.services.client.data.ExerciseTypeConfig
 import androidx.health.services.client.data.ExerciseUpdate
 import androidx.health.services.client.data.WarmUpConfig
 
@@ -247,4 +248,20 @@
  * @throws [android.os.RemoteException] if Health Service fails to process the call
  */
 @kotlin.jvm.Throws(android.os.RemoteException::class)
-public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
\ No newline at end of file
+public suspend fun ExerciseClient.getCapabilities() = getCapabilitiesAsync().await()
+
+/**
+ * Updates the configurable exercise type attributes for the current exercise.
+ *
+ * This can be used to update the configurable attributes for the ongoing exercise, as defined
+ * in [ExerciseTypeConfig].
+ *
+ * @param exerciseTypeConfig a configuration containing the new values for the configurable
+ * attributes
+ *
+ * @throws [android.os.RemoteException] if Health Service fails to process the call
+ */
+@kotlin.jvm.Throws(android.os.RemoteException::class)
+public suspend fun ExerciseClient.updateExerciseTypeConfig(
+    exerciseTypeConfig: ExerciseTypeConfig
+) = updateExerciseTypeConfigAsync(exerciseTypeConfig).await()
\ No newline at end of file
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
index 57ee614..a125add 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseConfig.kt
@@ -39,9 +39,11 @@
  * on-going exercise which can be used to pre-populate a new exercise.
  * @property swimmingPoolLengthMeters length (in meters) of the swimming pool, or 0 if not relevant to
  * this exercise
+ * @property exerciseTypeConfig [ExerciseTypeConfig] containing attributes which may be
+ * modified after the exercise has started
  */
 @Suppress("ParcelCreator")
-class ExerciseConfig(
+class ExerciseConfig @JvmOverloads constructor(
     val exerciseType: ExerciseType,
     val dataTypes: Set<DataType<*, *>>,
     val isAutoPauseAndResumeEnabled: Boolean,
@@ -49,6 +51,7 @@
     val exerciseGoals: List<ExerciseGoal<*>> = listOf(),
     val exerciseParams: Bundle = Bundle(),
     @FloatRange(from = 0.0) val swimmingPoolLengthMeters: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED,
+    val exerciseTypeConfig: ExerciseTypeConfig? = null
 ) {
 
     internal constructor(
@@ -65,7 +68,10 @@
           proto.swimmingPoolLength
         } else {
           SWIMMING_POOL_LENGTH_UNSPECIFIED
-        }
+        },
+        if (proto.hasExerciseTypeConfig()) {
+            ExerciseTypeConfig(proto.exerciseTypeConfig)
+        } else null
     )
 
     init {
@@ -98,6 +104,7 @@
         private var exerciseGoals: List<ExerciseGoal<*>> = emptyList()
         private var exerciseParams: Bundle = Bundle.EMPTY
         private var swimmingPoolLength: Float = SWIMMING_POOL_LENGTH_UNSPECIFIED
+        private var exerciseTypeConfig: ExerciseTypeConfig? = null
 
         /**
          * Sets the requested [DataType]s that should be tracked during this exercise. If not
@@ -171,11 +178,22 @@
 
         /** Sets the swimming pool length (in m). */
         @Suppress("MissingGetterMatchingBuilder")
-        public fun setSwimmingPoolLengthMeters(swimmingPoolLength: Float): Builder {
+        fun setSwimmingPoolLengthMeters(swimmingPoolLength: Float): Builder {
           this.swimmingPoolLength = swimmingPoolLength
           return this
         }
 
+        /**
+         * Sets the [ExerciseTypeConfig] which are configurable attributes for the ongoing exercise.
+         *
+         * @param exerciseTypeConfig [ExerciseTypeConfig] specifying active exercise type
+         * configurations
+         */
+        fun setExerciseTypeConfig(exerciseTypeConfig: ExerciseTypeConfig?): Builder {
+            this.exerciseTypeConfig = exerciseTypeConfig
+            return this
+        }
+
         /** Returns the built [ExerciseConfig]. */
         fun build(): ExerciseConfig {
             return ExerciseConfig(
@@ -185,7 +203,8 @@
                 isGpsEnabled,
                 exerciseGoals,
                 exerciseParams,
-                swimmingPoolLength
+                swimmingPoolLength,
+                exerciseTypeConfig
             )
         }
     }
@@ -197,19 +216,24 @@
             "isAutoPauseAndResumeEnabled=$isAutoPauseAndResumeEnabled, " +
             "isGpsEnabled=$isGpsEnabled, " +
             "exerciseGoals=$exerciseGoals, " +
-            "swimmingPoolLengthMeters=$swimmingPoolLengthMeters)"
+            "swimmingPoolLengthMeters=$swimmingPoolLengthMeters, " +
+            "exerciseTypeConfig=$exerciseTypeConfig)"
 
-    internal fun toProto(): DataProto.ExerciseConfig =
-        DataProto.ExerciseConfig.newBuilder()
-            .setExerciseType(exerciseType.toProto())
-            .addAllDataTypes(dataTypes.filter { !it.isAggregate }.map { it.proto })
-            .addAllAggregateDataTypes(dataTypes.filter { it.isAggregate }.map { it.proto })
-            .setIsAutoPauseAndResumeEnabled(isAutoPauseAndResumeEnabled)
-            .setIsGpsUsageEnabled(isGpsEnabled)
-            .addAllExerciseGoals(exerciseGoals.map { it.proto })
-            .setExerciseParams(BundlesUtil.toProto(exerciseParams))
-            .setSwimmingPoolLength(swimmingPoolLengthMeters)
-            .build()
+   internal fun toProto(): DataProto.ExerciseConfig {
+       val builder = DataProto.ExerciseConfig.newBuilder()
+           .setExerciseType(exerciseType.toProto())
+           .addAllDataTypes(dataTypes.filter { !it.isAggregate }.map { it.proto })
+           .addAllAggregateDataTypes(dataTypes.filter { it.isAggregate }.map { it.proto })
+           .setIsAutoPauseAndResumeEnabled(isAutoPauseAndResumeEnabled)
+           .setIsGpsUsageEnabled(isGpsEnabled)
+           .addAllExerciseGoals(exerciseGoals.map { it.proto })
+           .setExerciseParams(BundlesUtil.toProto(exerciseParams))
+           .setSwimmingPoolLength(swimmingPoolLengthMeters)
+       if (exerciseTypeConfig != null) {
+           builder.exerciseTypeConfig = exerciseTypeConfig.toProto()
+       }
+       return builder.build()
+   }
 
     companion object {
         /**
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt
new file mode 100644
index 0000000..3d643b7
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/ExerciseTypeConfig.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+package androidx.health.services.client.data
+
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo.Companion.toProto
+import androidx.health.services.client.proto.DataProto
+
+/**
+ * Configuration attributes for a specific exercise type that may be modified after the exercise has
+ * started.
+ *
+ * @property golfShotTrackingPlaceInfo location where user takes [DataType.GOLF_SHOT_COUNT] during
+ * [ExerciseType.GOLF] activity
+ */
+class ExerciseTypeConfig private constructor(
+  @GolfShotTrackingPlaceInfo
+  val golfShotTrackingPlaceInfo: Int = GolfShotTrackingPlaceInfo.UNSPECIFIED
+) {
+
+  internal constructor(
+    proto: DataProto.ExerciseTypeConfig
+  ) : this (
+    GolfShotTrackingPlaceInfo.fromProto(proto.golfShotTrackingPlaceInfo)
+  )
+
+  internal fun toProto(): DataProto.ExerciseTypeConfig {
+    return DataProto.ExerciseTypeConfig.newBuilder()
+      .setGolfShotTrackingPlaceInfo(golfShotTrackingPlaceInfo.toProto())
+      .build()
+  }
+
+  override fun toString(): String =
+    "ExerciseTypeConfig(golfShotTrackingPlaceInfo=$golfShotTrackingPlaceInfo)"
+
+  companion object {
+    /**
+     * Creates golf-specific exercise type configuration.
+     *
+     * @param golfShotTrackingPlaceInfo location where user takes [DataType.GOLF_SHOT_COUNT] during
+     * [ExerciseType.GOLF] activity
+     *
+     * @return an instance of [ExerciseTypeConfig] with specific [GolfShotTrackingPlaceInfo]
+     */
+    @JvmStatic
+    fun createGolfExerciseTypeConfig(
+      @GolfShotTrackingPlaceInfo golfShotTrackingPlaceInfo: Int
+    ): ExerciseTypeConfig {
+      return ExerciseTypeConfig(golfShotTrackingPlaceInfo)
+    }
+  }
+}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt
new file mode 100644
index 0000000..5cf89cb
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/GolfShotTrackingPlaceInfo.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+package androidx.health.services.client.data
+
+import androidx.annotation.IntDef
+import androidx.health.services.client.proto.DataProto
+import kotlin.annotation.AnnotationRetention.SOURCE
+
+/**
+ * The tracking information for a golf shot used in [ExerciseTypeConfig]. It is the semantic
+ * location of a user while golfing to assist golf swing activity recognition algorithms.
+ *
+ * @hide
+ */
+@Retention(AnnotationRetention.SOURCE)
+@IntDef(
+  GolfShotTrackingPlaceInfo.UNSPECIFIED,
+  GolfShotTrackingPlaceInfo.FAIRWAY,
+  GolfShotTrackingPlaceInfo.PUTTING_GREEN,
+  GolfShotTrackingPlaceInfo.TEE_BOX
+)
+annotation class GolfShotTrackingPlaceInfo {
+  companion object {
+    /** The golf shot is being taken from an unknown place. */
+    const val UNSPECIFIED: Int = 0
+    /** The golf shot is being taken from the fairway. */
+    const val FAIRWAY: Int = 1
+    /** The golf shot is being taken from the putting green. */
+    const val PUTTING_GREEN: Int = 2
+    /** The golf shot is being taken from the tee box area. */
+    const val TEE_BOX: Int = 3
+
+    internal fun @receiver:GolfShotTrackingPlaceInfo Int.toProto():
+      DataProto.GolfShotTrackingPlaceInfoType =
+      when (this) {
+        FAIRWAY -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY
+        PUTTING_GREEN ->
+          DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN
+        TEE_BOX -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX
+        else -> DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED
+      }
+
+    @GolfShotTrackingPlaceInfo
+    internal fun fromProto(proto: DataProto.GolfShotTrackingPlaceInfoType): Int =
+      when (proto) {
+        DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN ->
+          PUTTING_GREEN
+        DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX -> TEE_BOX
+        DataProto.GolfShotTrackingPlaceInfoType.GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY -> FAIRWAY
+        else -> UNSPECIFIED
+      }
+  }
+}
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
index 786f636..9f92ad7 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedExerciseClient.kt
@@ -27,6 +27,7 @@
 import androidx.health.services.client.data.ExerciseConfig
 import androidx.health.services.client.data.ExerciseGoal
 import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseTypeConfig
 import androidx.health.services.client.data.WarmUpConfig
 import androidx.health.services.client.impl.IpcConstants.EXERCISE_API_BIND_ACTION
 import androidx.health.services.client.impl.IpcConstants.SERVICE_PACKAGE_NAME
@@ -42,6 +43,7 @@
 import androidx.health.services.client.impl.request.FlushRequest
 import androidx.health.services.client.impl.request.PrepareExerciseRequest
 import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
 import com.google.common.util.concurrent.FutureCallback
 import com.google.common.util.concurrent.Futures
 import com.google.common.util.concurrent.ListenableFuture
@@ -216,6 +218,20 @@
             ContextCompat.getMainExecutor(context)
         )
 
+    override fun updateExerciseTypeConfigAsync(
+        exerciseTypeConfig: ExerciseTypeConfig
+    ): ListenableFuture<Void> {
+        return executeWithVersionCheck(
+            { service, resultFuture ->
+                service.updateExerciseTypeConfigForActiveExercise(
+                    UpdateExerciseTypeConfigRequest(packageName, exerciseTypeConfig),
+                    StatusCallback(resultFuture)
+                )
+            },
+            3
+        )
+    }
+
     internal companion object {
         internal const val CLIENT = "HealthServicesExerciseClient"
         internal val CLIENT_CONFIGURATION =
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
new file mode 100644
index 0000000..a3820b5
--- /dev/null
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+
+package androidx.health.services.client.impl.request
+
+import android.os.Parcelable
+import androidx.annotation.RestrictTo
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.ProtoParcelable
+import androidx.health.services.client.proto.RequestsProto
+
+/**
+ * Request for updating exercise type configuration in an [ExerciseTypeConfig].
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class UpdateExerciseTypeConfigRequest(
+    val packageName: String,
+    val exerciseTypeConfig: ExerciseTypeConfig,
+) : ProtoParcelable<RequestsProto.UpdateExerciseTypeConfigRequest>() {
+    override val proto: RequestsProto.UpdateExerciseTypeConfigRequest =
+        RequestsProto.UpdateExerciseTypeConfigRequest.newBuilder()
+            .setPackageName(packageName)
+            .setConfig(exerciseTypeConfig.toProto())
+            .build()
+
+    companion object {
+        @JvmField
+        val CREATOR: Parcelable.Creator<UpdateExerciseTypeConfigRequest> = newCreator { bytes ->
+            val proto = RequestsProto.UpdateExerciseTypeConfigRequest.parseFrom(bytes)
+            UpdateExerciseTypeConfigRequest(proto.packageName, ExerciseTypeConfig(proto.config))
+        }
+    }
+}
\ No newline at end of file
diff --git a/health/health-services-client/src/main/proto/data.proto b/health/health-services-client/src/main/proto/data.proto
index 420bbc6..65e2287 100644
--- a/health/health-services-client/src/main/proto/data.proto
+++ b/health/health-services-client/src/main/proto/data.proto
@@ -173,16 +173,31 @@
   reserved 2 to max;  // Next ID
 }
 
+enum GolfShotTrackingPlaceInfoType {
+  GOLF_SHOT_TRACKING_PLACE_INFO_UNSPECIFIED = 0;
+  GOLF_SHOT_TRACKING_PLACE_INFO_FAIRWAY = 1;
+  GOLF_SHOT_TRACKING_PLACE_INFO_PUTTING_GREEN = 2;
+  GOLF_SHOT_TRACKING_PLACE_INFO_TEE_BOX = 3;
+  reserved 4 to max; // Next ID
+}
+
+message ExerciseTypeConfig {
+  optional GolfShotTrackingPlaceInfoType golf_shot_tracking_place_info = 1;
+  reserved 2 to max; // Next ID
+}
+
 message ExerciseConfig {
   optional ExerciseType exercise_type = 1;
   repeated DataType data_types = 2;
   repeated DataType aggregate_data_types = 3;
-  optional bool is_auto_pause_and_resume_enabled = 4; // TODO(sarakato): Move to dynamicExericseConfig
+  optional bool is_auto_pause_and_resume_enabled = 4;
   optional bool is_gps_usage_enabled = 5;
   repeated ExerciseGoal exercise_goals = 6;
   optional Bundle exercise_params = 7; // TODO(b/241015676): Deprecate
   optional float swimming_pool_length = 8;
-  reserved 9 to max;  // Next ID
+  optional ExerciseTypeConfig exercise_type_config = 10;
+  reserved 9;
+  reserved 11 to max;  // Next ID
 }
 
 message ExerciseInfo {
diff --git a/health/health-services-client/src/main/proto/requests.proto b/health/health-services-client/src/main/proto/requests.proto
index d71c3a7..39e0b7c 100644
--- a/health/health-services-client/src/main/proto/requests.proto
+++ b/health/health-services-client/src/main/proto/requests.proto
@@ -116,3 +116,9 @@
   optional ExerciseConfig config = 2;
   reserved 3 to max;  // Next ID
 }
+
+message UpdateExerciseTypeConfigRequest {
+  optional string package_name = 1;
+  optional ExerciseTypeConfig config = 2;
+  reserved 3 to max;  // Next ID
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
index 8d2e322..41be3a0d 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/ExerciseClientTest.kt
@@ -34,7 +34,9 @@
 import androidx.health.services.client.data.ExerciseTrackedStatus
 import androidx.health.services.client.data.ExerciseType
 import androidx.health.services.client.data.ExerciseTypeCapabilities
+import androidx.health.services.client.data.ExerciseTypeConfig
 import androidx.health.services.client.data.ExerciseUpdate
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
 import androidx.health.services.client.data.WarmUpConfig
 import androidx.health.services.client.impl.IExerciseApiService
 import androidx.health.services.client.impl.IExerciseUpdateListener
@@ -51,6 +53,7 @@
 import androidx.health.services.client.impl.request.FlushRequest
 import androidx.health.services.client.impl.request.PrepareExerciseRequest
 import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
 import androidx.health.services.client.impl.response.AvailabilityResponse
 import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
 import androidx.health.services.client.impl.response.ExerciseInfoResponse
@@ -644,6 +647,23 @@
             .isEqualTo(passiveMonitoringCapabilities.toString())
     }
 
+    @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+    @Test
+    fun updateExerciseTypeConfigForActiveExercise() = runTest {
+        service.exerciseConfig = ExerciseConfig.builder(ExerciseType.GOLF).build()
+        val exerciseTypeConfig =
+            ExerciseTypeConfig.createGolfExerciseTypeConfig(GolfShotTrackingPlaceInfo.FAIRWAY)
+        val request =
+            UpdateExerciseTypeConfigRequest(
+                CLIENT_CONFIGURATION.servicePackageName, exerciseTypeConfig
+            )
+        val statusCallback = IStatusCallback.Default()
+
+        service.updateExerciseTypeConfigForActiveExercise(request, statusCallback)
+
+        Truth.assertThat(service.exerciseConfig?.exerciseTypeConfig).isEqualTo(exerciseTypeConfig)
+    }
+
     class FakeExerciseUpdateCallback : ExerciseUpdateCallback {
         val availabilities = mutableMapOf<DataType<*, *>, Availability>()
         val registrationFailureThrowables = mutableListOf<Throwable>()
@@ -855,6 +875,18 @@
             statusCallbackAction.invoke(statusCallback)
         }
 
+        override fun updateExerciseTypeConfigForActiveExercise(
+            updateExerciseTypeConfigRequest: UpdateExerciseTypeConfigRequest,
+            statuscallback: IStatusCallback
+        ) {
+            val newExerciseTypeConfig = updateExerciseTypeConfigRequest.exerciseTypeConfig
+            val newExerciseConfig =
+                ExerciseConfig.builder(
+                    exerciseConfig!!.exerciseType
+                ).setExerciseTypeConfig(newExerciseTypeConfig).build()
+            this.exerciseConfig = newExerciseConfig
+        }
+
         fun setException() {
             throwException = true
         }
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
index 8567fa6..c23763f 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseConfigTest.kt
@@ -43,6 +43,8 @@
                     DataTypeCondition(DISTANCE_TOTAL, 150.0, GREATER_THAN)
                 ),
             ),
+            exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+                GolfShotTrackingPlaceInfo.FAIRWAY)
         ).toProto()
 
         val config = ExerciseConfig(proto)
@@ -57,6 +59,51 @@
         assertThat(config.exerciseGoals[1].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
         assertThat(config.exerciseGoals[1].dataTypeCondition.threshold).isEqualTo(150.0)
         assertThat(config.exerciseGoals[1].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+        assertThat((config.exerciseTypeConfig)?.golfShotTrackingPlaceInfo).isEqualTo(
+         GolfShotTrackingPlaceInfo.FAIRWAY)
+    }
+
+    @Test
+    fun exerciseTypeConfigNull_protoRoundTrip() {
+        val proto = ExerciseConfig(
+            ExerciseType.RUNNING,
+            setOf(LOCATION, DISTANCE_TOTAL, HEART_RATE_BPM),
+            isAutoPauseAndResumeEnabled = true,
+            isGpsEnabled = true,
+            exerciseGoals = listOf(
+                ExerciseGoal.createOneTimeGoal(
+                    DataTypeCondition(DISTANCE_TOTAL, 50.0, GREATER_THAN)
+                ),
+                ExerciseGoal.createOneTimeGoal(
+                    DataTypeCondition(DISTANCE_TOTAL, 150.0, GREATER_THAN)
+                ),
+            )
+        ).toProto()
+
+        val config = ExerciseConfig(proto)
+
+        assertThat(config.exerciseType).isEqualTo(ExerciseType.RUNNING)
+        assertThat(config.dataTypes).containsExactly(LOCATION, HEART_RATE_BPM, DISTANCE_TOTAL)
+        assertThat(config.isAutoPauseAndResumeEnabled).isEqualTo(true)
+        assertThat(config.isGpsEnabled).isEqualTo(true)
+        assertThat(config.exerciseGoals[0].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
+        assertThat(config.exerciseGoals[0].dataTypeCondition.threshold).isEqualTo(50.0)
+        assertThat(config.exerciseGoals[0].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+        assertThat(config.exerciseGoals[1].dataTypeCondition.dataType).isEqualTo(DISTANCE_TOTAL)
+        assertThat(config.exerciseGoals[1].dataTypeCondition.threshold).isEqualTo(150.0)
+        assertThat(config.exerciseGoals[1].dataTypeCondition.comparisonType).isEqualTo(GREATER_THAN)
+        assertThat((config.exerciseTypeConfig)).isNull()
+    }
+
+    @Test
+    fun builder_exerciseTypeConfigNull() {
+        val exerciseTypeConfigNotSetExerciseConfig =
+            ExerciseConfig.builder(ExerciseType.UNKNOWN).build()
+        val setNullExerciseTypeConfigExerciseConfig =
+            ExerciseConfig.builder(ExerciseType.UNKNOWN).setExerciseTypeConfig(null).build()
+
+        assertThat(exerciseTypeConfigNotSetExerciseConfig.exerciseTypeConfig).isNull()
+        assertThat(setNullExerciseTypeConfigExerciseConfig.exerciseTypeConfig).isNull()
     }
 
     @Test
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt
new file mode 100644
index 0000000..10cf21d
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseTypeConfigTest.kt
@@ -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.health.services.client.data
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class ExerciseTypeConfigTest {
+    @Test
+    fun protoRoundTrip() {
+        val proto = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+            GolfShotTrackingPlaceInfo.FAIRWAY).toProto()
+        val config = ExerciseTypeConfig(proto)
+
+        assertThat(config.golfShotTrackingPlaceInfo).isEqualTo(GolfShotTrackingPlaceInfo.FAIRWAY)
+    }
+
+    @Test
+    fun createGolfExerciseTypeConfigFromFactoryMethod() {
+        val golfExerciseStyleConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+            GolfShotTrackingPlaceInfo.FAIRWAY)
+
+        assertThat(golfExerciseStyleConfig.golfShotTrackingPlaceInfo).isEqualTo(
+            GolfShotTrackingPlaceInfo.FAIRWAY)
+    }
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
index e5c0a69..b4b0537 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/data/ExerciseUpdateTest.kt
@@ -59,9 +59,12 @@
             exerciseConfig = ExerciseConfig(
                 WALKING,
                 setOf(CALORIES_TOTAL),
-                isAutoPauseAndResumeEnabled = true,
+                isAutoPauseAndResumeEnabled = false,
                 isGpsEnabled = false,
-                exerciseGoals = listOf(goal)
+                exerciseGoals = listOf(goal),
+                exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+                    GolfShotTrackingPlaceInfo.FAIRWAY
+                )
             ),
             activeDurationCheckpoint = ActiveDurationCheckpoint(42.instant(), 30.duration()),
             updateDurationFromBoot = 42.duration(),
@@ -81,7 +84,62 @@
             .isEqualTo(CALORIES_TOTAL)
         assertThat(markerSummary.achievedGoal.dataTypeCondition.dataType).isEqualTo(CALORIES_TOTAL)
         assertThat(update.exerciseConfig!!.exerciseType).isEqualTo(WALKING)
+        assertThat(
+            update.exerciseConfig!!.exerciseTypeConfig!!.golfShotTrackingPlaceInfo).isEqualTo(
+            GolfShotTrackingPlaceInfo.FAIRWAY)
         assertThat(update.activeDurationCheckpoint!!.activeDuration).isEqualTo(30.duration())
         assertThat(update.exerciseStateInfo.state).isEqualTo(ExerciseState.ACTIVE)
     }
-}
\ No newline at end of file
+
+    @Test
+    fun exerciseTypeConfigNull_protoRoundTrip() {
+        val goal = createOneTimeGoal(
+            DataTypeCondition(CALORIES_TOTAL, 125.0, GREATER_THAN_OR_EQUAL)
+        )
+        val proto = ExerciseUpdate(
+            latestMetrics = DataPointContainer(
+                listOf(DataPoints.calories(130.0, 15.duration(), 35.duration()))
+            ),
+            latestAchievedGoals = setOf(goal),
+            latestMilestoneMarkerSummaries = setOf(
+                MilestoneMarkerSummary(
+                    15.instant(),
+                    40.instant(),
+                    20.duration(),
+                    goal,
+                    DataPointContainer(
+                        listOf(DataPoints.calories(130.0, 15.duration(), 35.duration()))
+                    )
+                )
+            ),
+            exerciseStateInfo = ExerciseStateInfo(ExerciseState.ACTIVE, ExerciseEndReason.UNKNOWN),
+            exerciseConfig = ExerciseConfig(
+                WALKING,
+                setOf(CALORIES_TOTAL),
+                isAutoPauseAndResumeEnabled = false,
+                isGpsEnabled = false,
+                exerciseGoals = listOf(goal),
+            ),
+            activeDurationCheckpoint = ActiveDurationCheckpoint(42.instant(), 30.duration()),
+            updateDurationFromBoot = 42.duration(),
+            startTime = 10.instant()
+        ).proto
+
+        val update = ExerciseUpdate(proto)
+
+        val caloriesDataPoint = update.latestMetrics.getData(DataType.CALORIES).first()
+        val markerSummary = update.latestMilestoneMarkerSummaries.first()
+        assertThat(update.startTime).isEqualTo(10.instant())
+        assertThat(update.getUpdateDurationFromBoot()).isEqualTo(42.duration())
+        assertThat(caloriesDataPoint.value).isEqualTo(130.0)
+        assertThat(caloriesDataPoint.startDurationFromBoot).isEqualTo(15.duration())
+        assertThat(caloriesDataPoint.endDurationFromBoot).isEqualTo(35.duration())
+        assertThat(update.latestAchievedGoals.first().dataTypeCondition.dataType)
+            .isEqualTo(CALORIES_TOTAL)
+        assertThat(markerSummary.achievedGoal.dataTypeCondition.dataType).isEqualTo(CALORIES_TOTAL)
+        assertThat(update.exerciseConfig!!.exerciseType).isEqualTo(WALKING)
+        assertThat(update.exerciseConfig!!.exerciseTypeConfig).isNull()
+        assertThat(update.activeDurationCheckpoint!!.activeDuration).isEqualTo(30.duration())
+        assertThat(update.exerciseStateInfo.state).isEqualTo(ExerciseState.ACTIVE)
+    }
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
index 32e90cf..b519939 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedExerciseClientTest.kt
@@ -31,6 +31,8 @@
 import androidx.health.services.client.data.ExerciseType
 import androidx.health.services.client.data.ExerciseUpdate
 import androidx.health.services.client.data.WarmUpConfig
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
 import androidx.health.services.client.impl.event.ExerciseUpdateListenerEvent
 import androidx.health.services.client.impl.internal.IExerciseInfoCallback
 import androidx.health.services.client.impl.internal.IStatusCallback
@@ -41,6 +43,7 @@
 import androidx.health.services.client.impl.request.FlushRequest
 import androidx.health.services.client.impl.request.PrepareExerciseRequest
 import androidx.health.services.client.impl.request.StartExerciseRequest
+import androidx.health.services.client.impl.request.UpdateExerciseTypeConfigRequest
 import androidx.health.services.client.impl.response.AvailabilityResponse
 import androidx.health.services.client.impl.response.ExerciseCapabilitiesResponse
 import androidx.test.core.app.ApplicationProvider
@@ -107,7 +110,7 @@
             ExerciseType.WALKING,
             setOf(HEART_RATE_BPM),
             isAutoPauseAndResumeEnabled = false,
-            isGpsEnabled = false
+            isGpsEnabled = false,
         )
         val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
             AvailabilityResponse(HEART_RATE_BPM, ACQUIRING)
@@ -171,6 +174,34 @@
     }
 
     @Test
+    fun withExerciseTypeConfig_statsAndSample_startExercise() {
+        val exerciseConfig = ExerciseConfig(
+            ExerciseType.GOLF,
+            setOf(HEART_RATE_BPM, HEART_RATE_BPM_STATS),
+            isAutoPauseAndResumeEnabled = false,
+            isGpsEnabled = false,
+            exerciseTypeConfig = ExerciseTypeConfig.createGolfExerciseTypeConfig(
+                GolfShotTrackingPlaceInfo.FAIRWAY
+            )
+        )
+        val availabilityEvent = ExerciseUpdateListenerEvent.createAvailabilityUpdateEvent(
+            // Currently the proto form of HEART_RATE_BPM and HEART_RATE_BPM_STATS is identical. The
+            // APK doesn't know about _STATS, so pass the sample type to mimic that behavior.
+            AvailabilityResponse(HEART_RATE_BPM, ACQUIRING)
+        )
+        client.setUpdateCallback(callback)
+        client.startExerciseAsync(exerciseConfig)
+        shadowOf(getMainLooper()).idle()
+
+        fakeService.listener!!.onExerciseUpdateListenerEvent(availabilityEvent)
+        shadowOf(getMainLooper()).idle()
+
+        // When both the sample type and stat type are requested, both should be notified
+        assertThat(callback.availabilities).containsEntry(HEART_RATE_BPM, ACQUIRING)
+        assertThat(callback.availabilities).containsEntry(HEART_RATE_BPM_STATS, ACQUIRING)
+    }
+
+    @Test
     fun dataTypeInAvailabilityCallbackShouldMatchRequested_justSampleType_prepare() {
         val warmUpConfig = WarmUpConfig(
             ExerciseType.WALKING,
@@ -302,5 +333,12 @@
         override fun flushExercise(request: FlushRequest?, statusCallback: IStatusCallback?) {
             throw NotImplementedError()
         }
+
+        override fun updateExerciseTypeConfigForActiveExercise(
+            updateExerciseTypeConfigRequest: UpdateExerciseTypeConfigRequest?,
+            statuscallback: IStatusCallback?
+        ) {
+            throw NotImplementedError()
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt
new file mode 100644
index 0000000..1e2a23c
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequestTest.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.health.services.client.impl.request
+
+import android.os.Parcel
+import androidx.health.services.client.data.ExerciseTypeConfig
+import androidx.health.services.client.data.GolfShotTrackingPlaceInfo
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class UpdateExerciseTypeConfigRequestTest {
+    @Test
+    fun parcelableRoundTrip() {
+        val request =
+            UpdateExerciseTypeConfigRequest(
+                "package",
+                ExerciseTypeConfig.createGolfExerciseTypeConfig(GolfShotTrackingPlaceInfo.TEE_BOX)
+            )
+        val parcel = Parcel.obtain()
+
+        request.writeToParcel(parcel, 0)
+        parcel.setDataPosition(0)
+        val fromParcel = UpdateExerciseTypeConfigRequest.CREATOR.createFromParcel(parcel)
+
+        Truth.assertThat(request).isEqualTo(fromParcel)
+    }
+}
\ No newline at end of file
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
index 31442ee..39e1c14 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/MotionEventPredictor.java
@@ -28,9 +28,9 @@
  * app; a motion predictor is a utility that provides predicted {@link android.view.MotionEvent}
  * based on the previously received ones. Obtain a new predictor instance using
  * {@link #newInstance(android.view.View)}; put the motion events you receive into it with
- * {@link #recordMovement(android.view.MotionEvent)}, and call {@link #predict()} to retrieve the
+ * {@link #record(android.view.MotionEvent)}, and call {@link #predict()} to retrieve the
  * predicted  {@link android.view.MotionEvent} that would occur at the moment the next frame is
- * rendered on the display. Once no more predictions are needed, call {@link #dispose()} to stop it
+ * rendered on the display. Once no more predictions are needed, call {@link #close()} to stop it
  * and clean up resources.
  */
 public interface MotionEventPredictor extends AutoCloseable {
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
index df88594..106c01e4 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/KalmanMotionEventPredictor.java
@@ -30,11 +30,9 @@
  */
 @RestrictTo(LIBRARY)
 public class KalmanMotionEventPredictor implements MotionEventPredictor {
-    private MultiPointerPredictor mMultiPointerPredictor;
-    private boolean mClosed = false;
+    private MultiPointerPredictor mMultiPointerPredictor = new MultiPointerPredictor();
 
     public KalmanMotionEventPredictor() {
-        mMultiPointerPredictor = new MultiPointerPredictor();
         // 1 may seem arbitrary, but this basically tells the predictor to
         // just predict the next MotionEvent.
         // This will need to change as we want to build a prediction depending
@@ -44,13 +42,16 @@
 
     @Override
     public void record(@NonNull MotionEvent event) {
+        if (mMultiPointerPredictor == null) {
+            return;
+        }
         mMultiPointerPredictor.onTouchEvent(event);
     }
 
     @Nullable
     @Override
     public MotionEvent predict() {
-        if (mClosed) {
+        if (mMultiPointerPredictor == null) {
             return null;
         }
         return mMultiPointerPredictor.predict();
@@ -58,6 +59,6 @@
 
     @Override
     public void close() {
-        mClosed = true;
+        mMultiPointerPredictor = null;
     }
 }
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
index 1b0b3cd..f5929e0 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/MultiPointerPredictor.java
@@ -36,7 +36,7 @@
     private static final String TAG = "MultiPointerPredictor";
     private static final boolean DEBUG_PREDICTION = Log.isLoggable(TAG, Log.DEBUG);
 
-    private SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
+    private final SparseArray<SinglePointerPredictor> mPredictorMap = new SparseArray<>();
     private int mPredictionTargetMs = 0;
     private int mReportRateMs = 0;
 
diff --git a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
index 6a80269..65d828d 100644
--- a/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
+++ b/input/input-motionprediction/src/main/java/androidx/input/motionprediction/kalman/PointerKalmanFilter.java
@@ -30,14 +30,14 @@
  */
 @RestrictTo(LIBRARY)
 public class PointerKalmanFilter {
-    private KalmanFilter mXKalman;
-    private KalmanFilter mYKalman;
-    private KalmanFilter mPKalman;
+    private final KalmanFilter mXKalman;
+    private final KalmanFilter mYKalman;
+    private final KalmanFilter mPKalman;
 
-    private DVector2 mPosition = new DVector2();
-    private DVector2 mVelocity = new DVector2();
-    private DVector2 mAcceleration = new DVector2();
-    private DVector2 mJank = new DVector2();
+    private final DVector2 mPosition = new DVector2();
+    private final DVector2 mVelocity = new DVector2();
+    private final DVector2 mAcceleration = new DVector2();
+    private final DVector2 mJank = new DVector2();
     private double mPressure = 0;
     private double mPressureChange = 0;
 
@@ -46,9 +46,9 @@
 
     private int mNumIterations = 0;
 
-    private Matrix mNewX = new Matrix(1, 1);
-    private Matrix mNewY = new Matrix(1, 1);
-    private Matrix mNewP = new Matrix(1, 1);
+    private final Matrix mNewX = new Matrix(1, 1);
+    private final Matrix mNewY = new Matrix(1, 1);
+    private final Matrix mNewP = new Matrix(1, 1);
 
     /**
      * @param sigmaProcess lower value = more filtering
diff --git a/libraryversions.toml b/libraryversions.toml
index 5147f09..c5f8065 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -3,6 +3,7 @@
 ADS_IDENTIFIER = "1.0.0-alpha05"
 ANNOTATION = "1.6.0-alpha01"
 ANNOTATION_EXPERIMENTAL = "1.4.0-alpha01"
+APPACTIONS_INTERACTION = "1.0.0-alpha01"
 APPCOMPAT = "1.7.0-alpha02"
 APPSEARCH = "1.1.0-alpha03"
 ARCH_CORE = "2.2.0-alpha01"
@@ -13,15 +14,15 @@
 BLUETOOTH = "1.0.0-alpha01"
 BROWSER = "1.5.0-alpha02"
 BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.3.0-alpha01"
+CAMERA = "1.3.0-alpha02"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
-CAR_APP = "1.3.0-rc01"
+CAR_APP = "1.4.0-alpha01"
 COLLECTION = "1.3.0-alpha03"
 COLLECTION_KMP = "1.3.0-dev01"
-COMPOSE = "1.4.0-alpha02"
-COMPOSE_COMPILER = "1.4.0-alpha01"
-COMPOSE_MATERIAL3 = "1.1.0-alpha02"
+COMPOSE = "1.4.0-alpha03"
+COMPOSE_COMPILER = "1.4.0-alpha02"
+COMPOSE_MATERIAL3 = "1.1.0-alpha03"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha02"
 CONSTRAINTLAYOUT = "2.2.0-alpha05"
 CONSTRAINTLAYOUT_COMPOSE = "1.1.0-alpha05"
@@ -59,7 +60,7 @@
 FUTURES = "1.2.0-alpha01"
 GLANCE = "1.0.0-alpha06"
 GLANCE_TEMPLATE = "1.0.0-alpha01"
-GRAPHICS = "1.0.0-alpha02"
+GRAPHICS = "1.0.0-alpha03"
 GRIDLAYOUT = "1.1.0-alpha01"
 HEALTH_CONNECT = "1.0.0-alpha08"
 HEALTH_SERVICES_CLIENT = "1.0.0-beta02"
@@ -82,7 +83,7 @@
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-alpha01"
 MEDIA2 = "1.3.0-alpha01"
-MEDIAROUTER = "1.4.0-alpha01"
+MEDIAROUTER = "1.4.0-alpha02"
 METRICS = "1.0.0-alpha04"
 NAVIGATION = "2.6.0-alpha04"
 PAGING = "3.2.0-alpha04"
@@ -92,7 +93,7 @@
 PREFERENCE = "1.3.0-alpha01"
 PRINT = "1.1.0-beta01"
 PRIVACYSANDBOX_SDKRUNTIME = "1.0.0-alpha01"
-PRIVACYSANDBOX_TOOLS = "1.0.0-alpha01"
+PRIVACYSANDBOX_TOOLS = "1.0.0-alpha02"
 PRIVACYSANDBOX_UI = "1.0.0-alpha01"
 PROFILEINSTALLER = "1.3.0-alpha02"
 RECOMMENDATION = "1.1.0-alpha01"
@@ -124,7 +125,7 @@
 TRACING = "1.2.0-alpha02"
 TRACING_PERFETTO = "1.0.0-alpha07"
 TRANSITION = "1.5.0-alpha01"
-TV = "1.0.0-alpha02"
+TV = "1.0.0-alpha03"
 TVPROVIDER = "1.1.0-alpha02"
 VECTORDRAWABLE = "1.2.0-beta02"
 VECTORDRAWABLE_ANIMATED = "1.2.0-beta01"
@@ -140,17 +141,18 @@
 WEAR_PHONE_INTERACTIONS = "1.1.0-alpha04"
 WEAR_REMOTE_INTERACTIONS = "1.1.0-alpha01"
 WEAR_TILES = "1.2.0-alpha01"
-WEAR_WATCHFACE = "1.2.0-alpha04"
-WEBKIT = "1.6.0-alpha03"
-WINDOW = "1.1.0-alpha04"
+WEAR_WATCHFACE = "1.2.0-alpha05"
+WEBKIT = "1.6.0-alpha04"
+WINDOW = "1.1.0-alpha05"
 WINDOW_EXTENSIONS = "1.1.0-alpha02"
 WINDOW_SIDECAR = "1.0.0-rc01"
-WORK = "2.8.0-beta02"
+WORK = "2.9.0-alpha01"
 
 [groups]
 ACTIVITY = { group = "androidx.activity", atomicGroupVersion = "versions.ACTIVITY" }
 ADS = { group = "androidx.ads" }
 ANNOTATION = { group = "androidx.annotation" }
+APPACTIONS_INTERACTION = { group = "androidx.appactions.interaction", atomicGroupVersion = "versions.APPACTIONS_INTERACTION" }
 APPCOMPAT = { group = "androidx.appcompat", atomicGroupVersion = "versions.APPCOMPAT" }
 APPSEARCH = { group = "androidx.appsearch", atomicGroupVersion = "versions.APPSEARCH" }
 ARCH_CORE = { group = "androidx.arch.core", atomicGroupVersion = "versions.ARCH_CORE" }
diff --git a/lifecycle/lifecycle-common/api/current.txt b/lifecycle/lifecycle-common/api/current.txt
index 2358580..53075b6 100644
--- a/lifecycle/lifecycle-common/api/current.txt
+++ b/lifecycle/lifecycle-common/api/current.txt
@@ -2,12 +2,12 @@
 package androidx.lifecycle {
 
   public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
-    method public default void onCreate(androidx.lifecycle.LifecycleOwner);
-    method public default void onDestroy(androidx.lifecycle.LifecycleOwner);
-    method public default void onPause(androidx.lifecycle.LifecycleOwner);
-    method public default void onResume(androidx.lifecycle.LifecycleOwner);
-    method public default void onStart(androidx.lifecycle.LifecycleOwner);
-    method public default void onStop(androidx.lifecycle.LifecycleOwner);
+    method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
   }
 
   public abstract class Lifecycle {
diff --git a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
index 2358580..53075b6 100644
--- a/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-common/api/public_plus_experimental_current.txt
@@ -2,12 +2,12 @@
 package androidx.lifecycle {
 
   public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
-    method public default void onCreate(androidx.lifecycle.LifecycleOwner);
-    method public default void onDestroy(androidx.lifecycle.LifecycleOwner);
-    method public default void onPause(androidx.lifecycle.LifecycleOwner);
-    method public default void onResume(androidx.lifecycle.LifecycleOwner);
-    method public default void onStart(androidx.lifecycle.LifecycleOwner);
-    method public default void onStop(androidx.lifecycle.LifecycleOwner);
+    method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
   }
 
   public abstract class Lifecycle {
diff --git a/lifecycle/lifecycle-common/api/restricted_current.txt b/lifecycle/lifecycle-common/api/restricted_current.txt
index c4b3e23..86f017b 100644
--- a/lifecycle/lifecycle-common/api/restricted_current.txt
+++ b/lifecycle/lifecycle-common/api/restricted_current.txt
@@ -2,12 +2,12 @@
 package androidx.lifecycle {
 
   public interface DefaultLifecycleObserver extends androidx.lifecycle.LifecycleObserver {
-    method public default void onCreate(androidx.lifecycle.LifecycleOwner);
-    method public default void onDestroy(androidx.lifecycle.LifecycleOwner);
-    method public default void onPause(androidx.lifecycle.LifecycleOwner);
-    method public default void onResume(androidx.lifecycle.LifecycleOwner);
-    method public default void onStart(androidx.lifecycle.LifecycleOwner);
-    method public default void onStop(androidx.lifecycle.LifecycleOwner);
+    method public default void onCreate(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onDestroy(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onPause(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onResume(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStart(androidx.lifecycle.LifecycleOwner owner);
+    method public default void onStop(androidx.lifecycle.LifecycleOwner owner);
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public interface GeneratedAdapter {
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 061f3b0..71c6829 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -44,3 +44,11 @@
     inceptionYear = "2017"
     description = "Android Lifecycle-Common"
 }
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=all",
+        ]
+    }
+}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.java
deleted file mode 100644
index 63b0688..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.java
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.lifecycle;
-
-import androidx.annotation.NonNull;
-
-/**
- * Callback interface for listening to {@link LifecycleOwner} state changes.
- * If a class implements both this interface and {@link LifecycleEventObserver}, then
- * methods of {@code DefaultLifecycleObserver} will be called first, and then followed by the call
- * of {@link LifecycleEventObserver#onStateChanged(LifecycleOwner, Lifecycle.Event)}
- * <p>
- * If a class implements this interface and in the same time uses {@link OnLifecycleEvent}, then
- * annotations will be ignored.
- */
-@SuppressWarnings("unused")
-public interface DefaultLifecycleObserver extends LifecycleObserver {
-
-    /**
-     * Notifies that {@code ON_CREATE} event occurred.
-     * <p>
-     * This method will be called after the {@link LifecycleOwner}'s {@code onCreate}
-     * method returns.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onCreate(@NonNull LifecycleOwner owner) {
-    }
-
-    /**
-     * Notifies that {@code ON_START} event occurred.
-     * <p>
-     * This method will be called after the {@link LifecycleOwner}'s {@code onStart} method returns.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onStart(@NonNull LifecycleOwner owner) {
-    }
-
-    /**
-     * Notifies that {@code ON_RESUME} event occurred.
-     * <p>
-     * This method will be called after the {@link LifecycleOwner}'s {@code onResume}
-     * method returns.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onResume(@NonNull LifecycleOwner owner) {
-    }
-
-    /**
-     * Notifies that {@code ON_PAUSE} event occurred.
-     * <p>
-     * This method will be called before the {@link LifecycleOwner}'s {@code onPause} method
-     * is called.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onPause(@NonNull LifecycleOwner owner) {
-    }
-
-    /**
-     * Notifies that {@code ON_STOP} event occurred.
-     * <p>
-     * This method will be called before the {@link LifecycleOwner}'s {@code onStop} method
-     * is called.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onStop(@NonNull LifecycleOwner owner) {
-    }
-
-    /**
-     * Notifies that {@code ON_DESTROY} event occurred.
-     * <p>
-     * This method will be called before the {@link LifecycleOwner}'s {@code onDestroy} method
-     * is called.
-     *
-     * @param owner the component, whose state was changed
-     */
-    default void onDestroy(@NonNull LifecycleOwner owner) {
-    }
-}
-
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt
new file mode 100644
index 0000000..c0eef46
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/DefaultLifecycleObserver.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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.lifecycle
+
+/**
+ * Callback interface for listening to [LifecycleOwner] state changes.
+ * If a class implements both this interface and [LifecycleEventObserver], then
+ * methods of `DefaultLifecycleObserver` will be called first, and then followed by the call
+ * of [LifecycleEventObserver.onStateChanged]
+ *
+ *
+ * If a class implements this interface and in the same time uses [OnLifecycleEvent], then
+ * annotations will be ignored.
+ */
+public interface DefaultLifecycleObserver : LifecycleObserver {
+    /**
+     * Notifies that `ON_CREATE` event occurred.
+     *
+     *
+     * This method will be called after the [LifecycleOwner]'s `onCreate`
+     * method returns.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onCreate(owner: LifecycleOwner) {}
+
+    /**
+     * Notifies that `ON_START` event occurred.
+     *
+     *
+     * This method will be called after the [LifecycleOwner]'s `onStart` method returns.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onStart(owner: LifecycleOwner) {}
+
+    /**
+     * Notifies that `ON_RESUME` event occurred.
+     *
+     *
+     * This method will be called after the [LifecycleOwner]'s `onResume`
+     * method returns.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onResume(owner: LifecycleOwner) {}
+
+    /**
+     * Notifies that `ON_PAUSE` event occurred.
+     *
+     *
+     * This method will be called before the [LifecycleOwner]'s `onPause` method
+     * is called.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onPause(owner: LifecycleOwner) {}
+
+    /**
+     * Notifies that `ON_STOP` event occurred.
+     *
+     *
+     * This method will be called before the [LifecycleOwner]'s `onStop` method
+     * is called.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onStop(owner: LifecycleOwner) {}
+
+    /**
+     * Notifies that `ON_DESTROY` event occurred.
+     *
+     *
+     * This method will be called before the [LifecycleOwner]'s `onDestroy` method
+     * is called.
+     *
+     * @param owner the component, whose state was changed
+     */
+    public fun onDestroy(owner: LifecycleOwner) {}
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-extensions/build.gradle b/lifecycle/lifecycle-extensions/build.gradle
index cf246c3..92c8382 100644
--- a/lifecycle/lifecycle-extensions/build.gradle
+++ b/lifecycle/lifecycle-extensions/build.gradle
@@ -45,6 +45,7 @@
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testRules)
     androidTestImplementation(libs.espressoCore)
+    androidTestImplementation(libs.multidex)
     androidTestImplementation("androidx.appcompat:appcompat:1.1.0")
     androidTestImplementation(project(":internal-testutils-runtime"))
 }
@@ -60,5 +61,8 @@
 }
 
 android {
+    defaultConfig {
+        multiDexEnabled true
+    }
     namespace "androidx.lifecycle.extensions"
 }
diff --git a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
index 363fb58..5bebc62 100644
--- a/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
+++ b/lifecycle/lifecycle-runtime-compose/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.lifecycle.compose {
 
-  @kotlin.RequiresOptIn(message="This is an experimental Lifecycle Compose API.") @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalLifecycleComposeApi {
+  @kotlin.RequiresOptIn(message="This is an experimental Lifecycle Compose API.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface ExperimentalLifecycleComposeApi {
   }
 
   public final class FlowExtKt {
diff --git a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
index 5b17e2f..3f54e9a 100644
--- a/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
+++ b/lifecycle/lifecycle-runtime-compose/src/main/java/androidx/lifecycle/compose/ExperimentalLifecycleComposeApi.kt
@@ -24,4 +24,5 @@
     AnnotationTarget.FIELD,
     AnnotationTarget.PROPERTY_GETTER,
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalLifecycleComposeApi
diff --git a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
index a0955d6..6c9d4b8 100644
--- a/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
+++ b/lifecycle/lifecycle-viewmodel-savedstate/src/main/java/androidx/lifecycle/SavedStateHandle.kt
@@ -27,6 +27,7 @@
 import androidx.core.os.bundleOf
 import androidx.savedstate.SavedStateRegistry
 import java.io.Serializable
+import java.lang.ClassCastException
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -220,8 +221,15 @@
      */
     @MainThread
     operator fun <T> get(key: String): T? {
-        @Suppress("UNCHECKED_CAST")
-        return regular[key] as T?
+        return try {
+            @Suppress("UNCHECKED_CAST")
+            regular[key] as T?
+        } catch (e: ClassCastException) {
+            // Instead of failing on ClassCastException, we remove the value from the
+            // SavedStateHandle and return null.
+            remove<T>(key)
+            null
+        }
     }
 
     /**
diff --git a/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
index 07806af..5733f15 100644
--- a/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/ExperimentalPropertyAnnotationDetectorTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.build.lint
 
+import com.android.tools.lint.checks.infrastructure.TestMode
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -121,7 +122,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -199,7 +206,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -278,7 +291,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -371,7 +390,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -405,7 +430,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -443,7 +474,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
@@ -478,7 +515,13 @@
         """
         /* ktlint-enable max-line-length */
 
-        check(*input)
+        lint()
+            .files(
+                *stubs,
+                *input
+            )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257294309
+            .run()
             .expect(expected)
             .expectFixDiffs(expectedFixDiffs)
     }
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 6f87eac..33cb3fe 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -26,15 +26,18 @@
     buildTypes.all {
         consumerProguardFiles "proguard-rules.pro"
     }
+    defaultConfig {
+        multiDexEnabled true
+    }
     namespace "androidx.navigation.common"
 }
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    api(project(":lifecycle:lifecycle-runtime-ktx"))
+    api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.savedstate:savedstate-ktx:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
 
@@ -56,6 +59,7 @@
     androidTestImplementation(libs.mockitoCore, excludes.bytebuddy)
     androidTestImplementation(libs.dexmakerMockito, excludes.bytebuddy)
     androidTestImplementation(libs.kotlinStdlib)
+    androidTestImplementation(libs.multidex)
 
     lintPublish(project(':navigation:navigation-common-lint'))
 }
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 322181e..8495d53 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -33,13 +33,13 @@
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
+    api(project(":lifecycle:lifecycle-viewmodel-compose"))
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
index 2619c96..b6215a9 100644
--- a/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
+++ b/navigation/navigation-runtime-lint/src/test/java/androidx/navigation/runtime/lint/DeepLinkInActivityDestinationDetectorTest.kt
@@ -17,6 +17,7 @@
 package androidx.navigation.runtime.lint
 
 import com.android.tools.lint.checks.infrastructure.LintDetectorTest
+import com.android.tools.lint.checks.infrastructure.TestMode
 import com.android.tools.lint.detector.api.Detector
 import com.android.tools.lint.detector.api.Issue
 import org.junit.Test
@@ -89,6 +90,7 @@
             """
             )
         )
+            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257336973
             .run()
             .expect("""
 res/navigation/nav_main.xml:17: Warning: Do not attach a <deeplink> to an <activity> destination. Attach the deeplink directly to the second activity or the start destination of a nav host in the second activity instead. [DeepLinkInActivityDestination]
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 03a4616..fa30316 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,8 +26,8 @@
 dependencies {
     api(project(":navigation:navigation-common"))
     api("androidx.activity:activity-ktx:1.6.1")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
+    api(project(":lifecycle:lifecycle-runtime-ktx"))
+    api(project(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index b6c957c..21d312e 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1206,7 +1206,16 @@
             return false
         }
         val extras = intent.extras
-        var deepLink = extras?.getIntArray(KEY_DEEP_LINK_IDS)
+        var deepLink = try {
+            extras?.getIntArray(KEY_DEEP_LINK_IDS)
+        } catch (e: Exception) {
+            Log.e(
+                TAG,
+                "handleDeepLink() could not extract deepLink from $intent",
+                e
+            )
+            null
+        }
         var deepLinkArgs = extras?.getParcelableArrayList<Bundle>(KEY_DEEP_LINK_ARGS)
         val globalArgs = Bundle()
         val deepLinkExtras = extras?.getBundle(KEY_DEEP_LINK_EXTRAS)
diff --git a/paging/paging-common/api/public_plus_experimental_current.txt b/paging/paging-common/api/public_plus_experimental_current.txt
index 8782062..20d1eb3 100644
--- a/paging/paging-common/api/public_plus_experimental_current.txt
+++ b/paging/paging-common/api/public_plus_experimental_current.txt
@@ -46,7 +46,7 @@
     method @AnyThread public void onInvalidated();
   }
 
-  @kotlin.RequiresOptIn public @interface ExperimentalPagingApi {
+  @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalPagingApi {
   }
 
   public final class InvalidatingPagingSourceFactory<Key, Value> implements kotlin.jvm.functions.Function0<androidx.paging.PagingSource<Key,Value>> {
diff --git a/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt b/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
index c31ff68..f431c33 100644
--- a/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
+++ b/paging/paging-common/src/main/kotlin/androidx/paging/ExperimentalPagingApi.kt
@@ -21,4 +21,5 @@
  * source-incompatible change in newer versions of the artifact that supplies it.
  */
 @RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
 public annotation class ExperimentalPagingApi
\ No newline at end of file
diff --git a/paging/paging-testing/api/current.txt b/paging/paging-testing/api/current.txt
index f9a333b..b869342 100644
--- a/paging/paging-testing/api/current.txt
+++ b/paging/paging-testing/api/current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.paging.testing {
 
+  public final class PagerFlowSnapshotKt {
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+  }
+
+  public final class SnapshotLoader<Value> {
+  }
+
   public final class StaticListPagingSourceFactoryKt {
     method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
diff --git a/paging/paging-testing/api/public_plus_experimental_current.txt b/paging/paging-testing/api/public_plus_experimental_current.txt
index f9a333b..b869342 100644
--- a/paging/paging-testing/api/public_plus_experimental_current.txt
+++ b/paging/paging-testing/api/public_plus_experimental_current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.paging.testing {
 
+  public final class PagerFlowSnapshotKt {
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+  }
+
+  public final class SnapshotLoader<Value> {
+  }
+
   public final class StaticListPagingSourceFactoryKt {
     method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
diff --git a/paging/paging-testing/api/restricted_current.txt b/paging/paging-testing/api/restricted_current.txt
index f9a333b..b869342 100644
--- a/paging/paging-testing/api/restricted_current.txt
+++ b/paging/paging-testing/api/restricted_current.txt
@@ -1,6 +1,13 @@
 // Signature format: 4.0
 package androidx.paging.testing {
 
+  public final class PagerFlowSnapshotKt {
+    method public static suspend <Value> Object? asSnapshot(kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope, kotlin.jvm.functions.Function2<? super androidx.paging.testing.SnapshotLoader<Value>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> loadOperations, kotlin.coroutines.Continuation<? super java.util.List<? extends Value>>);
+  }
+
+  public final class SnapshotLoader<Value> {
+  }
+
   public final class StaticListPagingSourceFactoryKt {
     method public static <Value> kotlin.jvm.functions.Function0<androidx.paging.PagingSource<java.lang.Integer,Value>> asPagingSourceFactory(kotlinx.coroutines.flow.Flow<java.util.List<Value>>, kotlinx.coroutines.CoroutineScope coroutineScope);
   }
diff --git a/paging/paging-testing/build.gradle b/paging/paging-testing/build.gradle
index fd8d14d..8aef80c 100644
--- a/paging/paging-testing/build.gradle
+++ b/paging/paging-testing/build.gradle
@@ -29,6 +29,7 @@
 
     testImplementation(libs.junit)
     testImplementation(libs.kotlinCoroutinesTest)
+    testImplementation((libs.kotlinCoroutinesAndroid))
     testImplementation(project(":internal-testutils-paging"))
     testImplementation(libs.kotlinTest)
     testImplementation(libs.truth)
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
new file mode 100644
index 0000000..abf0652
--- /dev/null
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/PagerFlowSnapshot.kt
@@ -0,0 +1,121 @@
+/*
+ * 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.paging.testing
+
+import androidx.paging.DifferCallback
+import androidx.paging.LoadState
+import androidx.paging.LoadStates
+import androidx.paging.NullPaddedList
+import androidx.paging.Pager
+import androidx.paging.PagingData
+import androidx.paging.PagingDataDiffer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancelAndJoin
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Runs the [SnapshotLoader] load operations that are passed in and returns a List of loaded data.
+ *
+ * @param coroutineScope The [CoroutineScope] to collect from this Flow<PagingData> and contains
+ * the [CoroutineScope.coroutineContext] to load data from.
+ *
+ * @param loadOperations The block containing [SnapshotLoader] load operations.
+ */
+public suspend fun <Value : Any> Flow<PagingData<Value>>.asSnapshot(
+    coroutineScope: CoroutineScope,
+    loadOperations: suspend SnapshotLoader<Value>.() -> Unit
+): List<Value> {
+
+    lateinit var loader: SnapshotLoader<Value>
+
+    val callback = object : DifferCallback {
+        override fun onChanged(position: Int, count: Int) {
+            loader.onDataSetChanged(loader.generation.value)
+        }
+        override fun onInserted(position: Int, count: Int) {
+            loader.onDataSetChanged(loader.generation.value)
+        }
+        override fun onRemoved(position: Int, count: Int) {
+            loader.onDataSetChanged(loader.generation.value)
+        }
+    }
+
+    // PagingDataDiffer automatically switches to Dispatchers.Main to call presentNewList
+    val differ = object : PagingDataDiffer<Value>(callback) {
+        override suspend fun presentNewList(
+            previousList: NullPaddedList<Value>,
+            newList: NullPaddedList<Value>,
+            lastAccessedIndex: Int,
+            onListPresentable: () -> Unit
+        ): Int? {
+            onListPresentable()
+            return null
+        }
+    }
+
+    loader = SnapshotLoader(differ)
+
+    /**
+     * Launches collection on this [Pager.flow].
+     *
+     * The collection job is cancelled automatically after [loadOperations] completes.
+      */
+    val job = coroutineScope.launch {
+        this@asSnapshot.collectLatest {
+            // TODO increase generation count
+            differ.collectFrom(it)
+        }
+    }
+
+    /**
+     * Runs the input [loadOperations].
+     *
+     * Awaits for initial refresh to complete before invoking [loadOperations]. Automatically
+     * cancels the collection on this [Pager.flow] after [loadOperations] completes.
+     *
+     * Returns a List of loaded data.
+     */
+    return withContext(coroutineScope.coroutineContext) {
+        differ.awaitNotLoading()
+
+        loader.loadOperations()
+        job.cancelAndJoin()
+
+        differ.snapshot().items
+    }
+}
+
+/**
+ * Awaits until both source and mediator states are NotLoading. We do not care about the state of
+ * endOfPaginationReached. Source and mediator states need to be checked individually because
+ * the aggregated LoadStates can reflect `NotLoading` when source states are `Loading`.
+ */
+internal suspend fun <Value : Any> PagingDataDiffer<Value>.awaitNotLoading() {
+    loadStateFlow.filter {
+        it.source.isIdle() && it.mediator?.isIdle() ?: true
+    }.firstOrNull()
+}
+
+private fun LoadStates.isIdle(): Boolean {
+    return refresh is LoadState.NotLoading && append is LoadState.NotLoading &&
+        prepend is LoadState.NotLoading
+}
\ No newline at end of file
diff --git a/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
new file mode 100644
index 0000000..56cce09
--- /dev/null
+++ b/paging/paging-testing/src/main/java/androidx/paging/testing/SnapshotLoader.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.paging.testing
+
+import androidx.paging.DifferCallback
+import androidx.paging.PagingDataDiffer
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Contains the public APIs for load operations in tests.
+ *
+ * Tracks generational information and provides the listener to [DifferCallback] on
+ * [PagingDataDiffer] operations.
+ */
+public class SnapshotLoader<Value : Any> internal constructor(
+    private val differ: PagingDataDiffer<Value>
+) {
+    internal val generation = MutableStateFlow(Generation(0))
+
+    // TODO add public loading APIs such as scrollTo(index)
+
+    // the callback to be invoked by DifferCallback on a single generation
+    // increase the callbackCount to notify SnapshotLoader that the dataset has updated
+    internal fun onDataSetChanged(gen: Generation) {
+        val currGen = generation.value
+        // we make sure the generation with the dataset change is still valid because we
+        // want to disregard callbacks on stale generations
+        if (gen.id == currGen.id) {
+            generation.value = gen.copy(
+                callbackCount = currGen.callbackCount + 1
+            )
+        }
+    }
+}
+
+internal data class Generation(
+    // Id of the current Paging generation. Incremented on each new generation (when a new
+    // PagingData is received).
+    val id: Int,
+
+    /**
+     * A count of the number of times Paging invokes a [DifferCallback] callback within a single
+     * generation. Incremented on each [DifferCallback] callback invoked, i.e. on item inserted.
+     *
+     * The callbackCount enables [SnapshotLoader] to await for a requested item and continue
+     * loading next item only after a callback is invoked.
+     */
+    val callbackCount: Int = 0
+)
\ No newline at end of file
diff --git a/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
new file mode 100644
index 0000000..9304ec8
--- /dev/null
+++ b/paging/paging-testing/src/test/kotlin/androidx/paging/testing/PagerFlowSnapshotTest.kt
@@ -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.paging.testing
+
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+@RunWith(JUnit4::class)
+class PagerFlowSnapshotTest {
+
+    private val testScope = TestScope(UnconfinedTestDispatcher())
+
+    @Before
+    fun init() {
+        Dispatchers.setMain(UnconfinedTestDispatcher())
+    }
+
+    @Test
+    fun simpleInitialRefresh() {
+        val dataFlow = flowOf(List(30) { it })
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                prefetchDistance = 0
+            ),
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {}
+
+            assertThat(snapshot).containsExactlyElementsIn(
+                listOf(0, 1, 2, 3, 4)
+            )
+        }
+    }
+
+    @Test
+    fun emptyInitialRefresh() {
+        val dataFlow = emptyFlow<List<Int>>()
+        val factory = dataFlow.asPagingSourceFactory(testScope)
+        val pager = Pager(
+            config = PagingConfig(
+                pageSize = 3,
+                initialLoadSize = 5,
+                prefetchDistance = 0
+            ),
+            pagingSourceFactory = factory
+        )
+        testScope.runTest {
+            val snapshot = pager.flow.asSnapshot(this) {}
+
+            assertThat(snapshot).isEmpty()
+        }
+    }
+}
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index a39575d..33c2b39 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=9189666
+androidx.playground.snapshotBuildId=9282206
 androidx.playground.metalavaBuildId=8993580
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
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 56e1a34..2a3436970 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,6 +17,7 @@
 package androidx.privacysandbox.tools.apigenerator
 
 import androidx.privacysandbox.tools.apigenerator.parser.ApiStubParser
+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
@@ -30,6 +31,8 @@
 import androidx.privacysandbox.tools.core.model.ParsedApi
 import androidx.privacysandbox.tools.core.model.getOnlyService
 import androidx.privacysandbox.tools.core.model.hasSuspendFunctions
+import androidx.privacysandbox.tools.core.proto.PrivacySandboxToolsProtocol.ToolMetadata
+import com.google.protobuf.InvalidProtocolBufferException
 import java.io.File
 import java.nio.file.Path
 import java.util.zip.ZipInputStream
@@ -40,6 +43,7 @@
 import kotlin.io.path.isDirectory
 import kotlin.io.path.moveTo
 import kotlin.io.path.outputStream
+import kotlin.io.path.readBytes
 
 /** Generate source files for communicating with an SDK running in the Privacy Sandbox. */
 class PrivacySandboxApiGenerator {
@@ -173,12 +177,32 @@
                     }
             }
 
+            ensureValidMetadata(workingDirectory.resolve(Metadata.filePath))
             return ApiStubParser.parse(workingDirectory)
         } finally {
             workingDirectory.toFile().deleteRecursively()
         }
     }
 
+    private fun ensureValidMetadata(metadataFile: Path) {
+        require(metadataFile.exists()) {
+            "Missing tool metadata in SDK API descriptor."
+        }
+
+        val metadata = try {
+            ToolMetadata.parseFrom(metadataFile.readBytes())
+        } catch (e: InvalidProtocolBufferException) {
+            throw IllegalArgumentException("Invalid Privacy Sandbox tool metadata.", e)
+        }
+
+        val sdkCodeGenerationVersion = metadata.codeGenerationVersion
+        val consumerVersion = Metadata.toolMetadata.codeGenerationVersion
+        require(sdkCodeGenerationVersion <= consumerVersion) {
+            "SDK uses incompatible Privacy Sandbox tooling " +
+                "(version $sdkCodeGenerationVersion). Current version is $consumerVersion."
+        }
+    }
+
     private fun generateSuspendFunctionUtilities(
         api: ParsedApi,
         basePackageName: String,
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
index 33cf59b..10561b1 100644
--- 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
@@ -23,7 +23,6 @@
 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
@@ -46,7 +45,6 @@
     private val validMetadataContent = Metadata.toolMetadata.toByteArray()
 
     @Test
-    @Ignore("b/255740194")
     fun sdkDescriptorWithMissingMetadata_throws() {
         assertThrows<IllegalArgumentException> {
             runGeneratorWithResources(mapOf())
@@ -54,7 +52,6 @@
     }
 
     @Test
-    @Ignore("b/255740194")
     fun sdkDescriptorWithMetadataInWrongPath_throws() {
         assertThrows<IllegalArgumentException> {
             runGeneratorWithResources(
@@ -64,7 +61,6 @@
     }
 
     @Test
-    @Ignore("b/255740194")
     fun sdkDescriptorWithInvalidMetadataContent_throws() {
         assertThrows<IllegalArgumentException> {
             runGeneratorWithResources(
@@ -74,7 +70,6 @@
     }
 
     @Test
-    @Ignore("b/255740194")
     fun sdkDescriptorWithIncompatibleVersion_throws() {
         val sdkMetadata = ToolMetadata.newBuilder()
             .setCodeGenerationVersion(999)
@@ -89,7 +84,6 @@
     }
 
     @Test
-    @Ignore("b/255740194")
     fun sdkDescriptorWithLowerVersion_isCompatible() {
         val sdkMetadata = ToolMetadata.newBuilder()
             .setCodeGenerationVersion(0)
diff --git a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
index 422199b..a417d4a 100644
--- a/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
+++ b/profileinstaller/profileinstaller/src/main/AndroidManifest.xml
@@ -40,6 +40,9 @@
             <intent-filter>
                 <action android:name="androidx.profileinstaller.action.SAVE_PROFILE" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION" />
+            </intent-filter>
         </receiver>
     </application>
 </manifest>
\ No newline at end of file
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 b56858e..a1b18f0 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/GridLayoutManagerTest.java
@@ -850,6 +850,102 @@
     }
 
     @Test
+    public void rowCountForAccessibility_verticalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(34, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(3, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(1, count);
+    }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getRowCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(2, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(3, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_horizontalOrientation() throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 100));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(34, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(2, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final RecyclerView recyclerView = setupBasic(new Config(3, 2));
+        mGlm.setOrientation(RecyclerView.HORIZONTAL);
+        waitForFirstLayout(recyclerView);
+
+        int count = mGlm.getColumnCountForAccessibility(recyclerView.mRecycler,
+                recyclerView.mState);
+
+        assertEquals(1, count);
+    }
+
+    @Test
     public void accessibilityClassName() throws Throwable {
         final RecyclerView recyclerView = setupBasic(new Config(3, 100));
         waitForFirstLayout(recyclerView);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index 1649118..f00957f 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -22,6 +22,8 @@
 import static androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL;
 import static androidx.recyclerview.widget.LinearLayoutManager.VERTICAL;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.hamcrest.CoreMatchers.is;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -1473,4 +1475,22 @@
     private void assertFirstItemIsAtTop() {
         assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (1)");
     }
+
+    @Test
+    public void onInitializeAccessibilityNodeInfo_noAdapter() throws Throwable {
+        mRecyclerView = inflateWrappedRV();
+        mLayoutManager = new WrappedLinearLayoutManager(
+                getActivity(), LinearLayoutManager.VERTICAL, false);
+        mRecyclerView.setLayoutManager(mLayoutManager);
+
+        AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+        mActivityRule.runOnUiThread(() -> {
+            mLayoutManager.onInitializeAccessibilityNodeInfo(mRecyclerView.mRecycler,
+                    mRecyclerView.mState, nodeInfo);
+        });
+
+        assertThat(nodeInfo.getActionList()).doesNotContain(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
+
+    }
 }
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
index 81fc754..aa53614 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/StaggeredGridLayoutManagerTest.java
@@ -48,6 +48,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.core.view.AccessibilityDelegateCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 
@@ -1424,4 +1425,45 @@
                 Math.max(start, end), event.getToIndex());
 
     }
+
+    @Test
+    public void rowCountForAccessibility_horizontalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final int itemCount = 2;
+        Config config = new Config(HORIZONTAL, false, 3, GAP_HANDLING_NONE).itemCount(itemCount);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getRowCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(itemCount, count);
+    }
+
+    @Test
+    public void columnCountForAccessibility_verticalOrientation_fewerItemsThanSpanCount()
+            throws Throwable {
+        final int itemCount = 2;
+        Config config = new Config(VERTICAL, false, 3, GAP_HANDLING_NONE).itemCount(itemCount);
+        setupByConfig(config);
+        waitFirstLayout();
+
+        int count = mLayoutManager.getColumnCountForAccessibility(mRecyclerView.mRecycler,
+                mRecyclerView.mState);
+
+        assertEquals(itemCount, count);
+    }
+
+    @Test
+    public void onInitializeAccessibilityNodeInfo() throws Throwable {
+        setupByConfig(new Config(VERTICAL, false, 3, GAP_HANDLING_NONE));
+        waitFirstLayout();
+        final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
+
+        mActivityRule.runOnUiThread(
+                () -> mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(
+                        mRecyclerView.mRecycler, mRecyclerView.mState, info));
+        assertEquals(info.getClassName(),
+                "androidx.recyclerview.widget.StaggeredGridLayoutManager");
+    }
 }
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 d18f0b9..f043bf6 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/GridLayoutManager.java
@@ -122,7 +122,7 @@
     public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
             RecyclerView.State state) {
         if (mOrientation == HORIZONTAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, getItemCount());
         }
         if (state.getItemCount() < 1) {
             return 0;
@@ -136,7 +136,7 @@
     public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
             RecyclerView.State state) {
         if (mOrientation == VERTICAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, getItemCount());
         }
         if (state.getItemCount() < 1) {
             return 0;
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 19a9b5f..ac79210 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -294,7 +294,7 @@
             @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
         super.onInitializeAccessibilityNodeInfo(recycler, state, info);
         // TODO(b/251823537)
-        if (mRecyclerView.mAdapter.getItemCount() > 0) {
+        if (mRecyclerView.mAdapter != null && mRecyclerView.mAdapter.getItemCount() > 0) {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                 info.addAction(AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
             }
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
index 73f1d50..df03e317 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/StaggeredGridLayoutManager.java
@@ -1291,6 +1291,16 @@
     }
 
     @Override
+    public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
+            @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
+        super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+        // Setting the classname allows accessibility services to set a role for staggered grids
+        // and ensures that they are treated distinctly from canonical grids with clear row/column
+        // semantics.
+        info.setClassName("androidx.recyclerview.widget.StaggeredGridLayoutManager");
+    }
+
+    @Override
     public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state, @NonNull View host,
             @NonNull AccessibilityNodeInfoCompat info) {
@@ -1346,7 +1356,7 @@
     public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state) {
         if (mOrientation == HORIZONTAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, state.getItemCount());
         }
         return super.getRowCountForAccessibility(recycler, state);
     }
@@ -1355,7 +1365,7 @@
     public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
             @NonNull RecyclerView.State state) {
         if (mOrientation == VERTICAL) {
-            return mSpanCount;
+            return Math.min(mSpanCount, state.getItemCount());
         }
         return super.getColumnCountForAccessibility(recycler, state);
     }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
index 69b9a8f..e384141 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XCodeBlock.kt
@@ -59,6 +59,9 @@
         fun nextControlFlow(controlFlow: String, vararg args: Any?): Builder
         fun endControlFlow(): Builder
 
+        fun indent(): Builder
+        fun unindent(): Builder
+
         fun build(): XCodeBlock
 
         companion object {
@@ -71,7 +74,7 @@
                 name: String,
                 typeName: XTypeName,
                 assignExprFormat: String,
-                vararg assignExprArgs: Any
+                vararg assignExprArgs: Any?
             ) = apply {
                 addLocalVariable(
                     name = name,
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
index 342c41c..56cc91c 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XFunSpec.kt
@@ -34,6 +34,8 @@
 
     interface Builder : TargetLanguage {
 
+        val name: String
+
         fun addAnnotation(annotation: XAnnotationSpec)
 
         // TODO(b/247247442): Maybe make a XParameterSpec ?
@@ -135,7 +137,11 @@
                     KotlinFunSpec.Builder(
                         name,
                         FunSpec.constructorBuilder().apply {
-                            addModifiers(visibility.toKotlinVisibilityModifier())
+                            // Workaround for the unreleased fix in
+                            // https://github.com/square/kotlinpoet/pull/1342
+                            if (visibility != VisibilityModifier.PUBLIC) {
+                                addModifiers(visibility.toKotlinVisibilityModifier())
+                            }
                         }
                     )
                 }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
index 36bf690..a66f12e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XPropertySpec.kt
@@ -73,6 +73,22 @@
                 )
             }
         }
+
+        fun XPropertySpec.Builder.apply(
+            javaFieldBuilder: com.squareup.javapoet.FieldSpec.Builder.() -> Unit,
+            kotlinPropertyBuilder: com.squareup.kotlinpoet.PropertySpec.Builder.() -> Unit,
+        ): XPropertySpec.Builder = apply {
+            when (language) {
+                CodeLanguage.JAVA -> {
+                    check(this is JavaPropertySpec.Builder)
+                    this.actual.javaFieldBuilder()
+                }
+                CodeLanguage.KOTLIN -> {
+                    check(this is KotlinPropertySpec.Builder)
+                    this.actual.kotlinPropertyBuilder()
+                }
+            }
+        }
     }
 }
 
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 222db27..d1416b0 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
@@ -40,6 +40,7 @@
 import com.squareup.kotlinpoet.javapoet.JTypeName
 import com.squareup.kotlinpoet.javapoet.JWildcardTypeName
 import com.squareup.kotlinpoet.javapoet.KClassName
+import com.squareup.kotlinpoet.javapoet.KParameterizedTypeName
 import com.squareup.kotlinpoet.javapoet.KTypeName
 import com.squareup.kotlinpoet.javapoet.KWildcardTypeName
 import kotlin.reflect.KClass
@@ -56,11 +57,27 @@
 open class XTypeName protected constructor(
     internal open val java: JTypeName,
     internal open val kotlin: KTypeName,
-    internal val nullability: XNullability
+    val nullability: XNullability
 ) {
     val isPrimitive: Boolean
         get() = java.isPrimitive
 
+    /**
+     * Returns the raw [XTypeName] if this is a parametrized type name, or itself if not.
+     *
+     * @see [XClassName.parametrizedBy]
+     */
+    val rawTypeName: XTypeName
+        get() {
+            val javaRawType = java.let {
+                if (it is JParameterizedTypeName) it.rawType else it
+            }
+            val kotlinRawType = kotlin.let {
+                if (it is KParameterizedTypeName) it.rawType else it
+            }
+            return XTypeName(javaRawType, kotlinRawType, nullability)
+        }
+
     open fun copy(nullable: Boolean): XTypeName {
         // TODO(b/248633751): Handle primitive to boxed when becoming nullable?
         return XTypeName(
@@ -214,6 +231,11 @@
     val simpleNames: List<String> = java.simpleNames()
     val canonicalName: String = java.canonicalName()
 
+    /**
+     * Returns a parameterized type, applying the `typeArguments` to `this`.
+     *
+     * @see [XTypeName.rawTypeName]
+     */
     fun parametrizedBy(
         vararg typeArguments: XTypeName,
     ): XTypeName {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
index 5261713..c4608e9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeSpec.kt
@@ -37,12 +37,13 @@
         fun addProperty(propertySpec: XPropertySpec): Builder
         fun addFunction(functionSpec: XFunSpec): Builder
         fun addType(typeSpec: XTypeSpec): Builder
+        fun setPrimaryConstructor(functionSpec: XFunSpec): Builder
         fun setVisibility(visibility: VisibilityModifier)
         fun build(): XTypeSpec
 
         companion object {
 
-            fun XTypeSpec.Builder.addOriginatingElement(element: XElement) = apply {
+            fun Builder.addOriginatingElement(element: XElement) = apply {
                 when (language) {
                     CodeLanguage.JAVA -> {
                         check(this is JavaTypeSpec.Builder)
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
index b8d2752..3620c0e 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaCodeBlock.kt
@@ -88,6 +88,14 @@
             actual.endControlFlow()
         }
 
+        override fun indent() = apply {
+            actual.indent()
+        }
+
+        override fun unindent() = apply {
+            actual.unindent()
+        }
+
         override fun build(): XCodeBlock {
             return JavaCodeBlock(actual.build())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 470f3b7..fc14709 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -22,7 +22,6 @@
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.processing.KnownTypeNames.KOTLIN_UNIT
 import androidx.room.compiler.processing.XNullability
 import com.squareup.javapoet.CodeBlock
 import com.squareup.javapoet.MethodSpec
@@ -36,7 +35,7 @@
 ) : JavaLang(), XFunSpec {
 
     internal class Builder(
-        private val name: String,
+        override val name: String,
         internal val actual: MethodSpec.Builder
     ) : JavaLang(), XFunSpec.Builder {
 
@@ -82,7 +81,7 @@
         }
 
         override fun returns(typeName: XTypeName) = apply {
-            if (typeName.java == JTypeName.VOID || typeName.java == KOTLIN_UNIT) {
+            if (typeName.java == JTypeName.VOID) {
                 return@apply
             }
             // TODO(b/247242374) Add nullability annotations for non-private methods
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
index f281154..8a50dae 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaTypeSpec.kt
@@ -69,6 +69,8 @@
             actual.addType(typeSpec.actual)
         }
 
+        override fun setPrimaryConstructor(functionSpec: XFunSpec) = addFunction(functionSpec)
+
         override fun setVisibility(visibility: VisibilityModifier) {
             actual.addModifiers(visibility.toJavaVisibilityModifier())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
index 7958c03..825a499 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinCodeBlock.kt
@@ -94,6 +94,14 @@
             actual.endControlFlow()
         }
 
+        override fun indent() = apply {
+            actual.indent()
+        }
+
+        override fun unindent() = apply {
+            actual.unindent()
+        }
+
         override fun build(): XCodeBlock {
             return KotlinCodeBlock(actual.build())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
index 4c90427..4f95e62 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinFunSpec.kt
@@ -24,7 +24,6 @@
 import com.squareup.kotlinpoet.FunSpec
 import com.squareup.kotlinpoet.KModifier
 import com.squareup.kotlinpoet.ParameterSpec
-import com.squareup.kotlinpoet.UNIT
 
 internal class KotlinFunSpec(
     override val name: String,
@@ -32,7 +31,7 @@
 ) : KotlinLang(), XFunSpec {
 
     internal class Builder(
-        private val name: String,
+        override val name: String,
         internal val actual: FunSpec.Builder
     ) : KotlinLang(), XFunSpec.Builder {
 
@@ -68,9 +67,6 @@
         }
 
         override fun returns(typeName: XTypeName) = apply {
-            if (typeName.kotlin == UNIT) {
-                return@apply
-            }
             actual.returns(typeName.kotlin)
         }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
index 425a758..3151ff0 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/kotlin/KotlinTypeSpec.kt
@@ -69,6 +69,14 @@
             actual.addType(typeSpec.actual)
         }
 
+        override fun setPrimaryConstructor(functionSpec: XFunSpec) = apply {
+            require(functionSpec is KotlinFunSpec)
+            actual.primaryConstructor(functionSpec.actual)
+            functionSpec.actual.delegateConstructorArguments.forEach {
+                actual.addSuperclassConstructorParameter(it)
+            }
+        }
+
         override fun setVisibility(visibility: VisibilityModifier) {
             actual.addModifiers(visibility.toKotlinVisibilityModifier())
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
index 1f71ce4..9d3f1bb 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/XProcessingEnv.kt
@@ -24,6 +24,7 @@
 import com.google.devtools.ksp.processing.Resolver
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
+import com.squareup.kotlinpoet.javapoet.KClassName
 import javax.annotation.processing.ProcessingEnvironment
 import kotlin.reflect.KClass
 
@@ -94,6 +95,15 @@
     fun getDeclaredType(type: XTypeElement, vararg types: XType): XType
 
     /**
+     * Returns an [XType] representing a wildcard type.
+     *
+     * In Java source, this represents types like `?`, `? extends T`, and `? super T`.
+     *
+     * In Kotlin source, this represents types like `*`, `out T`, and `in T`.
+     */
+    fun getWildcardType(consumerSuper: XType? = null, producerExtends: XType? = null): XType
+
+    /**
      * Return an [XArrayType] that has [type] as the [XArrayType.componentType].
      */
     fun getArrayType(type: XType): XArrayType
@@ -119,7 +129,17 @@
         }
         return when (backend) {
             Backend.JAVAC -> requireType(typeName.java)
-            Backend.KSP -> requireType(typeName.kotlin.toString())
+            Backend.KSP -> {
+                val kClassName = typeName.kotlin as? KClassName
+                    ?: error("cannot find required type ${typeName.kotlin}")
+                requireType(kClassName.canonicalName)
+            }
+        }.let {
+            when (typeName.nullability) {
+                XNullability.NULLABLE -> it.makeNullable()
+                XNullability.NONNULL -> it.makeNonNullable()
+                XNullability.UNKNOWN -> it
+            }
         }
     }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index da6d0de..c9c5393 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -139,11 +139,6 @@
             check(it is JavacType)
             it.typeMirror
         }.toTypedArray()
-        check(
-            types.all {
-                it is JavacType
-            }
-        )
         return wrap<JavacDeclaredType>(
             typeMirror = typeUtils.getDeclaredType(type.element, *args),
             // type elements cannot have nullability hence we don't synthesize anything here
@@ -152,6 +147,20 @@
         )
     }
 
+    override fun getWildcardType(consumerSuper: XType?, producerExtends: XType?): XType {
+        check(consumerSuper == null || producerExtends == null) {
+            "Cannot supply both super and extends bounds."
+        }
+        return wrap(
+            typeMirror = typeUtils.getWildcardType(
+                (producerExtends as? JavacType)?.typeMirror,
+                (consumerSuper as? JavacType)?.typeMirror,
+            ),
+            kotlinType = null,
+            elementNullability = null
+        )
+    }
+
     fun wrapTypeElement(element: TypeElement) = typeElementStore[element]
 
     /**
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
index 48ddf0a..87f05bf 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspAnnotationValue.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.processing.InternalXAnnotationValue
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
@@ -93,10 +94,26 @@
                     !is List<*> -> listOf(result)
                     else -> result
                 }.map {
-                    KspAnnotationValue(env, this, valueType.componentType, valueArgument) { it }
+                    KspAnnotationValue(env, this, valueType.componentType, valueArgument) {
+                        convertValueToType(it, valueType.componentType)
+                    }
                 }
             }
-            else -> result
+            else -> convertValueToType(result, valueType)
         }
     }
 }
+
+private fun convertValueToType(value: Any?, valueType: XType): Any? {
+    // Unlike Javac, KSP does not convert the value to the type declared on the annotation class's
+    // annotation value automatically so we have to do that conversion manually here.
+    return when (valueType.asTypeName()) {
+        XTypeName.PRIMITIVE_BYTE -> (value as Number).toByte()
+        XTypeName.PRIMITIVE_SHORT -> (value as Number).toShort()
+        XTypeName.PRIMITIVE_INT -> (value as Number).toInt()
+        XTypeName.PRIMITIVE_LONG -> (value as Number).toLong()
+        XTypeName.PRIMITIVE_FLOAT -> (value as Number).toFloat()
+        XTypeName.PRIMITIVE_DOUBLE -> (value as Number).toDouble()
+        else -> value
+    }
+}
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index d58625a..002a36a 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -144,7 +144,11 @@
             }
             resolver.getTypeArgument(
                 argType.ksType.createTypeReference(),
-                variance = Variance.INVARIANT
+                variance = if (argType is KspTypeArgumentType) {
+                    argType.typeArg.variance
+                } else {
+                    Variance.INVARIANT
+                }
             )
         }
         return wrap(
@@ -153,6 +157,31 @@
         )
     }
 
+    override fun getWildcardType(consumerSuper: XType?, producerExtends: XType?): XType {
+        check(consumerSuper == null || producerExtends == null) {
+            "Cannot supply both super and extends bounds."
+        }
+        return wrap(
+            ksTypeArgument = if (consumerSuper != null) {
+                resolver.getTypeArgument(
+                    typeRef = (consumerSuper as KspType).ksType.createTypeReference(),
+                    variance = Variance.CONTRAVARIANT
+                )
+            } else if (producerExtends != null) {
+                resolver.getTypeArgument(
+                    typeRef = (producerExtends as KspType).ksType.createTypeReference(),
+                    variance = Variance.COVARIANT
+                )
+            } else {
+                // This returns the type "out Any?", which should be equivalent to "*"
+                resolver.getTypeArgument(
+                    typeRef = resolver.builtIns.anyType.makeNullable().createTypeReference(),
+                    variance = Variance.COVARIANT
+                )
+            }
+        )
+    }
+
     override fun getArrayType(type: XType): KspArrayType {
         check(type is KspType)
         return arrayTypeFactory.createWithComponentType(type)
@@ -184,7 +213,7 @@
         ksType = typeReference.resolve()
     )
 
-    fun wrap(ksTypeParam: KSTypeParameter, ksTypeArgument: KSTypeArgument): KspType {
+    fun wrap(ksTypeArgument: KSTypeArgument): KspType {
         val typeRef = ksTypeArgument.type
         if (typeRef != null && ksTypeArgument.variance == Variance.INVARIANT) {
             // fully resolved type argument, return regular type.
@@ -196,7 +225,6 @@
         return KspTypeArgumentType(
             env = this,
             typeArg = ksTypeArgument,
-            typeParam = ksTypeParam,
             jvmTypeResolver = null
         )
     }
@@ -231,7 +259,6 @@
                     ksType.createTypeReference(),
                     declaration.variance
                 ),
-                typeParam = declaration,
                 jvmTypeResolver = null
             )
         }
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index 3acfe4a..133b002 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -175,9 +175,7 @@
         if (env.resolver.isJavaRawType(ksType)) {
             emptyList()
         } else {
-            ksType.arguments.mapIndexed { index, arg ->
-                env.wrap(ksType.declaration.typeParameters[index], arg)
-            }
+            ksType.arguments.map { env.wrap(it) }
         }
     }
 
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index f7471ae..668554d 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -19,7 +19,6 @@
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import com.google.devtools.ksp.symbol.KSTypeArgument
-import com.google.devtools.ksp.symbol.KSTypeParameter
 import com.google.devtools.ksp.symbol.KSTypeReference
 import com.squareup.kotlinpoet.javapoet.JTypeName
 import com.squareup.kotlinpoet.javapoet.KTypeName
@@ -30,7 +29,6 @@
  */
 internal class KspTypeArgumentType(
     env: KspProcessingEnv,
-    val typeParam: KSTypeParameter,
     val typeArg: KSTypeArgument,
     jvmTypeResolver: KspJvmTypeResolver?
 ) : KspType(
@@ -68,7 +66,6 @@
     override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
         return KspTypeArgumentType(
             env = env,
-            typeParam = typeParam,
             typeArg = DelegatingTypeArg(
                 original = typeArg,
                 type = ksType.withNullability(nullability).createTypeReference()
@@ -80,7 +77,6 @@
     override fun copyWithJvmTypeResolver(jvmTypeResolver: KspJvmTypeResolver): KspType {
         return KspTypeArgumentType(
             env = env,
-            typeParam = typeParam,
             typeArg = typeArg,
             jvmTypeResolver = jvmTypeResolver
         )
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
index 4a42d64..f325855 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/codegen/XTypeNameTest.kt
@@ -124,4 +124,11 @@
             ).hashCode()
         ).isEqualTo(expectedClass.hashCode())
     }
+
+    @Test
+    fun rawType() {
+        val expectedRawClass = XClassName.get("foo", "Bar")
+        assertThat(expectedRawClass.parametrizedBy(String::class.asClassName()).rawTypeName)
+            .isEqualTo(expectedRawClass)
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
index 8c01a28..3d05e0b 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XAnnotationValueTest.kt
@@ -200,9 +200,9 @@
                     int[] intVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    intParam = 1,
-                    intArrayParam = {3, 5, 7},
-                    intVarArgsParam = {9, 11, 13}
+                    intParam = (short) 1,
+                    intArrayParam = {(byte) 3, (short) 5, 7},
+                    intVarArgsParam = {(byte) 9, (short) 11, 13}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -290,9 +290,9 @@
                     short[] shortVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    shortParam = (short) 1,
-                    shortArrayParam = {(short) 3, (short) 5, (short) 7},
-                    shortVarArgsParam = {(short) 9, (short) 11, (short) 13}
+                    shortParam = (byte) 1,
+                    shortArrayParam = {(byte) 3, (short) 5, 7},
+                    shortVarArgsParam = {(byte) 9, (short) 11, 13}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -380,9 +380,9 @@
                     long[] longVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    longParam = 1L,
-                    longArrayParam = {3L, 5L, 7L},
-                    longVarArgsParam = {9L, 11L, 13L}
+                    longParam = (byte) 1,
+                    longArrayParam = {(short) 3, (int) 5, 7L},
+                    longVarArgsParam = {(short) 9, (int) 11, 13L}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -470,9 +470,9 @@
                     float[] floatVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    floatParam = 1.1F,
-                    floatArrayParam = {3.1F, 5.1F, 7.1F},
-                    floatVarArgsParam = {9.1F, 11.1F, 13.1F}
+                    floatParam = (byte) 1,
+                    floatArrayParam = {(short) 3, 5.1F, 7.1F},
+                    floatVarArgsParam = {9, 11.1F, 13.1F}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -487,9 +487,9 @@
                     vararg val floatVarArgsParam: Float,
                 )
                 @MyAnnotation(
-                    floatParam = 1.1F,
-                    floatArrayParam = [3.1F, 5.1F, 7.1F],
-                    floatVarArgsParam = [9.1F, 11.1F, 13.1F],
+                    floatParam = 1F,
+                    floatArrayParam = [3F, 5.1F, 7.1F],
+                    floatVarArgsParam = [9F, 11.1F, 13.1F],
                 )
                 class MyClass
                 """.trimIndent()
@@ -530,20 +530,20 @@
             assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
                 .isEqualTo("""
                     @test.MyAnnotation(
-                        floatParam = 1.1f,
-                        floatArrayParam = {3.1f, 5.1f, 7.1f},
-                        floatVarArgsParam = {9.1f, 11.1f, 13.1f}
+                        floatParam = 1.0f,
+                        floatArrayParam = {3.0f, 5.1f, 7.1f},
+                        floatVarArgsParam = {9.0f, 11.1f, 13.1f}
                     )
                     """.removeWhiteSpace())
 
             val floatParam = annotation.getAnnotationValue("floatParam")
-            checkSingleValue(floatParam, 1.1F)
+            checkSingleValue(floatParam, 1.0F)
 
             val floatArrayParam = annotation.getAnnotationValue("floatArrayParam")
-            checkListValues(floatArrayParam, 3.1F, 5.1F, 7.1F)
+            checkListValues(floatArrayParam, 3.0F, 5.1F, 7.1F)
 
             val floatVarArgsParam = annotation.getAnnotationValue("floatVarArgsParam")
-            checkListValues(floatVarArgsParam, 9.1F, 11.1F, 13.1F)
+            checkListValues(floatVarArgsParam, 9.0F, 11.1F, 13.1F)
         }
     }
 
@@ -560,9 +560,9 @@
                     double[] doubleVarArgsParam(); // There's no varargs in java so use array
                 }
                 @MyAnnotation(
-                    doubleParam = 1.1,
-                    doubleArrayParam = {3.1, 5.1, 7.1},
-                    doubleVarArgsParam = {9.1, 11.1, 13.1}
+                    doubleParam = (byte) 1,
+                    doubleArrayParam = {(short) 3, 5.1F, 7.1},
+                    doubleVarArgsParam = {9, 11.1F, 13.1}
                 )
                 class MyClass {}
                 """.trimIndent()
@@ -577,9 +577,9 @@
                     vararg val doubleVarArgsParam: Double,
                 )
                 @MyAnnotation(
-                    doubleParam = 1.1,
-                    doubleArrayParam = [3.1, 5.1, 7.1],
-                    doubleVarArgsParam = [9.1, 11.1, 13.1],
+                    doubleParam = 1.0,
+                    doubleArrayParam = [3.0, 5.1, 7.1],
+                    doubleVarArgsParam = [9.0, 11.1, 13.1],
                 )
                 class MyClass
                 """.trimIndent()
@@ -615,25 +615,57 @@
             val annotation = invocation.processingEnv.requireTypeElement("test.MyClass")
                 .getAllAnnotations()
                 .single { it.qualifiedName == "test.MyAnnotation" }
+            annotation.getAnnotationValue("doubleParam").value
+            annotation.getAnnotationValue("doubleArrayParam").value
+            annotation.getAnnotationValue("doubleVarArgsParam").value
 
-            // Compare the AnnotationSpec string ignoring whitespace
-            assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
-                .isEqualTo("""
-                    @test.MyAnnotation(
-                        doubleParam = 1.1,
-                        doubleArrayParam = {3.1, 5.1, 7.1},
-                        doubleVarArgsParam = {9.1, 11.1, 13.1}
+            // The java source allows an interesting corner case where you can use a float,
+            // e.g. 5.1F, in place of a double and the value returned is converted to a double.
+            // Note that the kotlin source doesn't even allow this case so we've separated them
+            // into two separate checks below.
+            if (sourceKind == SourceKind.JAVA) {
+                // Compare the AnnotationSpec string ignoring whitespace
+                assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
+                    .isEqualTo(
+                        """
+                        @test.MyAnnotation(
+                            doubleParam = 1.0,
+                            doubleArrayParam = {3.0, 5.099999904632568, 7.1},
+                            doubleVarArgsParam = {9.0, 11.100000381469727, 13.1}
+                        )
+                        """.removeWhiteSpace()
                     )
-                    """.removeWhiteSpace())
 
-            val doubleParam = annotation.getAnnotationValue("doubleParam")
-            checkSingleValue(doubleParam, 1.1)
+                val doubleParam = annotation.getAnnotationValue("doubleParam")
+                checkSingleValue(doubleParam, 1.0)
 
-            val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
-            checkListValues(doubleArrayParam, 3.1, 5.1, 7.1)
+                val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
+                checkListValues(doubleArrayParam, 3.0, 5.099999904632568, 7.1)
 
-            val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
-            checkListValues(doubleVarArgsParam, 9.1, 11.1, 13.1)
+                val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
+                checkListValues(doubleVarArgsParam, 9.0, 11.100000381469727, 13.1)
+            } else {
+                // Compare the AnnotationSpec string ignoring whitespace
+                assertThat(annotation.toAnnotationSpec().toString().removeWhiteSpace())
+                    .isEqualTo(
+                        """
+                        @test.MyAnnotation(
+                            doubleParam = 1.0,
+                            doubleArrayParam = {3.0, 5.1, 7.1},
+                            doubleVarArgsParam = {9.0, 11.1, 13.1}
+                        )
+                        """.removeWhiteSpace()
+                    )
+
+                val doubleParam = annotation.getAnnotationValue("doubleParam")
+                checkSingleValue(doubleParam, 1.0)
+
+                val doubleArrayParam = annotation.getAnnotationValue("doubleArrayParam")
+                checkListValues(doubleArrayParam, 3.0, 5.1, 7.1)
+
+                val doubleVarArgsParam = annotation.getAnnotationValue("doubleVarArgsParam")
+                checkListValues(doubleVarArgsParam, 9.0, 11.1, 13.1)
+            }
         }
     }
 
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index b49b301..3802cb2 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -587,12 +587,7 @@
                     )
                 subject.getField("x").let { field ->
                     assertThat(field.isFinal()).isFalse()
-                    // b/250567151: Remove exception for KSP + classes
-                    if (invocation.isKsp && pkg == "lib") {
-                        assertThat(field.isPrivate()).isTrue()
-                    } else {
-                        assertThat(field.isPrivate()).isFalse()
-                    }
+                    assertThat(field.isPrivate()).isFalse()
                 }
             }
         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
index 4e5c8b2..54f581f 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeTest.kt
@@ -24,6 +24,7 @@
 import androidx.room.compiler.processing.util.asJClassName
 import androidx.room.compiler.processing.util.asKClassName
 import androidx.room.compiler.processing.util.dumpToString
+import androidx.room.compiler.processing.util.getDeclaredField
 import androidx.room.compiler.processing.util.getDeclaredMethodByJvmName
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethodByJvmName
@@ -1307,4 +1308,73 @@
             result.hasErrorContaining("Unresolved reference")
         }
     }
+
+    @Test
+    fun getWildcardType() {
+        fun XTestInvocation.checkType() {
+            val usageElement = processingEnv.requireTypeElement("test.Usage")
+            val fooElement = processingEnv.requireTypeElement("test.Foo")
+            val barType = processingEnv.requireType("test.Bar")
+
+            // Test a manually constructed Foo<Bar>
+            val fooBarType = processingEnv.getDeclaredType(fooElement, barType)
+            val fooBarUsageType = usageElement.getDeclaredField("fooBar").type
+            assertThat(fooBarType.asTypeName()).isEqualTo(fooBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<? extends Bar>
+            val fooExtendsBarType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType(producerExtends = barType)
+            )
+            val fooExtendsBarUsageType = usageElement.getDeclaredField("fooExtendsBar").type
+            assertThat(fooExtendsBarType.asTypeName())
+                .isEqualTo(fooExtendsBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<? super Bar>
+            val fooSuperBarType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType(consumerSuper = barType)
+            )
+            val fooSuperBarUsageType = usageElement.getDeclaredField("fooSuperBar").type
+            assertThat(fooSuperBarType.asTypeName()).isEqualTo(fooSuperBarUsageType.asTypeName())
+
+            // Test a manually constructed Foo<?>
+            val fooUnboundedType = processingEnv.getDeclaredType(
+                fooElement,
+                processingEnv.getWildcardType()
+            )
+            val fooUnboundedUsageType = usageElement.getDeclaredField("fooUnbounded").type
+            assertThat(fooUnboundedType.asTypeName()).isEqualTo(fooUnboundedUsageType.asTypeName())
+        }
+
+        runProcessorTest(listOf(Source.java(
+            "test.Foo",
+            """
+            package test;
+            class Usage {
+              Foo<?> fooUnbounded;
+              Foo<Bar> fooBar;
+              Foo<? extends Bar> fooExtendsBar;
+              Foo<? super Bar> fooSuperBar;
+            }
+            interface Foo<T> {}
+            interface Bar {}
+            """.trimIndent()
+        ))) { it.checkType() }
+
+        runProcessorTest(listOf(Source.kotlin(
+            "test.Usage.kt",
+            """
+            package test
+            class Usage {
+              val fooUnbounded: Foo<*> = TODO()
+              val fooBar: Foo<Bar> = TODO()
+              val fooExtendsBar: Foo<out Bar> = TODO()
+              val fooSuperBar: Foo<in Bar> = TODO()
+            }
+            interface Foo<T>
+            interface Bar
+            """.trimIndent()
+        ))) { it.checkType() }
+    }
 }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
index 766bfe6..b753598 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFieldElementTest.kt
@@ -30,7 +30,6 @@
 import androidx.room.compiler.processing.util.typeName
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
-import com.google.devtools.ksp.symbol.Origin
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import com.squareup.javapoet.TypeVariableName
@@ -233,22 +232,11 @@
             val element = invocation.processingEnv.requireTypeElement(input.qName)
             input.expected.forEach { (name, modifiers) ->
                 val field = element.getField(name)
-                // b/250567151: Remove exception for KSP + classes
-                if (invocation.isKsp &&
-                        (element as KspTypeElement).declaration.origin == Origin.KOTLIN_LIB &&
-                        name.lowercase().contains("lateinit")) {
-                    assertWithMessage("${input.qName}:$name")
-                        .that(field.modifiers)
-                        .containsExactlyElementsIn(
-                            listOf(PRIVATE)
-                        )
-                } else {
-                    assertWithMessage("${input.qName}:$name")
-                        .that(field.modifiers)
-                        .containsExactlyElementsIn(
-                            modifiers
-                        )
-                }
+                assertWithMessage("${input.qName}:$name")
+                    .that(field.modifiers)
+                    .containsExactlyElementsIn(
+                        modifiers
+                    )
                 assertThat(field.enclosingElement).isEqualTo(element)
             }
         }
diff --git a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
index e3cbe2f..4075f36 100644
--- a/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
+++ b/room/room-compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspFilerTest.kt
@@ -204,6 +204,24 @@
             fileDependencies[fileName] = dependencies
             return OutputStream.nullOutputStream()
         }
+
+        override fun associateByPath(
+            sources: List<KSFile>,
+            path: String,
+            extensionName: String
+        ) {
+            // no-op for the sake of dependency tracking.
+        }
+
+        override fun createNewFileByPath(
+            dependencies: Dependencies,
+            path: String,
+            extensionName: String
+        ): OutputStream {
+            val fileName = path.split(File.separator).last()
+            fileDependencies[fileName] = dependencies
+            return OutputStream.nullOutputStream()
+        }
     }
 
     companion object {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
index acad3d6..fa49f87 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/DatabaseProcessingStep.kt
@@ -16,7 +16,6 @@
 
 package androidx.room
 
-import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XProcessingEnvConfig
@@ -100,7 +99,7 @@
         }
 
         databases?.forEach { db ->
-            DatabaseWriter(db, CodeLanguage.JAVA).write(context.processingEnv)
+            DatabaseWriter(db, context.codeLanguage).write(context.processingEnv)
             if (db.exportSchema) {
                 val schemaOutFolderPath = context.schemaOutFolderPath
                 if (schemaOutFolderPath == null) {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
similarity index 61%
rename from room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
rename to room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 9ef5f62..17d262b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -30,7 +30,6 @@
 import androidx.room.compiler.codegen.asMutableClassName
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
@@ -51,15 +50,14 @@
     get() = ArrayTypeName.of(typeName)
 
 object SupportDbTypeNames {
-    val DB: ClassName = ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
+    val DB = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteDatabase")
     val SQLITE_STMT: XClassName =
         XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteStatement")
-    val SQLITE_OPEN_HELPER: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper")
-    val SQLITE_OPEN_HELPER_CALLBACK: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper.Callback")
-    val SQLITE_OPEN_HELPER_CONFIG: ClassName =
-        ClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper.Configuration")
+    val SQLITE_OPEN_HELPER = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper")
+    val SQLITE_OPEN_HELPER_CALLBACK =
+        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Callback")
+    val SQLITE_OPEN_HELPER_CONFIG =
+        XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteOpenHelper", "Configuration")
     val QUERY = XClassName.get("$SQLITE_PACKAGE.db", "SupportSQLiteQuery")
 }
 
@@ -67,7 +65,8 @@
     val STRING_UTIL: XClassName = XClassName.get("$ROOM_PACKAGE.util", "StringUtil")
     val ROOM_DB: XClassName = XClassName.get(ROOM_PACKAGE, "RoomDatabase")
     val ROOM_DB_KT = XClassName.get(ROOM_PACKAGE, "RoomDatabaseKt")
-    val ROOM_DB_CONFIG: ClassName = ClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
+    val ROOM_DB_CALLBACK = XClassName.get(ROOM_PACKAGE, "RoomDatabase", "Callback")
+    val ROOM_DB_CONFIG = XClassName.get(ROOM_PACKAGE, "DatabaseConfiguration")
     val INSERTION_ADAPTER: XClassName =
         XClassName.get(ROOM_PACKAGE, "EntityInsertionAdapter")
     val UPSERTION_ADAPTER: XClassName =
@@ -76,45 +75,34 @@
         XClassName.get(ROOM_PACKAGE, "EntityDeletionOrUpdateAdapter")
     val SHARED_SQLITE_STMT: XClassName =
         XClassName.get(ROOM_PACKAGE, "SharedSQLiteStatement")
-    val INVALIDATION_TRACKER: ClassName =
-        ClassName.get(ROOM_PACKAGE, "InvalidationTracker")
+    val INVALIDATION_TRACKER = XClassName.get(ROOM_PACKAGE, "InvalidationTracker")
     val INVALIDATION_OBSERVER: ClassName =
         ClassName.get("$ROOM_PACKAGE.InvalidationTracker", "Observer")
     val ROOM_SQL_QUERY: XClassName =
         XClassName.get(ROOM_PACKAGE, "RoomSQLiteQuery")
-    val OPEN_HELPER: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper")
-    val OPEN_HELPER_DELEGATE: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper.Delegate")
-    val OPEN_HELPER_VALIDATION_RESULT: ClassName =
-        ClassName.get(ROOM_PACKAGE, "RoomOpenHelper.ValidationResult")
-    val TABLE_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo")
-    val TABLE_INFO_COLUMN: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.Column")
-    val TABLE_INFO_FOREIGN_KEY: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.ForeignKey")
-    val TABLE_INFO_INDEX: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "TableInfo.Index")
-    val FTS_TABLE_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "FtsTableInfo")
-    val VIEW_INFO: ClassName =
-        ClassName.get("$ROOM_PACKAGE.util", "ViewInfo")
+    val OPEN_HELPER = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper")
+    val OPEN_HELPER_DELEGATE = XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "Delegate")
+    val OPEN_HELPER_VALIDATION_RESULT =
+        XClassName.get(ROOM_PACKAGE, "RoomOpenHelper", "ValidationResult")
+    val TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "TableInfo")
+    val TABLE_INFO_COLUMN = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Column")
+    val TABLE_INFO_FOREIGN_KEY = XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "ForeignKey")
+    val TABLE_INFO_INDEX =
+        XClassName.get("$ROOM_PACKAGE.util", "TableInfo", "Index")
+    val FTS_TABLE_INFO = XClassName.get("$ROOM_PACKAGE.util", "FtsTableInfo")
+    val VIEW_INFO = XClassName.get("$ROOM_PACKAGE.util", "ViewInfo")
     val LIMIT_OFFSET_DATA_SOURCE: ClassName =
         ClassName.get("$ROOM_PACKAGE.paging", "LimitOffsetDataSource")
     val DB_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "DBUtil")
     val CURSOR_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "CursorUtil")
-    val MIGRATION: ClassName = ClassName.get("$ROOM_PACKAGE.migration", "Migration")
-    val AUTO_MIGRATION_SPEC: ClassName = ClassName.get(
-        "$ROOM_PACKAGE.migration",
-        "AutoMigrationSpec"
-    )
+    val MIGRATION = XClassName.get("$ROOM_PACKAGE.migration", "Migration")
+    val AUTO_MIGRATION_SPEC = XClassName.get("$ROOM_PACKAGE.migration", "AutoMigrationSpec")
     val UUID_UTIL: XClassName =
         XClassName.get("$ROOM_PACKAGE.util", "UUIDUtil")
-    val AMBIGUOUS_COLUMN_RESOLVER: ClassName =
-        ClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
+    val AMBIGUOUS_COLUMN_RESOLVER = XClassName.get(ROOM_PACKAGE, "AmbiguousColumnResolver")
+    val RELATION_UTIL = XClassName.get("androidx.room.util", "RelationUtil")
 }
 
 object PagingTypeNames {
@@ -144,31 +132,32 @@
 
 object AndroidTypeNames {
     val CURSOR: XClassName = XClassName.get("android.database", "Cursor")
-    val BUILD: ClassName = ClassName.get("android.os", "Build")
+    val BUILD = XClassName.get("android.os", "Build")
     val CANCELLATION_SIGNAL: XClassName = XClassName.get("android.os", "CancellationSignal")
 }
 
 object CollectionTypeNames {
-    val ARRAY_MAP: ClassName = ClassName.get(COLLECTION_PACKAGE, "ArrayMap")
-    val LONG_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
-    val INT_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
-}
-
-object KotlinCollectionTypeNames {
-    val MUTABLE_LIST = List::class.asMutableClassName()
+    val ARRAY_MAP = XClassName.get(COLLECTION_PACKAGE, "ArrayMap")
+    val LONG_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
+    val INT_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
 }
 
 object CommonTypeNames {
-    val ARRAYS = ClassName.get("java.util", "Arrays")
-    val LIST = ClassName.get("java.util", "List")
+    val LIST = List::class.asClassName()
+    val MUTABLE_LIST = List::class.asMutableClassName()
     val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
-    val MAP = ClassName.get("java.util", "Map")
-    val SET = ClassName.get("java.util", "Set")
-    val STRING = ClassName.get("java.lang", "String")
+    val MAP = Map::class.asClassName()
+    val MUTABLE_MAP = Map::class.asMutableClassName()
+    val HASH_MAP = XClassName.get("java.util", "HashMap")
+    val SET = Set::class.asClassName()
+    val MUTABLE_SET = Set::class.asMutableClassName()
+    val HASH_SET = XClassName.get("java.util", "HashSet")
+    val STRING = String::class.asClassName()
     val INTEGER = ClassName.get("java.lang", "Integer")
     val OPTIONAL = ClassName.get("java.util", "Optional")
     val UUID = ClassName.get("java.util", "UUID")
-    val BYTE_BUFFER = ClassName.get("java.nio", "ByteBuffer")
+    val BYTE_BUFFER = XClassName.get("java.nio", "ByteBuffer")
+    val JAVA_CLASS = XClassName.get("java.lang", "Class")
 }
 
 object GuavaBaseTypeNames {
@@ -259,15 +248,28 @@
     val RECEIVE_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "ReceiveChannel")
     val SEND_CHANNEL = ClassName.get("kotlinx.coroutines.channels", "SendChannel")
     val FLOW = ClassName.get("kotlinx.coroutines.flow", "Flow")
+    val LAZY = XClassName.get("kotlin", "Lazy")
 }
 
 object RoomMemberNames {
+    val DB_UTIL_QUERY = RoomTypeNames.DB_UTIL.packageMember("query")
+    val DB_UTIL_DROP_FTS_SYNC_TRIGGERS = RoomTypeNames.DB_UTIL.packageMember("dropFtsSyncTriggers")
     val CURSOR_UTIL_GET_COLUMN_INDEX =
         RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndex")
+    val CURSOR_UTIL_GET_COLUMN_INDEX_OR_THROW =
+        RoomTypeNames.CURSOR_UTIL.packageMember("getColumnIndexOrThrow")
+    val CURSOR_UTIL_WRAP_MAPPED_COLUMNS =
+        RoomTypeNames.CURSOR_UTIL.packageMember("wrapMappedColumns")
     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 TABLE_INFO_READ =
+        RoomTypeNames.TABLE_INFO.companionMember("read", isJvmStatic = true)
+    val FTS_TABLE_INFO_READ =
+        RoomTypeNames.FTS_TABLE_INFO.companionMember("read", isJvmStatic = true)
+    val VIEW_INFO_READ =
+        RoomTypeNames.VIEW_INFO.companionMember("read", isJvmStatic = true)
 }
 
 val DEFERRED_TYPES = listOf(
@@ -288,10 +290,10 @@
     ReactiveStreamsTypeNames.PUBLISHER
 )
 
-fun TypeName.defaultValue(): String {
+fun XTypeName.defaultValue(): String {
     return if (!isPrimitive) {
         "null"
-    } else if (this == TypeName.BOOLEAN) {
+    } else if (this == XTypeName.PRIMITIVE_BOOLEAN) {
         "false"
     } else {
         "0"
@@ -365,44 +367,141 @@
 }.build()
 
 /**
+ * Generates an array literal with the given [values]
+ *
+ * Example: `ArrayLiteral(XTypeName.PRIMITIVE_INT, 1, 2, 3)`
+ *
+ * For Java will produce: `new int[] {1, 2, 3}`
+ *
+ * For Kotlin will produce: `intArrayOf(1, 2, 3)`,
+ */
+fun ArrayLiteral(
+    language: CodeLanguage,
+    type: XTypeName,
+    vararg values: Any
+): XCodeBlock {
+    val space = when (language) {
+        CodeLanguage.JAVA -> "%W"
+        CodeLanguage.KOTLIN -> " "
+    }
+    val initExpr = when (language) {
+        CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[] ", type)
+        CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
+    }
+    val openingChar = when (language) {
+        CodeLanguage.JAVA -> "{"
+        CodeLanguage.KOTLIN -> "("
+    }
+    val closingChar = when (language) {
+        CodeLanguage.JAVA -> "}"
+        CodeLanguage.KOTLIN -> ")"
+    }
+    return XCodeBlock.of(
+        language,
+        "%L$openingChar%L$closingChar",
+        initExpr,
+        XCodeBlock.builder(language).apply {
+            val joining = Array(values.size) { i ->
+                XCodeBlock.of(
+                    language,
+                    if (type == CommonTypeNames.STRING) "%S" else "%L",
+                    values[i]
+                )
+            }
+            val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+            add(placeholders, *joining)
+        }.build()
+    )
+}
+
+/**
  * Generates a 2D array literal where the value at `i`,`j` will be produced by `valueProducer.
  * For example:
  * ```
- * DoubleArrayLiteral(TypeName.INT, 2, { _ -> 3 }, { i, j -> i + j })
+ * DoubleArrayLiteral(XTypeName.PRIMITIVE_INT, 2, { _ -> 3 }, { i, j -> i + j })
  * ```
- * will produce:
+ * For Java will produce:
  * ```
  * new int[][] {
- *   { 0, 1, 2 },
- *   { 1, 2, 3 }
+ *   {0, 1, 2},
+ *   {1, 2, 3}
  * }
  * ```
+ * For Kotlin will produce:
+ * ```
+ * arrayOf(
+ *   intArrayOf(0, 1, 2),
+ *   intArrayOf(1, 2, 3)
+ * )
+ * ```
  */
 fun DoubleArrayLiteral(
-    type: TypeName,
+    language: CodeLanguage,
+    type: XTypeName,
     rowSize: Int,
     columnSizeProducer: (Int) -> Int,
     valueProducer: (Int, Int) -> Any
-): CodeBlock = CodeBlock.of(
-    "new $T[][] {$W$L$W}", type,
-    CodeBlock.join(
-        List(rowSize) { i ->
-            CodeBlock.of(
-                "{$W$L$W}",
-                CodeBlock.join(
-                    List(columnSizeProducer(i)) { j ->
-                        CodeBlock.of(
-                            if (type == CommonTypeNames.STRING) S else L,
-                            valueProducer(i, j)
-                        )
-                    },
-                    ",$W"
-                ),
-            )
-        },
-        ",$W"
+): XCodeBlock {
+    val space = when (language) {
+        CodeLanguage.JAVA -> "%W"
+        CodeLanguage.KOTLIN -> " "
+    }
+    val outerInit = when (language) {
+        CodeLanguage.JAVA -> XCodeBlock.of(language, "new %T[][] ", type)
+        CodeLanguage.KOTLIN -> XCodeBlock.of(language, "arrayOf")
+    }
+    val innerInit = when (language) {
+        CodeLanguage.JAVA -> XCodeBlock.of(language, "", type)
+        CodeLanguage.KOTLIN -> XCodeBlock.of(language, getArrayOfFunction(type))
+    }
+    val openingChar = when (language) {
+        CodeLanguage.JAVA -> "{"
+        CodeLanguage.KOTLIN -> "("
+    }
+    val closingChar = when (language) {
+        CodeLanguage.JAVA -> "}"
+        CodeLanguage.KOTLIN -> ")"
+    }
+    return XCodeBlock.of(
+        language,
+        "%L$openingChar%L$closingChar",
+        outerInit,
+        XCodeBlock.builder(language).apply {
+            val joining = Array(rowSize) { i ->
+                XCodeBlock.of(
+                    language,
+                    "%L$openingChar%L$closingChar",
+                    innerInit,
+                    XCodeBlock.builder(language).apply {
+                        val joining = Array(columnSizeProducer(i)) { j ->
+                            XCodeBlock.of(
+                                language,
+                                if (type == CommonTypeNames.STRING) "%S" else "%L",
+                                valueProducer(i, j)
+                            )
+                        }
+                        val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+                        add(placeholders, *joining)
+                    }.build()
+                )
+            }
+            val placeholders = joining.joinToString(separator = ",$space") { "%L" }
+            add(placeholders, *joining)
+        }.build()
     )
-)
+}
+
+private fun getArrayOfFunction(type: XTypeName) = when (type) {
+    XTypeName.PRIMITIVE_BOOLEAN -> "booleanArrayOf"
+    XTypeName.PRIMITIVE_BYTE -> "byteArrayOf"
+    XTypeName.PRIMITIVE_SHORT -> "shortArrayOf"
+    XTypeName.PRIMITIVE_INT -> "intArrayOf"
+    XTypeName.PRIMITIVE_LONG -> "longArrayOf"
+    XTypeName.PRIMITIVE_CHAR -> "charArrayOf"
+    XTypeName.PRIMITIVE_FLOAT -> "floatArrayOf"
+    XTypeName.PRIMITIVE_DOUBLE -> "doubleArrayOf"
+    else -> "arrayOf"
+}
 
 /**
  * Code of expression for [Collection.size] in Kotlin, and [java.util.Collection.size] for Java.
@@ -426,4 +525,16 @@
         CodeLanguage.KOTLIN -> "%L.size" // kotlin.Array.size and primitives (e.g. IntArray)
     },
     varName
+)
+
+/**
+ * Code of expression for [Map.keys] in Kotlin, and [java.util.Map.keySet] for Java.
+ */
+fun MapKeySetExprCode(language: CodeLanguage, varName: String) = XCodeBlock.of(
+    language,
+    when (language) {
+        CodeLanguage.JAVA -> "%L.keySet()" // java.util.Map.keySet()
+        CodeLanguage.KOTLIN -> "%L.keys" // kotlin.collections.Map.keys
+    },
+    varName
 )
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
index 7272936..1bb79c5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xtype_ext.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.ext
 
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isByte
@@ -60,7 +61,7 @@
 /**
  * Returns `true` if this is a `ByteBuffer` type.
  */
-fun XType.isByteBuffer(): Boolean = typeName == BYTE_BUFFER
+fun XType.isByteBuffer(): Boolean = asTypeName() == BYTE_BUFFER
 
 /**
  * Returns `true` if this represents a `UUID` type.
@@ -111,7 +112,7 @@
 fun XType.isSupportedMapTypeArg(): Boolean {
     if (this.typeName.isPrimitive) return true
     if (this.typeName.isBoxedPrimitive) return true
-    if (this.typeName == CommonTypeNames.STRING) return true
+    if (this.typeName == CommonTypeNames.STRING.toJavaPoet()) return true
     if (this.isTypeOf(ByteArray::class)) return true
     if (this.isArray() && this.isByte()) return true
     val typeElement = this.typeElement ?: return false
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
index b76b817..f8266b0 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/parser/SqlParser.kt
@@ -17,17 +17,17 @@
 package androidx.room.parser
 
 import androidx.room.ColumnInfo
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
 import androidx.room.ext.CommonTypeNames
 import androidx.room.parser.expansion.isCoreSelect
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.TypeName
+import java.util.Locale
 import org.antlr.v4.runtime.tree.ParseTree
 import org.antlr.v4.runtime.tree.TerminalNode
-import java.util.Locale
 
-@Suppress("FunctionName")
 class QueryVisitor(
     private val original: String,
     private val syntaxErrors: List<String>,
@@ -269,7 +269,7 @@
 
     fun getTypeMirrors(env: XProcessingEnv): List<XType>? {
         return when (this) {
-            TEXT -> withBoxedAndNullableTypes(env, CommonTypeNames.STRING)
+            TEXT -> withBoxedAndNullableTypes(env, CommonTypeNames.STRING.toJavaPoet())
             INTEGER -> withBoxedAndNullableTypes(
                 env, TypeName.INT, TypeName.BYTE, TypeName.CHAR,
                 TypeName.LONG, TypeName.SHORT
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
index 14ec3a8..a3bc71c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/Context.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.RewriteQueriesToDropUnusedColumns
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XElement
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
@@ -133,14 +134,16 @@
             processingEnv.requireType("java.lang.Void")
         }
         val STRING: XType by lazy {
-            processingEnv.requireType("java.lang.String")
+            processingEnv.requireType(String::class.asClassName())
         }
         val READONLY_COLLECTION: XType by lazy {
-            if (processingEnv.backend == XProcessingEnv.Backend.KSP) {
-                processingEnv.requireType("kotlin.collections.Collection")
-            } else {
-                processingEnv.requireType("java.util.Collection")
-            }
+            processingEnv.requireType(Collection::class.asClassName())
+        }
+        val LIST: XType by lazy {
+            processingEnv.requireType(List::class.asClassName())
+        }
+        val SET: XType by lazy {
+            processingEnv.requireType(Set::class.asClassName())
         }
     }
 
@@ -168,9 +171,33 @@
         return Pair(result, collector)
     }
 
-    fun fork(element: XElement, forceSuppressedWarnings: Set<Warning> = emptySet()): Context {
+    /**
+     * Forks the processor context adding suppressed warnings a type converters found in the
+     * given [element].
+     *
+     * @param element the element from which to create the fork.
+     * @param forceSuppressedWarnings the warning that will be silenced regardless if they are
+     * present or not in the [element].
+     * @param forceBuiltInConverters the built-in converter states that will be set regardless of
+     * the states found in the [element].
+     */
+    fun fork(
+        element: XElement,
+        forceSuppressedWarnings: Set<Warning> = emptySet(),
+        forceBuiltInConverters: BuiltInConverterFlags? = null
+    ): Context {
         val suppressedWarnings = SuppressWarningProcessor.getSuppressedWarnings(element)
-        val processConvertersResult = CustomConverterProcessor.findConverters(this, element)
+        val processConvertersResult =
+            CustomConverterProcessor.findConverters(this, element).let { result ->
+                if (forceBuiltInConverters != null) {
+                    result.copy(
+                        builtInConverterFlags =
+                            result.builtInConverterFlags.withNext(forceBuiltInConverters)
+                    )
+                } else {
+                    result
+                }
+            }
         val subBuiltInConverterFlags = typeConverters.builtInConverterFlags.withNext(
             processConvertersResult.builtInConverterFlags
         )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
index e1fe4c3..ad2acae 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/CustomConverterProcessor.kt
@@ -184,16 +184,18 @@
     /**
      * Order of classes is important hence they are a LinkedHashSet not a set.
      */
-    open class ProcessResult(
+    data class ProcessResult(
         val classes: LinkedHashSet<XType>,
         val converters: List<CustomTypeConverterWrapper>,
         val builtInConverterFlags: BuiltInConverterFlags
     ) {
-        object EMPTY : ProcessResult(
-            classes = LinkedHashSet(),
-            converters = emptyList(),
-            builtInConverterFlags = BuiltInConverterFlags.DEFAULT
-        )
+        companion object {
+            val EMPTY = ProcessResult(
+                classes = LinkedHashSet(),
+                converters = emptyList(),
+                builtInConverterFlags = BuiltInConverterFlags.DEFAULT
+            )
+        }
 
         operator fun plus(other: ProcessResult): ProcessResult {
             val newClasses = LinkedHashSet<XType>()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
index 140c869..6252b56 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/PojoProcessor.kt
@@ -743,7 +743,8 @@
                     fieldName = field.name,
                     jvmName = field.name,
                     type = field.type,
-                    callType = CallType.FIELD
+                    callType = CallType.FIELD,
+                    isMutableField = !field.element.isFinal()
                 )
             },
             assignFromMethod = { match ->
@@ -756,7 +757,8 @@
                             CallType.SYNTHETIC_METHOD
                         } else {
                             CallType.METHOD
-                        }
+                        },
+                    isMutableField = !field.element.isFinal()
                 )
             },
             reportAmbiguity = { matching ->
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 0781870..ff8e11b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -17,6 +17,7 @@
 package androidx.room.solver
 
 import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
 import androidx.room.compiler.processing.isEnum
@@ -72,7 +73,9 @@
 import androidx.room.solver.query.result.ImmutableMapQueryResultAdapter
 import androidx.room.solver.query.result.ListQueryResultAdapter
 import androidx.room.solver.query.result.MapQueryResultAdapter
+import androidx.room.solver.query.result.MultimapQueryResultAdapter
 import androidx.room.solver.query.result.MultimapQueryResultAdapter.Companion.validateMapTypeArgs
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
 import androidx.room.solver.query.result.OptionalQueryResultAdapter
 import androidx.room.solver.query.result.PojoRowAdapter
 import androidx.room.solver.query.result.QueryResultAdapter
@@ -598,27 +601,26 @@
                 immutableClassName = immutableClassName
             )
         } else if (typeMirror.isTypeOf(java.util.Map::class) ||
-            typeMirror.rawType.typeName == ARRAY_MAP ||
-            typeMirror.rawType.typeName == LONG_SPARSE_ARRAY ||
-            typeMirror.rawType.typeName == INT_SPARSE_ARRAY
+            typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet() ||
+            typeMirror.rawType.typeName == LONG_SPARSE_ARRAY.toJavaPoet() ||
+            typeMirror.rawType.typeName == INT_SPARSE_ARRAY.toJavaPoet()
         ) {
-            val keyTypeArg = if (typeMirror.rawType.typeName == LONG_SPARSE_ARRAY) {
-                context.processingEnv.requireType(TypeName.LONG)
-            } else if (typeMirror.rawType.typeName == INT_SPARSE_ARRAY) {
-                context.processingEnv.requireType(TypeName.INT)
-            } else {
-                typeMirror.typeArguments[0].extendsBoundOrSelf()
+            val mapType = when (typeMirror.rawType.asTypeName()) {
+                LONG_SPARSE_ARRAY -> MultimapQueryResultAdapter.MapType.LONG_SPARSE
+                INT_SPARSE_ARRAY -> MultimapQueryResultAdapter.MapType.INT_SPARSE
+                ARRAY_MAP -> MultimapQueryResultAdapter.MapType.ARRAY_MAP
+                else -> MultimapQueryResultAdapter.MapType.DEFAULT
+            }
+            val keyTypeArg = when (mapType) {
+                MultimapQueryResultAdapter.MapType.LONG_SPARSE ->
+                    context.processingEnv.requireType(TypeName.LONG)
+                MultimapQueryResultAdapter.MapType.INT_SPARSE ->
+                    context.processingEnv.requireType(TypeName.INT)
+                else ->
+                    typeMirror.typeArguments[0].extendsBoundOrSelf()
             }
 
-            val isSparseArray = if (typeMirror.rawType.typeName == LONG_SPARSE_ARRAY) {
-                LONG_SPARSE_ARRAY
-            } else if (typeMirror.rawType.typeName == INT_SPARSE_ARRAY) {
-                INT_SPARSE_ARRAY
-            } else {
-                null
-            }
-
-            val mapValueTypeArg = if (isSparseArray != null) {
+            val mapValueTypeArg = if (mapType.isSparseArray()) {
                 typeMirror.typeArguments[0].extendsBoundOrSelf()
             } else {
                 typeMirror.typeArguments[1].extendsBoundOrSelf()
@@ -639,48 +641,53 @@
             if (collectionTypeRaw.isAssignableFrom(mapValueTypeArg.rawType)) {
                 // The Map's value type argument is assignable to a Collection, we need to make
                 // sure it is either a list or a set.
-                if (
-                    mapValueTypeArg.isTypeOf(java.util.List::class) ||
-                    mapValueTypeArg.isTypeOf(java.util.Set::class)
-                ) {
-                    val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
-
-                    val keyRowAdapter = findRowAdapter(
-                        typeMirror = keyTypeArg,
-                        query = query,
-                        columnName = mapInfo?.keyColumnName
-                    ) ?: return null
-
-                    val valueRowAdapter = findRowAdapter(
-                        typeMirror = valueTypeArg,
-                        query = query,
-                        columnName = mapInfo?.valueColumnName
-                    ) ?: return null
-
-                    validateMapTypeArgs(
-                        keyTypeArg = keyTypeArg,
-                        valueTypeArg = valueTypeArg,
-                        keyReader = findCursorValueReader(keyTypeArg, null),
-                        valueReader = findCursorValueReader(valueTypeArg, null),
-                        mapInfo = mapInfo,
-                        logger = context.logger
-                    )
-                    return MapQueryResultAdapter(
-                        context = context,
-                        parsedQuery = query,
-                        keyTypeArg = keyTypeArg,
-                        valueTypeArg = valueTypeArg,
-                        keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
-                        valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
-                        valueCollectionType = mapValueTypeArg,
-                        isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP,
-                        isSparseArray = isSparseArray
-                    )
-                } else {
-                    context.logger.e(
-                        valueCollectionMustBeListOrSet(mapValueTypeArg.typeName)
-                    )
+                val listTypeRaw = context.COMMON_TYPES.LIST.rawType
+                val setTypeRaw = context.COMMON_TYPES.SET.rawType
+                val collectionValueType = when {
+                    mapValueTypeArg.rawType.isAssignableFrom(listTypeRaw) ->
+                        MultimapQueryResultAdapter.CollectionValueType.LIST
+                    mapValueTypeArg.rawType.isAssignableFrom(setTypeRaw) ->
+                        MultimapQueryResultAdapter.CollectionValueType.SET
+                    else -> {
+                        context.logger.e(
+                            valueCollectionMustBeListOrSet(mapValueTypeArg.typeName)
+                        )
+                        return null
+                    }
                 }
+
+                val valueTypeArg = mapValueTypeArg.typeArguments.single().extendsBoundOrSelf()
+
+                val keyRowAdapter = findRowAdapter(
+                    typeMirror = keyTypeArg,
+                    query = query,
+                    columnName = mapInfo?.keyColumnName
+                ) ?: return null
+
+                val valueRowAdapter = findRowAdapter(
+                    typeMirror = valueTypeArg,
+                    query = query,
+                    columnName = mapInfo?.valueColumnName
+                ) ?: return null
+
+                validateMapTypeArgs(
+                    keyTypeArg = keyTypeArg,
+                    valueTypeArg = valueTypeArg,
+                    keyReader = findCursorValueReader(keyTypeArg, null),
+                    valueReader = findCursorValueReader(valueTypeArg, null),
+                    mapInfo = mapInfo,
+                    logger = context.logger
+                )
+                return MapQueryResultAdapter(
+                    context = context,
+                    parsedQuery = query,
+                    keyTypeArg = keyTypeArg,
+                    valueTypeArg = valueTypeArg,
+                    keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
+                    valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
+                    valueCollectionType = collectionValueType,
+                    mapType = mapType
+                )
             } else {
                 val keyRowAdapter = findRowAdapter(
                     typeMirror = keyTypeArg,
@@ -709,8 +716,7 @@
                     keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
                     valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
                     valueCollectionType = null,
-                    isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP,
-                    isSparseArray = isSparseArray
+                    mapType = mapType
                 )
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
index 6381625..31c6b80 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/AmbiguousColumnIndexAdapter.kt
@@ -17,16 +17,14 @@
 package androidx.room.solver.query.result
 
 import androidx.room.AmbiguousColumnResolver
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.DoubleArrayLiteral
-import androidx.room.ext.L
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.T
-import androidx.room.ext.W
 import androidx.room.parser.ParsedQuery
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.ColumnIndexVar
-import com.squareup.javapoet.TypeName
 
 /**
  * An index adapter that uses [AmbiguousColumnResolver] to create the index variables for
@@ -46,7 +44,7 @@
      */
     override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
         val cursorIndexMappingVarName = scope.getTmpVar("_cursorIndices")
-        scope.builder().apply {
+        scope.builder.apply {
             val resultInfo = query.resultInfo
             if (resultInfo != null && query.hasTopStarProjection == false) {
                 // Query result columns are known, use ambiguous column resolver at compile-time
@@ -56,34 +54,42 @@
                     mappings = mappings.map { it.usedColumns.toTypedArray() }.toTypedArray()
                 )
                 val rowMappings = DoubleArrayLiteral(
-                    type = TypeName.INT,
+                    language = language,
+                    type = XTypeName.PRIMITIVE_INT,
                     rowSize = cursorIndices.size,
                     columnSizeProducer = { i -> cursorIndices[i].size },
                     valueProducer = { i, j -> cursorIndices[i][j] }
                 )
-                addStatement(
-                    "final $T[][] $L = $L",
-                    TypeName.INT,
-                    cursorIndexMappingVarName,
-                    rowMappings
+                addLocalVariable(
+                    name = cursorIndexMappingVarName,
+                    typeName = XTypeName.getArrayName(
+                        XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+                    ),
+                    assignExpr = rowMappings
                 )
             } else {
                 // Generate code that uses ambiguous column resolver at runtime, providing the
                 // query result column names from the Cursor and the result object column names in
                 // an array literal.
                 val rowMappings = DoubleArrayLiteral(
+                    language = language,
                     type = CommonTypeNames.STRING,
                     rowSize = mappings.size,
                     columnSizeProducer = { i -> mappings[i].usedColumns.size },
                     valueProducer = { i, j -> mappings[i].usedColumns[j] }
                 )
-                addStatement(
-                    "final $T[][] $L = $T.resolve($L.getColumnNames(),$W$L)",
-                    TypeName.INT,
-                    cursorIndexMappingVarName,
-                    RoomTypeNames.AMBIGUOUS_COLUMN_RESOLVER,
-                    cursorVarName,
-                    rowMappings
+                addLocalVariable(
+                    name = cursorIndexMappingVarName,
+                    typeName = XTypeName.getArrayName(
+                        XTypeName.getArrayName(XTypeName.PRIMITIVE_INT)
+                    ),
+                    assignExpr = XCodeBlock.of(
+                        language,
+                        "%T.resolve(%L.getColumnNames(), %L)",
+                        RoomTypeNames.AMBIGUOUS_COLUMN_RESOLVER,
+                        cursorVarName,
+                        rowMappings
+                    )
                 )
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
index 0139594..4bebca1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/EntityRowAdapter.kt
@@ -16,23 +16,18 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XFunSpec
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.AndroidTypeNames.CURSOR
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.ext.AndroidTypeNames
+import androidx.room.ext.ArrayLiteral
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.S
-import androidx.room.ext.T
-import androidx.room.ext.W
+import androidx.room.ext.RoomMemberNames
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.ColumnIndexVar
 import androidx.room.vo.Entity
 import androidx.room.vo.columnNames
 import androidx.room.writer.EntityCursorConverterWriter
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.TypeName
 
 class EntityRowAdapter(val entity: Entity) : QueryMappedRowAdapter(entity.type) {
 
@@ -47,12 +42,15 @@
         private var indexVars: List<ColumnIndexVar>? = null
 
         override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
-            indexVars = entity.columnNames.map {
+            indexVars = entity.columnNames.map { columnName ->
                 ColumnIndexVar(
-                    column = it,
-                    indexVar = CodeBlock.of(
-                        "$T.getColumnIndex($N, $S)",
-                        CURSOR_UTIL.toJavaPoet(), cursorVarName, it
+                    column = columnName,
+                    indexVar = XCodeBlock.of(
+                        scope.language,
+                        "%M(%L, %S)",
+                        RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX,
+                        cursorVarName,
+                        columnName
                     ).toString()
                 )
             }
@@ -76,24 +74,27 @@
             // solely used in the shared converter method and whose getColumnIndex() is overridden
             // to return the resolved column index.
             cursorDelegateVarName = scope.getTmpVar("_wrappedCursor")
-            val entityColumnNamesParam = CodeBlock.of(
-                "new $T[] { $L }",
+            val entityColumnNamesParam = ArrayLiteral(
+                scope.language,
                 CommonTypeNames.STRING,
-                CodeBlock.join(entity.columnNames.map { CodeBlock.of(S, it) }, ",$W")
+                *entity.columnNames.toTypedArray()
             )
-            val entityColumnIndicesParam = CodeBlock.of(
-                "new $T[] { $L }",
-                TypeName.INT,
-                CodeBlock.join(indices.map { CodeBlock.of(L, it.indexVar) }, ",$W")
+            val entityColumnIndicesParam = ArrayLiteral(
+                scope.language,
+                XTypeName.PRIMITIVE_INT,
+                *indices.map { it.indexVar }.toTypedArray()
             )
-            scope.builder().addStatement(
-                "final $T $N = $T.wrapMappedColumns($N, $L, $L)",
-                CURSOR.toJavaPoet(),
-                cursorDelegateVarName,
-                CURSOR_UTIL.toJavaPoet(),
-                cursorVarName,
-                entityColumnNamesParam,
-                entityColumnIndicesParam
+            scope.builder.addLocalVariable(
+                checkNotNull(cursorDelegateVarName),
+                AndroidTypeNames.CURSOR,
+                assignExpr = XCodeBlock.of(
+                    scope.language,
+                    "%M(%L, %L, %L)",
+                    RoomMemberNames.CURSOR_UTIL_WRAP_MAPPED_COLUMNS,
+                    cursorVarName,
+                    entityColumnNamesParam,
+                    entityColumnIndicesParam
+                )
             )
         }
         functionSpec = scope.writer.getOrCreateFunction(EntityCursorConverterWriter(entity))
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
index 8698fae..5b991a1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/GuavaImmutableMultimapQueryResultAdapter.kt
@@ -43,7 +43,7 @@
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
         val mapVarName = scope.getTmpVar("_mapBuilder")
 
-        scope.builder().apply {
+        scope.builder.apply {
             val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
             if (duplicateColumns.isNotEmpty()) {
                 // There are duplicate columns in the result objects, generate code that provides
@@ -86,6 +86,7 @@
                     dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
                         ?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
                 val columnNullCheckCodeBlock = getColumnNullCheckCode(
+                    language = language,
                     cursorVarName = cursorVarName,
                     indexVars = valueIndexVars
                 )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
index 2ef7f52..df6d058 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/ListQueryResultAdapter.kt
@@ -18,8 +18,8 @@
 
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.CommonTypeNames.ARRAY_LIST
-import androidx.room.ext.KotlinCollectionTypeNames.MUTABLE_LIST
 import androidx.room.solver.CodeGenScope
 
 class ListQueryResultAdapter(
@@ -29,7 +29,7 @@
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
         scope.builder.apply {
             rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
-            val listTypeName = MUTABLE_LIST.parametrizedBy(typeArg.asTypeName())
+            val listTypeName = CommonTypeNames.MUTABLE_LIST.parametrizedBy(typeArg.asTypeName())
             addLocalVariable(
                 name = outVarName,
                 typeName = listTypeName,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
index a14741b..fd8d8f1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MapQueryResultAdapter.kt
@@ -16,15 +16,16 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.CollectionTypeNames.ARRAY_MAP
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.CommonTypeNames
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
+import androidx.room.solver.query.result.MultimapQueryResultAdapter.MapType.Companion.isSparseArray
 
 class MapQueryResultAdapter(
     context: Context,
@@ -33,58 +34,54 @@
     override val valueTypeArg: XType,
     private val keyRowAdapter: QueryMappedRowAdapter,
     private val valueRowAdapter: QueryMappedRowAdapter,
-    private val valueCollectionType: XType?,
-    isArrayMap: Boolean = false,
-    private val isSparseArray: ClassName? = null,
+    private val valueCollectionType: CollectionValueType?,
+    private val mapType: MapType
 ) : MultimapQueryResultAdapter(context, parsedQuery, listOf(keyRowAdapter, valueRowAdapter)) {
 
-    private val declaredValueType = if (valueCollectionType != null) {
-        ParameterizedTypeName.get(
-            valueCollectionType.typeElement?.className,
-            valueTypeArg.typeName
-        )
+    // The type name of the result map value
+    // For Map<Foo, Bar> it is Bar
+    // for Map<Foo, List<Bar> it is List<Bar>
+    private val valueTypeName = if (valueCollectionType != null) {
+        valueCollectionType.className.parametrizedBy(valueTypeArg.asTypeName())
     } else {
-        valueTypeArg.typeName
+        valueTypeArg.asTypeName()
     }
 
-    private val implValueType = if (valueCollectionType != null) {
-        ParameterizedTypeName.get(
-            declaredToImplCollection[valueCollectionType.typeElement?.className],
-            valueTypeArg.typeName
-        )
-    } else {
-        valueTypeArg.typeName
+    // The type name of the concrete result map value
+    // For Map<Foo, Bar> it is Bar
+    // For Map<Foo, List<Bar> it is ArrayList<Bar>
+    private val implValueTypeName = when (valueCollectionType) {
+        CollectionValueType.LIST ->
+            CommonTypeNames.ARRAY_LIST.parametrizedBy(valueTypeArg.asTypeName())
+        CollectionValueType.SET ->
+            CommonTypeNames.HASH_SET.parametrizedBy(valueTypeArg.asTypeName())
+        else ->
+            valueTypeArg.asTypeName()
     }
 
-    private val mapType = if (isSparseArray != null) {
-        ParameterizedTypeName.get(
-            isSparseArray,
-            declaredValueType
-        )
-    } else {
-        ParameterizedTypeName.get(
-            if (isArrayMap) ARRAY_MAP else ClassName.get(Map::class.java),
-            keyTypeArg.typeName,
-            declaredValueType
-        )
+    // The type name of the result map
+    private val mapTypeName = when (mapType) {
+        MapType.DEFAULT, MapType.ARRAY_MAP ->
+            mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
+        MapType.LONG_SPARSE, MapType.INT_SPARSE ->
+            mapType.className.parametrizedBy(valueTypeName)
     }
 
-    private val implMapType = if (isSparseArray != null) {
-        ParameterizedTypeName.get(
-            isSparseArray,
-            declaredValueType
-        )
-    } else {
-        // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
-        ParameterizedTypeName.get(
-            if (isArrayMap) ARRAY_MAP else ClassName.get(LinkedHashMap::class.java),
-            keyTypeArg.typeName,
-            declaredValueType
-        )
+    // The type name of the concrete result map
+    private val implMapTypeName = when (mapType) {
+        MapType.DEFAULT ->
+            // LinkedHashMap is used as impl to preserve key ordering for ordered query results.
+            LinkedHashMap::class.asClassName().parametrizedBy(
+                keyTypeArg.asTypeName(), valueTypeName
+            )
+        MapType.ARRAY_MAP ->
+            mapType.className.parametrizedBy(keyTypeArg.asTypeName(), valueTypeName)
+        MapType.LONG_SPARSE, MapType.INT_SPARSE ->
+            mapType.className.parametrizedBy(valueTypeName)
     }
 
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             val dupeColumnsIndexAdapter: AmbiguousColumnIndexAdapter?
             if (duplicateColumns.isNotEmpty()) {
                 // There are duplicate columns in the result objects, generate code that provides
@@ -108,18 +105,23 @@
                 }
             }
 
-            addStatement("final $T $L = new $T()", mapType, outVarName, implMapType)
+            addLocalVariable(
+                name = outVarName,
+                typeName = mapTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(language, implMapTypeName)
+            )
 
             val tmpKeyVarName = scope.getTmpVar("_key")
             val tmpValueVarName = scope.getTmpVar("_value")
-            beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", keyTypeArg.typeName, tmpKeyVarName)
+            beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+                addLocalVariable(tmpKeyVarName, keyTypeArg.asTypeName())
                 keyRowAdapter.convert(tmpKeyVarName, cursorVarName, scope)
 
                 val valueIndexVars =
                     dupeColumnsIndexAdapter?.getIndexVarsForMapping(valueRowAdapter.mapping)
                         ?: valueRowAdapter.getDefaultIndexAdapter().getIndexVars()
                 val columnNullCheckCodeBlock = getColumnNullCheckCode(
+                    language = language,
                     cursorVarName = cursorVarName,
                     indexVars = valueIndexVars
                 )
@@ -128,23 +130,33 @@
                 // opposed to a 1-to-many mapping.
                 if (valueCollectionType != null) {
                     val tmpCollectionVarName = scope.getTmpVar("_values")
-                    addStatement("$T $L", declaredValueType, tmpCollectionVarName)
+                    addLocalVariable(tmpCollectionVarName, valueTypeName)
 
-                    if (isSparseArray != null) {
-                        beginControlFlow("if ($L.get($L) != null)", outVarName, tmpKeyVarName)
+                    if (mapType.isSparseArray()) {
+                        beginControlFlow("if (%L.get(%L) != null)", outVarName, tmpKeyVarName)
                     } else {
-                        beginControlFlow("if ($L.containsKey($L))", outVarName, tmpKeyVarName)
+                        beginControlFlow("if (%L.containsKey(%L))", outVarName, tmpKeyVarName)
                     }.apply {
+                        val getFunction = when (language) {
+                            CodeLanguage.JAVA -> "get"
+                            CodeLanguage.KOTLIN ->
+                                if (mapType.isSparseArray()) "get" else "getValue"
+                        }
                         addStatement(
-                            "$L = $L.get($L)",
+                            "%L = %L.%L(%L)",
                             tmpCollectionVarName,
                             outVarName,
+                            getFunction,
                             tmpKeyVarName
                         )
                     }.nextControlFlow("else").apply {
-                        addStatement("$L = new $T()", tmpCollectionVarName, implValueType)
                         addStatement(
-                            "$L.put($L, $L)",
+                            "%L = %L",
+                            tmpCollectionVarName,
+                            XCodeBlock.ofNewInstance(language, implValueTypeName)
+                        )
+                        addStatement(
+                            "%L.put(%L, %L)",
                             outVarName,
                             tmpKeyVarName,
                             tmpCollectionVarName
@@ -153,37 +165,41 @@
 
                     // Perform value columns null check, in a 1-to-many mapping we still add the key
                     // with an empty collection as the value entry.
-                    beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
+                    beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
                         addStatement("continue")
                     }.endControlFlow()
 
-                    addStatement("final $T $L", valueTypeArg.typeName, tmpValueVarName)
+                    addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
                     valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
-                    addStatement("$L.add($L)", tmpCollectionVarName, tmpValueVarName)
+                    addStatement("%L.add(%L)", tmpCollectionVarName, tmpValueVarName)
                 } else {
                     // Perform value columns null check, in a 1-to-1 mapping we still add the key
-                    // with a null value entry.
-                    beginControlFlow("if ($L)", columnNullCheckCodeBlock).apply {
-                        addStatement("$L.put($L, null)", outVarName, tmpKeyVarName)
-                        addStatement("continue")
+                    // with a null value entry if permitted.
+                    beginControlFlow("if (%L)", columnNullCheckCodeBlock).apply {
+                        if (
+                            language == CodeLanguage.KOTLIN &&
+                            valueTypeArg.nullability == XNullability.NONNULL
+                        ) {
+                            // TODO(b/249984504): Generate / output a better message.
+                            addStatement("error(%S)", "Missing value for a key.")
+                        } else {
+                            addStatement("%L.put(%L, null)", outVarName, tmpKeyVarName)
+                            addStatement("continue")
+                        }
                     }.endControlFlow()
 
-                    addStatement(
-                        "final $T $L",
-                        valueTypeArg.typeElement?.className,
-                        tmpValueVarName
-                    )
+                    addLocalVariable(tmpValueVarName, valueTypeArg.asTypeName())
                     valueRowAdapter.convert(tmpValueVarName, cursorVarName, scope)
 
                     // For consistency purposes, in the one-to-one object mapping case, if
                     // multiple values are encountered for the same key, we will only consider
                     // the first ever encountered mapping.
-                    if (isSparseArray != null) {
-                        beginControlFlow("if ($L.get($L) == null)", outVarName, tmpKeyVarName)
+                    if (mapType.isSparseArray()) {
+                        beginControlFlow("if (%L.get(%L) == null)", outVarName, tmpKeyVarName)
                     } else {
-                        beginControlFlow("if (!$L.containsKey($L))", outVarName, tmpKeyVarName)
+                        beginControlFlow("if (!%L.containsKey(%L))", outVarName, tmpKeyVarName)
                     }.apply {
-                        addStatement("$L.put($L, $L)", outVarName, tmpKeyVarName, tmpValueVarName)
+                        addStatement("%L.put(%L, %L)", outVarName, tmpKeyVarName, tmpValueVarName)
                     }.endControlFlow()
                 }
             }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
index 2320bba..72cbded 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultiTypedPagingSourceQueryResultBinder.kt
@@ -19,7 +19,7 @@
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.ext.AndroidTypeNames.CURSOR
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.L
 import androidx.room.ext.N
 import androidx.room.solver.CodeGenScope
@@ -76,7 +76,7 @@
         return MethodSpec.methodBuilder("convertRows").apply {
             addAnnotation(Override::class.java)
             addModifiers(Modifier.PROTECTED)
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
+            returns(ParameterizedTypeName.get(LIST.toJavaPoet(), itemTypeName))
             val cursorParam = ParameterSpec.builder(CURSOR.toJavaPoet(), "cursor")
                 .build()
             addParameter(cursorParam)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
index 3b2c0f3..41f6df3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/MultimapQueryResultAdapter.kt
@@ -16,10 +16,13 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XClassName
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.W
+import androidx.room.ext.CollectionTypeNames
+import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.implementsEqualsAndHashcode
 import androidx.room.log.RLog
 import androidx.room.parser.ParsedQuery
@@ -32,8 +35,6 @@
 import androidx.room.vo.ColumnIndexVar
 import androidx.room.vo.MapInfo
 import androidx.room.vo.Warning
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
 
 /**
  * Abstract class for Map and Multimap result adapters.
@@ -91,12 +92,23 @@
         }
     }
 
-    companion object {
+    enum class MapType(val className: XClassName) {
+        DEFAULT(CommonTypeNames.MUTABLE_MAP),
+        ARRAY_MAP(CollectionTypeNames.ARRAY_MAP),
+        LONG_SPARSE(CollectionTypeNames.LONG_SPARSE_ARRAY),
+        INT_SPARSE(CollectionTypeNames.INT_SPARSE_ARRAY);
 
-        val declaredToImplCollection = mapOf<ClassName, ClassName>(
-            ClassName.get(List::class.java) to ClassName.get(ArrayList::class.java),
-            ClassName.get(Set::class.java) to ClassName.get(HashSet::class.java)
-        )
+        companion object {
+            fun MapType.isSparseArray() = this == LONG_SPARSE || this == INT_SPARSE
+        }
+    }
+
+    enum class CollectionValueType(val className: XClassName) {
+        LIST(CommonTypeNames.MUTABLE_LIST),
+        SET(CommonTypeNames.MUTABLE_SET)
+    }
+
+    companion object {
 
         /**
          * Checks if the @MapInfo annotation is needed for clarification regarding the return type
@@ -144,16 +156,23 @@
      * Generates a code expression that verifies if all matched fields are null.
      */
     fun getColumnNullCheckCode(
+        language: CodeLanguage,
         cursorVarName: String,
         indexVars: List<ColumnIndexVar>
-    ): CodeBlock {
+    ) = XCodeBlock.builder(language).apply {
+        val space = when (language) {
+            CodeLanguage.JAVA -> "%W"
+            CodeLanguage.KOTLIN -> " "
+        }
         val conditions = indexVars.map {
-            CodeBlock.of(
-                "$L.isNull($L)",
+            XCodeBlock.of(
+                language,
+                "%L.isNull(%L)",
                 cursorVarName,
                 it.indexVar
             )
         }
-        return CodeBlock.join(conditions, "$W&&$W")
-    }
+        val placeholders = conditions.joinToString(separator = "$space&&$space") { "%L" }
+        add(placeholders, *conditions.toTypedArray())
+    }.build()
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
index 2d1ac5e..d2b4ba1 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PojoRowAdapter.kt
@@ -18,7 +18,6 @@
 
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
 import androidx.room.parser.ParsedQuery
 import androidx.room.processor.Context
 import androidx.room.processor.ProcessorErrors
@@ -121,16 +120,16 @@
     private fun emitRelationCollectorsReady(cursorVarName: String, scope: CodeGenScope) {
         if (relationCollectors.isNotEmpty()) {
             relationCollectors.forEach { it.writeInitCode(scope) }
-            scope.builder().apply {
-                beginControlFlow("while ($L.moveToNext())", cursorVarName).apply {
+            scope.builder.apply {
+                beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
                     relationCollectors.forEach {
                         it.writeReadParentKeyCode(cursorVarName, fieldsWithIndices, scope)
                     }
                 }
                 endControlFlow()
+                addStatement("%L.moveToPosition(-1)", cursorVarName)
             }
-            scope.builder().addStatement("$L.moveToPosition(-1)", cursorVarName)
-            relationCollectors.forEach { it.writeCollectionCode(scope) }
+            relationCollectors.forEach { it.writeFetchRelationCall(scope) }
         }
     }
 
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
index 58bc4d6..d5b08e8 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
@@ -19,7 +19,7 @@
 import androidx.room.compiler.codegen.XPropertySpec
 import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.ext.AndroidTypeNames.CURSOR
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.L
 import androidx.room.ext.N
 import androidx.room.ext.RoomTypeNames
@@ -72,7 +72,7 @@
         MethodSpec.methodBuilder("convertRows").apply {
             addAnnotation(Override::class.java)
             addModifiers(Modifier.PROTECTED)
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, itemTypeName))
+            returns(ParameterizedTypeName.get(LIST.toJavaPoet(), itemTypeName))
             val cursorParam = ParameterSpec.builder(CURSOR.toJavaPoet(), "cursor")
                 .build()
             addParameter(cursorParam)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
index 57a3a43..cfc68ba 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/query/result/SingleNamedColumnRowAdapter.kt
@@ -16,17 +16,14 @@
 
 package androidx.room.solver.query.result
 
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.S
-import androidx.room.ext.T
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.types.CursorValueReader
 import androidx.room.vo.ColumnIndexVar
-import com.squareup.javapoet.TypeName
 import java.util.Locale
 
 /**
@@ -47,14 +44,16 @@
 
         override fun onCursorReady(cursorVarName: String, scope: CodeGenScope) {
             indexVarName = scope.getTmpVar(indexVarNamePrefix)
-            scope.builder().addStatement(
-                "final $T $L = $T.$L($L, $S)",
-                TypeName.INT,
-                indexVarName,
-                CURSOR_UTIL.toJavaPoet(),
-                "getColumnIndexOrThrow",
-                cursorVarName,
-                columnName
+            scope.builder.addLocalVariable(
+                name = indexVarName,
+                typeName = XTypeName.PRIMITIVE_INT,
+                assignExpr = XCodeBlock.of(
+                    scope.language,
+                    "%M(%L, %S)",
+                    RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX_OR_THROW,
+                    cursorVarName,
+                    columnName
+                )
             )
         }
 
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 caada40..828d0db 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
@@ -78,7 +78,7 @@
             XCodeBlock.of(
                 scope.language,
                 "(%L) -> %L",
-                innerContinuationParamName, adapterScope.builder().build()
+                innerContinuationParamName, adapterScope.generate()
             )
         } else {
             Function1TypeSpec(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
index 1bbe2a1..9ae6f33 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/ByteBufferColumnTypeAdapter.kt
@@ -17,12 +17,11 @@
 package androidx.room.solver.types
 
 import androidx.room.compiler.codegen.XCodeBlock
-import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.CommonTypeNames
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.solver.CodeGenScope
-import java.nio.ByteBuffer
 
 class ByteBufferColumnTypeAdapter constructor(out: XType) : ColumnTypeAdapter(
     out = out,
@@ -39,7 +38,7 @@
                 addStatement(
                     "%L = %T.wrap(%L.getBlob(%L))",
                     outVarName,
-                    ByteBuffer::class.asClassName(),
+                    CommonTypeNames.BYTE_BUFFER,
                     cursorVarName,
                     indexVarName
                 )
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
index 2bb2c0c..a07e21c 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CompositeTypeConverter.kt
@@ -27,7 +27,7 @@
     cost = conv1.cost + conv2.cost
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             val conv1Output = conv1.convert(inputVarName, scope)
             conv2.convert(
                 inputVarName = conv1Output,
@@ -38,7 +38,7 @@
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
-        scope.builder().apply {
+        scope.builder.apply {
             val conv1Output = conv1.convert(
                 inputVarName = inputVarName,
                 scope = scope
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
index a62cf47..a5269bb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/CustomTypeConverterWrapper.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.codegen.XFunSpec
 import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.apply
 import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.decapitalize
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.CustomTypeConverter
@@ -85,21 +86,41 @@
     }
 
     private fun providedTypeConverter(scope: CodeGenScope): XFunSpec {
-        val className = custom.className
-        val baseName = className.simpleNames.last().decapitalize(Locale.US)
+        val fieldTypeName = when (scope.language) {
+            CodeLanguage.JAVA -> custom.className
+            CodeLanguage.KOTLIN -> KotlinTypeNames.LAZY.parametrizedBy(custom.className)
+        }
+        val baseName = custom.className.simpleNames.last().decapitalize(Locale.US)
         val converterClassName = custom.className
         scope.writer.addRequiredTypeConverter(converterClassName)
         val converterField = scope.writer.getOrCreateProperty(
             object : TypeWriter.SharedPropertySpec(
-                baseName, custom.className
+                baseName, fieldTypeName
             ) {
-                override val isMutable = true
+                override val isMutable = scope.language == CodeLanguage.JAVA
 
                 override fun getUniqueKey(): String {
                     return "converter_${custom.className}"
                 }
 
                 override fun prepare(writer: TypeWriter, builder: XPropertySpec.Builder) {
+                    // For Kotlin we'll rely on kotlin.Lazy while for Java we'll memoize the
+                    // provided converter in the getter.
+                    if (builder.language == CodeLanguage.KOTLIN) {
+                        builder.apply {
+                            initializer(
+                                XCodeBlock.builder(language).apply {
+                                    beginControlFlow("lazy")
+                                    addStatement(
+                                        "checkNotNull(%L.getTypeConverter(%L))",
+                                        DaoWriter.DB_PROPERTY_NAME,
+                                        XCodeBlock.ofJavaClassLiteral(language, custom.className)
+                                    )
+                                    endControlFlow()
+                                }.build()
+                            )
+                        }
+                    }
                 }
             }
         )
@@ -115,39 +136,40 @@
             ) {
                 val body = buildConvertFunctionBody(builder.language)
                 builder.apply(
-                    // Apply synchronized modifier for Java
+                    // Apply synchronized modifier for Java since function checks and sets the
+                    // converter in the shared field.
                     javaMethodBuilder = {
                         addModifiers(Modifier.SYNCHRONIZED)
-                        builder.addCode(body)
                     },
-                    // Use synchronized std-lib function for Kotlin
-                    kotlinFunctionBuilder = {
-                        beginControlFlow("return synchronized")
-                        builder.addCode(body)
-                        endControlFlow()
-                    }
+                    kotlinFunctionBuilder = { }
                 )
+                builder.addCode(body)
                 builder.returns(custom.className)
             }
 
-            private fun buildConvertFunctionBody(language: CodeLanguage): XCodeBlock {
-                return XCodeBlock.builder(language).apply {
-                    beginControlFlow("if (%N == null)", converterField)
-                    addStatement(
-                        "%N = %L.getTypeConverter(%L)",
-                        converterField,
-                        DaoWriter.DB_PROPERTY_NAME,
-                        XCodeBlock.ofJavaClassLiteral(language, custom.className)
-                    )
-                    endControlFlow()
-                    when (language) {
-                        CodeLanguage.JAVA ->
-                            addStatement("return %N", converterField)
-                        CodeLanguage.KOTLIN ->
-                            addStatement("return@synchronized %N", converterField)
+            private fun buildConvertFunctionBody(
+                language: CodeLanguage
+            ) = XCodeBlock.builder(language).apply {
+                // For Java we implement the memoization logic in the converter getter, meanwhile
+                // for Kotlin we rely on kotlin.Lazy so the getter just delegates to it.
+                when (language) {
+                    CodeLanguage.JAVA -> {
+                        beginControlFlow("if (%N == null)", converterField).apply {
+                            addStatement(
+                                "%N = %L.getTypeConverter(%L)",
+                                converterField,
+                                DaoWriter.DB_PROPERTY_NAME,
+                                XCodeBlock.ofJavaClassLiteral(language, custom.className)
+                            )
+                        }
+                        endControlFlow()
+                        addStatement("return %N", converterField)
                     }
-                }.build()
-            }
+                    CodeLanguage.KOTLIN -> {
+                        addStatement("return %N.value", converterField)
+                    }
+                }
+            }.build()
         }
         return scope.writer.getOrCreateFunction(funSpec)
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
index b127c87..aafefb5 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NoOpConverter.kt
@@ -16,7 +16,6 @@
 
 package androidx.room.solver.types
 
-import androidx.room.ext.L
 import androidx.room.compiler.processing.XType
 import androidx.room.solver.CodeGenScope
 
@@ -32,8 +31,7 @@
     type, type
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder()
-            .addStatement("$L = $L", outputVarName, inputVarName)
+        scope.builder.addStatement("%L = %L", outputVarName, inputVarName)
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
index 288899f..3b30e79 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/NullAwareTypeConverters.kt
@@ -16,22 +16,19 @@
 
 package androidx.room.solver.types
 
-import androidx.annotation.VisibleForTesting
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
-import java.lang.IllegalStateException
 
 /**
  * A type converter that checks if the input is null and returns null instead of calling the
  * [delegate].
  */
 class NullSafeTypeConverter(
-    @VisibleForTesting
-    internal val delegate: TypeConverter
+    val delegate: TypeConverter
 ) : TypeConverter(
     from = delegate.from.makeNullable(),
     to = delegate.to.makeNullable(),
@@ -44,11 +41,13 @@
     }
 
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement("$L = null", outputVarName)
-            nextControlFlow("else")
-            delegate.convert(inputVarName, outputVarName, scope)
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addStatement("%L = null", outputVarName)
+            }
+            nextControlFlow("else").apply {
+                delegate.convert(inputVarName, outputVarName, scope)
+            }
             endControlFlow()
         }
     }
@@ -71,28 +70,48 @@
     }
 
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement(
-                "throw new $T($S)", IllegalStateException::class.java,
-                "Expected non-null ${from.typeName}, but it was null."
-            )
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addIllegalStateException()
+            }
             nextControlFlow("else").apply {
-                addStatement("$L = $L", outputVarName, inputVarName)
+                addStatement("%L = %L", outputVarName, inputVarName)
             }
             endControlFlow()
         }
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
-        scope.builder().apply {
-            beginControlFlow("if($L == null)", inputVarName)
-            addStatement(
-                "throw new $T($S)", IllegalStateException::class.java,
-                "Expected non-null ${from.typeName}, but it was null."
-            )
+        scope.builder.apply {
+            beginControlFlow("if (%L == null)", inputVarName).apply {
+                addIllegalStateException()
+            }
             endControlFlow()
         }
         return inputVarName
     }
+
+    private fun XCodeBlock.Builder.addIllegalStateException() {
+        val message = "Expected non-null ${from.typeName}, but it was null."
+        when (language) {
+            CodeLanguage.JAVA -> {
+                addStatement(
+                    "throw %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        ILLEGAL_STATE_EXCEPTION,
+                        "%S",
+                        message
+                    )
+                )
+            }
+            CodeLanguage.KOTLIN -> {
+                addStatement("error(%S)", message)
+            }
+        }
+    }
+
+    companion object {
+        private val ILLEGAL_STATE_EXCEPTION = IllegalStateException::class.asClassName()
+    }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
index dfda989..d8bb5d3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/StringColumnTypeAdapter.kt
@@ -16,6 +16,7 @@
 
 package androidx.room.solver.types
 
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XProcessingEnv
 import androidx.room.compiler.processing.XType
@@ -68,7 +69,7 @@
 
     companion object {
         fun create(env: XProcessingEnv): List<StringColumnTypeAdapter> {
-            val stringType = env.requireType(CommonTypeNames.STRING)
+            val stringType = env.requireType(CommonTypeNames.STRING.toJavaPoet())
             return if (env.backend == XProcessingEnv.Backend.KSP) {
                 listOf(
                     StringColumnTypeAdapter(stringType.makeNonNullable()),
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
index 2f8ae17..d702c1a 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/TypeConverter.kt
@@ -18,8 +18,6 @@
 
 import androidx.annotation.VisibleForTesting
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -52,8 +50,8 @@
         scope: CodeGenScope
     ): String {
         val outVarName = scope.getTmpVar()
-        scope.builder().apply {
-            addStatement("final $T $L", to.typeName, outVarName)
+        scope.builder.apply {
+            addLocalVariable(outVarName, to.asTypeName())
         }
         doConvert(
             inputVarName = inputVarName,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
index ad07028..2465b26 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/types/UpCastTypeConverter.kt
@@ -17,7 +17,6 @@
 package androidx.room.solver.types
 
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
 import androidx.room.solver.CodeGenScope
 
 /**
@@ -34,9 +33,7 @@
     cost = Cost.UP_CAST
 ) {
     override fun doConvert(inputVarName: String, outputVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
-            addStatement("$L = $L", outputVarName, inputVarName)
-        }
+        scope.builder.addStatement("%L = %L", outputVarName, inputVarName)
     }
 
     override fun doConvert(inputVarName: String, scope: CodeGenScope): String {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
index 3e60540..35248a6 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/AutoMigration.kt
@@ -32,7 +32,7 @@
     val schemaDiff: SchemaDiffResult,
     val isSpecProvided: Boolean,
 ) {
-    val specClassName = specElement?.className
+    val specClassName = specElement?.asClassName()
 
     fun getImplTypeName(databaseClassName: XClassName): XClassName {
         return XClassName.get(
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
index 81678a9..05165a4 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/FieldGetter.kt
@@ -18,15 +18,19 @@
 
 import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
+import androidx.room.ext.capitalize
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.types.StatementValueBinder
+import java.util.Locale
 
 data class FieldGetter(
     val fieldName: String,
     val jvmName: String,
     val type: XType,
-    val callType: CallType
+    val callType: CallType,
+    val isMutableField: Boolean
 ) {
     fun writeGet(ownerVar: String, outVar: String, builder: XCodeBlock.Builder) {
         builder.addLocalVariable(
@@ -43,8 +47,25 @@
         binder: StatementValueBinder,
         scope: CodeGenScope
     ) {
-        val varExpr = getterExpression(ownerVar, scope.language).toString()
-        binder.bindToStmt(stmtParamVar, indexVar, varExpr, scope)
+        val varExpr = getterExpression(ownerVar, scope.language)
+        // A temporary local val is needed in Kotlin if the field or property is mutable (var)
+        // and is nullable since otherwise smart cast will fail indicating that the property
+        // might have changed when binding to statement.
+        val needTempVal = scope.language == CodeLanguage.KOTLIN &&
+            (callType == CallType.FIELD || callType == CallType.SYNTHETIC_METHOD) &&
+            type.nullability != XNullability.NONNULL &&
+            isMutableField
+        if (needTempVal) {
+            val tmpField = scope.getTmpVar("_tmp${fieldName.capitalize(Locale.US)}")
+            scope.builder.addLocalVariable(
+                name = tmpField,
+                typeName = type.asTypeName(),
+                assignExpr = varExpr
+            )
+            binder.bindToStmt(stmtParamVar, indexVar, tmpField, scope)
+        } else {
+            binder.bindToStmt(stmtParamVar, indexVar, varExpr.toString(), scope)
+        }
     }
 
     private fun getterExpression(ownerVar: String, codeLanguage: CodeLanguage): XCodeBlock {
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
index 1d2c0a2..a5d810d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/Relation.kt
@@ -37,7 +37,7 @@
     // the projection for the query
     val projection: List<String>
 ) {
-    val pojoTypeName by lazy { pojoType.typeName }
+    val pojoTypeName by lazy { pojoType.asTypeName() }
 
     fun createLoadAllSql(): String {
         val resultFields = projection.toSet()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
index 606cfda..219cb0d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/vo/RelationCollector.kt
@@ -16,18 +16,22 @@
 
 package androidx.room.vo
 
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.processing.XType
+import androidx.room.BuiltInTypeConverters
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.processing.XNullability
 import androidx.room.ext.CollectionTypeNames
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.parser.ParsedQuery
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.parser.SqlParser
 import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors.ISSUE_TRACKER_LINK
 import androidx.room.processor.ProcessorErrors.cannotFindQueryResultAdapter
 import androidx.room.processor.ProcessorErrors.relationAffinityMismatch
 import androidx.room.processor.ProcessorErrors.relationJunctionChildAffinityMismatch
@@ -36,13 +40,10 @@
 import androidx.room.solver.query.parameter.QueryParameterAdapter
 import androidx.room.solver.query.result.RowAdapter
 import androidx.room.solver.query.result.SingleColumnRowAdapter
+import androidx.room.solver.types.CursorValueReader
 import androidx.room.verifier.DatabaseVerificationErrors
 import androidx.room.writer.QueryWriter
 import androidx.room.writer.RelationCollectorFunctionWriter
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
 import java.nio.ByteBuffer
 import java.util.Locale
 
@@ -51,14 +52,27 @@
  */
 data class RelationCollector(
     val relation: Relation,
+    // affinity between relation fields
     val affinity: SQLTypeAffinity,
-    val mapTypeName: ParameterizedTypeName,
-    val keyTypeName: TypeName,
-    val relationTypeName: TypeName,
+    // concrete map type name to store relationship
+    val mapTypeName: XTypeName,
+    // map key type name, not the same as the parent or entity field type
+    val keyTypeName: XTypeName,
+    // map value type name, it is assignable to the @Relation field
+    val relationTypeName: XTypeName,
+    // query writer for the relating entity query
     val queryWriter: QueryWriter,
+    // key reader for the parent field
+    val parentKeyColumnReader: CursorValueReader,
+    // key reader for the entity field
+    val entityKeyColumnReader: CursorValueReader,
+    // adapter for the relating pojo
     val rowAdapter: RowAdapter,
+    // parsed relating entity query
     val loadAllQuery: ParsedQuery,
-    val relationTypeIsCollection: Boolean
+    // true if `relationTypeName` is a Collection, when it is `relationTypeName` is always non null.
+    val relationTypeIsCollection: Boolean,
+    val javaLambdaSyntaxAvailable: Boolean
 ) {
     // variable name of map containing keys to relation collections, set when writing the code
     // generator in writeInitCode
@@ -68,8 +82,12 @@
         varName = scope.getTmpVar(
             "_collection${relation.field.getPath().stripNonJava().capitalize(Locale.US)}"
         )
-        scope.builder().apply {
-            addStatement("final $T $L = new $T()", mapTypeName, varName, mapTypeName)
+        scope.builder.apply {
+            addLocalVariable(
+                name = varName,
+                typeName = mapTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(language, mapTypeName)
+            )
         }
     }
 
@@ -79,26 +97,25 @@
         fieldsWithIndices: List<FieldWithIndex>,
         scope: CodeGenScope
     ) {
-        val indexVar = fieldsWithIndices.firstOrNull {
-            it.field === relation.parentField
-        }?.indexVar
-        scope.builder().apply {
-            readKey(cursorVarName, indexVar, scope) { tmpVar ->
+        val indexVar = fieldsWithIndices.firstOrNull { it.field === relation.parentField }?.indexVar
+        checkNotNull(indexVar) {
+            "Expected an index var for a column named '${relation.parentField.columnName}' to " +
+                "query the '${relation.pojoType}' @Relation but didn't. Please file a bug at " +
+                ISSUE_TRACKER_LINK
+        }
+        scope.builder.apply {
+            readKey(cursorVarName, indexVar, parentKeyColumnReader, scope) { tmpVar ->
+                // for relation collection put an empty collections in the map, otherwise put nulls
                 if (relationTypeIsCollection) {
-                    val tmpCollectionVar = scope.getTmpVar(
-                        "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}Collection"
-                    )
-                    addStatement(
-                        "$T $L = $L.get($L)", relationTypeName, tmpCollectionVar,
-                        varName, tmpVar
-                    )
-                    beginControlFlow("if ($L == null)", tmpCollectionVar).apply {
-                        addStatement("$L = new $T()", tmpCollectionVar, relationTypeName)
-                        addStatement("$L.put($L, $L)", varName, tmpVar, tmpCollectionVar)
+                    beginControlFlow("if (!%L.containsKey(%L))", varName, tmpVar).apply {
+                        addStatement(
+                            "%L.put(%L, %L)",
+                            varName, tmpVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                        )
                     }
                     endControlFlow()
                 } else {
-                    addStatement("$L.put($L, null)", varName, tmpVar)
+                    addStatement("%L.put(%L, null)", varName, tmpVar)
                 }
             }
         }
@@ -110,77 +127,116 @@
         fieldsWithIndices: List<FieldWithIndex>,
         scope: CodeGenScope
     ): Pair<String, Field> {
-        val indexVar = fieldsWithIndices.firstOrNull {
-            it.field === relation.parentField
-        }?.indexVar
-        val tmpvarNameSuffix = if (relationTypeIsCollection) "Collection" else ""
+        val indexVar = fieldsWithIndices.firstOrNull { it.field === relation.parentField }?.indexVar
+        checkNotNull(indexVar) {
+            "Expected an index var for a column named '${relation.parentField.columnName}' to " +
+                "query the '${relation.pojoType}' @Relation but didn't. Please file a bug at " +
+                ISSUE_TRACKER_LINK
+        }
+        val tmpVarNameSuffix = if (relationTypeIsCollection) "Collection" else ""
         val tmpRelationVar = scope.getTmpVar(
-            "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}$tmpvarNameSuffix"
+            "_tmp${relation.field.name.stripNonJava().capitalize(Locale.US)}$tmpVarNameSuffix"
         )
-        scope.builder().apply {
-            addStatement("$T $L = null", relationTypeName, tmpRelationVar)
-            readKey(cursorVarName, indexVar, scope) { tmpVar ->
-                addStatement("$L = $L.get($L)", tmpRelationVar, varName, tmpVar)
-            }
-            if (relationTypeIsCollection) {
-                beginControlFlow("if ($L == null)", tmpRelationVar).apply {
-                    addStatement("$L = new $T()", tmpRelationVar, relationTypeName)
+        scope.builder.apply {
+            addLocalVariable(
+                name = tmpRelationVar,
+                typeName = relationTypeName
+            )
+            readKey(
+                cursorVarName = cursorVarName,
+                indexVar = indexVar,
+                keyReader = parentKeyColumnReader,
+                scope = scope,
+                onKeyReady = { tmpKeyVar ->
+                    if (relationTypeIsCollection) {
+                        // For Kotlin use getValue() as get() return a nullable value, when the
+                        // relation is a collection the map is pre-filled with empty collection
+                        // values for all keys, so this is safe. Special case for LongSParseArray
+                        // since it does not have a getValue() from Kotlin.
+                        val usingLongSparseArray =
+                            mapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+                        when (language) {
+                            CodeLanguage.JAVA -> addStatement(
+                                "%L = %L.get(%L)",
+                                tmpRelationVar, varName, tmpKeyVar
+                            )
+                            CodeLanguage.KOTLIN -> if (usingLongSparseArray) {
+                                addStatement(
+                                    "%L = checkNotNull(%L.get(%L))",
+                                    tmpRelationVar, varName, tmpKeyVar
+                                )
+                            } else {
+                                addStatement(
+                                    "%L = %L.getValue(%L)",
+                                    tmpRelationVar, varName, tmpKeyVar
+                                )
+                            }
+                        }
+                    } else {
+                        addStatement("%L = %L.get(%L)", tmpRelationVar, varName, tmpKeyVar)
+                        if (language == CodeLanguage.KOTLIN && relation.field.nonNull) {
+                            beginControlFlow("if (%L == null)", tmpRelationVar)
+                            // TODO(b/249984504): Generate / output a better message.
+                            addStatement("error(%S)", "Missing relationship item.")
+                            endControlFlow()
+                        }
+                    }
+                },
+                onKeyUnavailable = {
+                    if (relationTypeIsCollection) {
+                        addStatement(
+                            "%L = %L",
+                            tmpRelationVar, XCodeBlock.ofNewInstance(language, relationTypeName)
+                        )
+                    } else {
+                        addStatement("%L = null", tmpRelationVar)
+                    }
                 }
-                endControlFlow()
-            }
+            )
         }
         return tmpRelationVar to relation.field
     }
 
-    fun writeCollectionCode(scope: CodeGenScope) {
+    // called to write the invocation to the fetch relationship method
+    fun writeFetchRelationCall(scope: CodeGenScope) {
         val method = scope.writer
             .getOrCreateFunction(RelationCollectorFunctionWriter(this))
-        scope.builder().apply {
-            addStatement("$L($L)", method.name, varName)
-        }
+        scope.builder.addStatement("%L(%L)", method.name, varName)
     }
 
+    // called to read key and call `onKeyReady` to write code once it is successfully read
     fun readKey(
         cursorVarName: String,
-        indexVar: String?,
+        indexVar: String,
+        keyReader: CursorValueReader,
         scope: CodeGenScope,
-        postRead: CodeBlock.Builder.(String) -> Unit
+        onKeyReady: XCodeBlock.Builder.(String) -> Unit
     ) {
-        val cursorGetter = when (affinity) {
-            SQLTypeAffinity.INTEGER -> "getLong"
-            SQLTypeAffinity.REAL -> "getDouble"
-            SQLTypeAffinity.TEXT -> "getString"
-            SQLTypeAffinity.BLOB -> "getBlob"
-            else -> {
-                "getString"
-            }
-        }
-        scope.builder().apply {
-            val keyType = if (mapTypeName.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY) {
-                keyTypeName.unbox()
-            } else {
-                keyTypeName
-            }
+        readKey(cursorVarName, indexVar, keyReader, scope, onKeyReady, null)
+    }
+
+    // called to read key and call `onKeyReady` to write code once it is successfully read and
+    // `onKeyUnavailable` if the key is unavailable (missing column due to bad projection).
+    private fun readKey(
+        cursorVarName: String,
+        indexVar: String,
+        keyReader: CursorValueReader,
+        scope: CodeGenScope,
+        onKeyReady: XCodeBlock.Builder.(String) -> Unit,
+        onKeyUnavailable: (XCodeBlock.Builder.() -> Unit)?,
+    ) {
+        scope.builder.apply {
             val tmpVar = scope.getTmpVar("_tmpKey")
-            fun addKeyReadStatement() {
-                if (keyTypeName == TypeName.get(ByteBuffer::class.java)) {
-                    addStatement(
-                        "final $T $L = $T.wrap($L.$L($L))",
-                        keyType, tmpVar, keyTypeName, cursorVarName, cursorGetter, indexVar
-                    )
-                } else {
-                    addStatement(
-                        "final $T $L = $L.$L($L)",
-                        keyType, tmpVar, cursorVarName, cursorGetter, indexVar
-                    )
-                }
-                this.postRead(tmpVar)
-            }
-            if (relation.parentField.nonNull) {
-                addKeyReadStatement()
+            addLocalVariable(tmpVar, keyReader.typeMirror().asTypeName())
+            keyReader.readFromCursor(tmpVar, cursorVarName, indexVar, scope)
+            if (keyReader.typeMirror().nullability == XNullability.NONNULL) {
+                onKeyReady(tmpVar)
             } else {
-                beginControlFlow("if (!$L.isNull($L))", cursorVarName, indexVar).apply {
-                    addKeyReadStatement()
+                beginControlFlow("if (%L != null)", tmpVar)
+                onKeyReady(tmpVar)
+                if (onKeyUnavailable != null) {
+                    nextControlFlow("else")
+                    onKeyUnavailable()
                 }
                 endControlFlow()
             }
@@ -189,7 +245,7 @@
 
     /**
      * Adapter for binding a LongSparseArray keys into query arguments. This special adapter is only
-     * used for binding the relationship query who's keys have INTEGER affinity.
+     * used for binding the relationship query whose keys have INTEGER affinity.
      */
     private class LongSparseArrayKeyQueryParameterAdapter : QueryParameterAdapter(true) {
         override fun bindToStmt(
@@ -198,16 +254,27 @@
             startIndexVarName: String,
             scope: CodeGenScope
         ) {
-            scope.builder().apply {
+            scope.builder.apply {
                 val itrIndexVar = "i"
                 val itrItemVar = scope.getTmpVar("_item")
-                beginControlFlow(
-                    "for (int $L = 0; $L < $L.size(); i++)",
-                    itrIndexVar, itrIndexVar, inputVarName
-                ).apply {
-                    addStatement("long $L = $L.keyAt($L)", itrItemVar, inputVarName, itrIndexVar)
-                    addStatement("$L.bindLong($L, $L)", stmtVarName, startIndexVarName, itrItemVar)
-                    addStatement("$L ++", startIndexVarName)
+                when (language) {
+                    CodeLanguage.JAVA -> beginControlFlow(
+                        "for (int %L = 0; %L < %L.size(); i++)",
+                        itrIndexVar, itrIndexVar, inputVarName
+                    )
+                    CodeLanguage.KOTLIN -> beginControlFlow(
+                        "for (%L in 0 until %L.size())",
+                        itrIndexVar, inputVarName
+                    )
+                }.apply {
+                    addLocalVal(
+                        itrItemVar,
+                        XTypeName.PRIMITIVE_LONG,
+                        "%L.keyAt(%L)",
+                        inputVarName, itrIndexVar
+                    )
+                    addStatement("%L.bindLong(%L, %L)", stmtVarName, startIndexVarName, itrItemVar)
+                    addStatement("%L++", startIndexVarName)
                 }
                 endControlFlow()
             }
@@ -218,9 +285,11 @@
             outputVarName: String,
             scope: CodeGenScope
         ) {
-            scope.builder().addStatement(
-                "final $T $L = $L.size()",
-                TypeName.INT, outputVarName, inputVarName
+            scope.builder.addLocalVal(
+                outputVarName,
+                XTypeName.PRIMITIVE_INT,
+                "%L.size()",
+                inputVarName
             )
         }
     }
@@ -237,12 +306,16 @@
             return relations.map { relation ->
                 val context = baseContext.fork(
                     element = relation.field.element,
-                    forceSuppressedWarnings = setOf(Warning.CURSOR_MISMATCH)
+                    forceSuppressedWarnings = setOf(Warning.CURSOR_MISMATCH),
+                    forceBuiltInConverters = BuiltInConverterFlags.DEFAULT.copy(
+                        byteBuffer = BuiltInTypeConverters.State.ENABLED
+                    )
                 )
                 val affinity = affinityFor(context, relation)
-                val keyType = keyTypeFor(context, affinity)
-                val (relationTypeName, isRelationCollection) = relationTypeFor(relation)
-                val tmpMapType = temporaryMapTypeFor(context, affinity, keyType, relationTypeName)
+                val keyTypeName = keyTypeFor(context, affinity)
+                val (relationTypeName, isRelationCollection) = relationTypeFor(context, relation)
+                val tmpMapTypeName =
+                    temporaryMapTypeFor(context, affinity, keyTypeName, relationTypeName)
 
                 val loadAllQuery = relation.createLoadAllSql()
                 val parsedQuery = SqlParser.parse(loadAllQuery)
@@ -263,10 +336,10 @@
                 val resultInfo = parsedQuery.resultInfo
 
                 val usingLongSparseArray =
-                    tmpMapType.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY
+                    tmpMapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
                 val queryParam = if (usingLongSparseArray) {
                     val longSparseArrayElement = context.processingEnv
-                        .requireTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY)
+                        .requireTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName)
                     QueryParameter(
                         name = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
                         sqlName = RelationCollectorFunctionWriter.PARAM_MAP_VARIABLE,
@@ -274,8 +347,8 @@
                         queryParamAdapter = LONG_SPARSE_ARRAY_KEY_QUERY_PARAM_ADAPTER
                     )
                 } else {
-                    val keyTypeMirror = keyTypeMirrorFor(context, affinity)
-                    val set = context.processingEnv.requireTypeElement("java.util.Set")
+                    val keyTypeMirror = context.processingEnv.requireType(keyTypeName)
+                    val set = checkNotNull(context.COMMON_TYPES.SET.typeElement)
                     val keySet = context.processingEnv.getDeclaredType(set, keyTypeMirror)
                     QueryParameter(
                         name = RelationCollectorFunctionWriter.KEY_SET_VARIABLE,
@@ -299,11 +372,29 @@
                     query = parsedQuery
                 )
 
+                val parentKeyColumnReader = context.typeAdapterStore.findCursorValueReader(
+                    output = context.processingEnv.requireType(keyTypeName).let {
+                        if (!relation.parentField.nonNull) it.makeNullable() else it
+                    },
+                    affinity = affinity
+                )
+                val entityKeyColumnReader = context.typeAdapterStore.findCursorValueReader(
+                    output = context.processingEnv.requireType(keyTypeName).let { keyType ->
+                        if (!relation.entityField.nonNull) keyType.makeNullable() else keyType
+                    },
+                    affinity = affinity
+                )
+                // We should always find a readers since key types all have built in converters
+                check(parentKeyColumnReader != null && entityKeyColumnReader != null) {
+                    "Missing one of the relation key value reader for type $keyTypeName"
+                }
+
                 // row adapter that matches full response
                 fun getDefaultRowAdapter(): RowAdapter? {
                     return context.typeAdapterStore.findRowAdapter(relation.pojoType, parsedQuery)
                 }
-                val rowAdapter = if (relation.projection.size == 1 && resultInfo != null &&
+                val rowAdapter = if (
+                    relation.projection.size == 1 && resultInfo != null &&
                     (resultInfo.columns.size == 1 || resultInfo.columns.size == 2)
                 ) {
                     // check for a column adapter first
@@ -329,13 +420,16 @@
                     RelationCollector(
                         relation = relation,
                         affinity = affinity,
-                        mapTypeName = tmpMapType,
-                        keyTypeName = keyType,
+                        mapTypeName = tmpMapTypeName,
+                        keyTypeName = keyTypeName,
                         relationTypeName = relationTypeName,
                         queryWriter = queryWriter,
+                        parentKeyColumnReader = parentKeyColumnReader,
+                        entityKeyColumnReader = entityKeyColumnReader,
                         rowAdapter = rowAdapter,
                         loadAllQuery = parsedQuery,
-                        relationTypeIsCollection = isRelationCollection
+                        relationTypeIsCollection = isRelationCollection,
+                        javaLambdaSyntaxAvailable = context.processingEnv.jvmVersion >= 8
                     )
                 }
             }.filterNotNull()
@@ -399,87 +493,65 @@
         }
 
         // Gets the resulting relation type name. (i.e. the Pojo's @Relation field type name.)
-        private fun relationTypeFor(relation: Relation) =
-            if (relation.field.typeName.toJavaPoet() is ParameterizedTypeName) {
-                val paramType = relation.field.typeName.toJavaPoet() as ParameterizedTypeName
-                val paramTypeName = if (paramType.rawType == CommonTypeNames.LIST) {
-                    ParameterizedTypeName.get(
-                        ClassName.get(ArrayList::class.java),
-                        relation.pojoTypeName
-                    )
-                } else if (paramType.rawType == CommonTypeNames.SET) {
-                    ParameterizedTypeName.get(
-                        ClassName.get(HashSet::class.java),
-                        relation.pojoTypeName
-                    )
-                } else {
-                    ParameterizedTypeName.get(
-                        ClassName.get(ArrayList::class.java),
-                        relation.pojoTypeName
-                    )
-                }
+        private fun relationTypeFor(
+            context: Context,
+            relation: Relation
+        ) = relation.field.type.let { fieldType ->
+            if (fieldType.typeArguments.isNotEmpty()) {
+                val rawType = fieldType.rawType
+                val paramTypeName =
+                    if (context.COMMON_TYPES.LIST.rawType.isAssignableFrom(rawType)) {
+                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                    } else if (context.COMMON_TYPES.SET.rawType.isAssignableFrom(rawType)) {
+                        CommonTypeNames.HASH_SET.parametrizedBy(relation.pojoTypeName)
+                    } else {
+                        // Default to ArrayList and see how things go...
+                        CommonTypeNames.ARRAY_LIST.parametrizedBy(relation.pojoTypeName)
+                    }
                 paramTypeName to true
             } else {
-                relation.pojoTypeName to false
+                relation.pojoTypeName.copy(nullable = true) to false
             }
+        }
 
         // Gets the type name of the temporary key map.
         private fun temporaryMapTypeFor(
             context: Context,
             affinity: SQLTypeAffinity,
-            keyType: TypeName,
-            relationTypeName: TypeName
-        ): ParameterizedTypeName {
+            keyTypeName: XTypeName,
+            valueTypeName: XTypeName,
+        ): XTypeName {
             val canUseLongSparseArray = context.processingEnv
-                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY) != null
+                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
             val canUseArrayMap = context.processingEnv
-                .findTypeElement(CollectionTypeNames.ARRAY_MAP) != null
+                .findTypeElement(CollectionTypeNames.ARRAY_MAP.canonicalName) != null
             return when {
-                canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER -> {
-                    ParameterizedTypeName.get(
-                        CollectionTypeNames.LONG_SPARSE_ARRAY,
-                        relationTypeName
-                    )
-                }
-                canUseArrayMap -> {
-                    ParameterizedTypeName.get(
-                        CollectionTypeNames.ARRAY_MAP,
-                        keyType, relationTypeName
-                    )
-                }
-                else -> {
-                    ParameterizedTypeName.get(
-                        ClassName.get(java.util.HashMap::class.java),
-                        keyType, relationTypeName
-                    )
-                }
-            }
-        }
-
-        // Gets the type mirror of the relationship key.
-        private fun keyTypeMirrorFor(context: Context, affinity: SQLTypeAffinity): XType {
-            val processingEnv = context.processingEnv
-            return when (affinity) {
-                SQLTypeAffinity.INTEGER -> processingEnv.requireType("java.lang.Long")
-                SQLTypeAffinity.REAL -> processingEnv.requireType("java.lang.Double")
-                SQLTypeAffinity.TEXT -> context.COMMON_TYPES.STRING
-                SQLTypeAffinity.BLOB -> processingEnv.requireType("java.nio.ByteBuffer")
-                else -> {
-                    context.COMMON_TYPES.STRING
-                }
+                canUseLongSparseArray && affinity == SQLTypeAffinity.INTEGER ->
+                    CollectionTypeNames.LONG_SPARSE_ARRAY.parametrizedBy(valueTypeName)
+                canUseArrayMap ->
+                    CollectionTypeNames.ARRAY_MAP.parametrizedBy(keyTypeName, valueTypeName)
+                else ->
+                    CommonTypeNames.HASH_MAP.parametrizedBy(keyTypeName, valueTypeName)
             }
         }
 
         // Gets the type name of the relationship key.
-        private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): TypeName {
+        private fun keyTypeFor(context: Context, affinity: SQLTypeAffinity): XTypeName {
+            val canUseLongSparseArray = context.processingEnv
+                .findTypeElement(CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName) != null
             return when (affinity) {
-                SQLTypeAffinity.INTEGER -> TypeName.LONG.box()
-                SQLTypeAffinity.REAL -> TypeName.DOUBLE.box()
-                SQLTypeAffinity.TEXT -> TypeName.get(String::class.java)
-                SQLTypeAffinity.BLOB -> TypeName.get(ByteBuffer::class.java)
+                SQLTypeAffinity.INTEGER ->
+                    if (canUseLongSparseArray) {
+                        XTypeName.PRIMITIVE_LONG
+                    } else {
+                        Long::class.asClassName()
+                    }
+                SQLTypeAffinity.REAL -> Double::class.asClassName()
+                SQLTypeAffinity.TEXT -> String::class.asClassName()
+                SQLTypeAffinity.BLOB -> ByteBuffer::class.asClassName()
                 else -> {
-                    // no affinity select from type
-                    context.COMMON_TYPES.STRING.typeName
+                    // no affinity default to String
+                    String::class.asClassName()
                 }
             }
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
index 290af1e..a302a22 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/AutoMigrationWriter.kt
@@ -24,7 +24,6 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
 import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addProperty
-import androidx.room.compiler.codegen.toXClassName
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.SupportDbTypeNames
@@ -53,17 +52,17 @@
         )
         builder.apply {
             addOriginatingElement(dbElement)
-            superclass(RoomTypeNames.MIGRATION.toXClassName())
+            superclass(RoomTypeNames.MIGRATION)
 
             if (autoMigration.specClassName != null) {
                 builder.addProperty(
                     name = "callback",
-                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC.toXClassName(),
+                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC,
                     visibility = VisibilityModifier.PRIVATE,
                     initExpr = if (!autoMigration.isSpecProvided) {
                         XCodeBlock.ofNewInstance(
                             codeLanguage,
-                            autoMigration.specClassName.toXClassName()
+                            autoMigration.specClassName
                         )
                     } else {
                         null
@@ -89,7 +88,7 @@
             )
             if (autoMigration.isSpecProvided) {
                 addParameter(
-                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC.toXClassName(),
+                    typeName = RoomTypeNames.AUTO_MIGRATION_SPEC,
                     name = "callback",
                 )
                 addStatement("this.callback = callback")
@@ -104,15 +103,15 @@
             visibility = VisibilityModifier.PUBLIC,
             isOverride = true,
         ).apply {
-                addParameter(
-                    typeName = SupportDbTypeNames.DB.toXClassName(),
-                    name = "database",
-                )
-                addMigrationStatements(this)
-                if (autoMigration.specClassName != null) {
-                    addStatement("callback.onPostMigrate(database)")
-                }
+            addParameter(
+                typeName = SupportDbTypeNames.DB,
+                name = "database",
+            )
+            addMigrationStatements(this)
+            if (autoMigration.specClassName != null) {
+                addStatement("callback.onPostMigrate(database)")
             }
+        }
         return migrateFunctionBuilder.build()
     }
 
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 4d378b7..ea0d93b 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
@@ -134,7 +134,7 @@
             }
             addProperty(dbProperty)
 
-            addFunction(
+            setPrimaryConstructor(
                 createConstructor(
                     shortcutMethods,
                     dao.constructorParamType != null
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
index fe49b03..f1d09cf 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/DatabaseWriter.kt
@@ -16,43 +16,31 @@
 
 package androidx.room.writer
 
-import androidx.annotation.NonNull
 import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XPropertySpec
+import androidx.room.compiler.codegen.XPropertySpec.Companion.apply
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
-import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.apply
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.processing.MethodSpecHelper
-import androidx.room.compiler.processing.addOriginatingElement
+import androidx.room.compiler.codegen.XTypeSpec.Builder.Companion.addOriginatingElement
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
 import androidx.room.ext.SupportDbTypeNames
-import androidx.room.ext.T
-import androidx.room.ext.W
 import androidx.room.ext.decapitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.DaoMethod
 import androidx.room.vo.Database
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.FieldSpec
-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 com.squareup.javapoet.WildcardTypeName
+import com.squareup.kotlinpoet.FunSpec
+import com.squareup.kotlinpoet.KModifier
 import java.util.Locale
-import javax.lang.model.element.Modifier.FINAL
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PROTECTED
-import javax.lang.model.element.Modifier.PUBLIC
-import javax.lang.model.element.Modifier.VOLATILE
+import javax.lang.model.element.Modifier
 
 /**
  * Writes implementation of classes that were annotated with @Database.
@@ -62,189 +50,171 @@
     codeLanguage: CodeLanguage
 ) : TypeWriter(codeLanguage) {
     override fun createTypeSpecBuilder(): XTypeSpec.Builder {
-        val builder = XTypeSpec.classBuilder(codeLanguage, database.implTypeName)
-        builder.apply(
-            javaTypeBuilder = {
-                addOriginatingElement(database.element)
-                addModifiers(PUBLIC)
-                addModifiers(FINAL)
-                superclass(database.typeName.toJavaPoet())
-                addMethod(createCreateOpenHelper())
-                addMethod(createCreateInvalidationTracker())
-                addMethod(createClearAllTables())
-                addMethod(createCreateTypeConvertersMap())
-                addMethod(createCreateAutoMigrationSpecsSet())
-                addMethod(getAutoMigrations())
-                addDaoImpls(this)
-            },
-            kotlinTypeBuilder = {
-                TODO("Kotlin codegen not yet implemented!")
-            }
-        )
-        return builder
+        return XTypeSpec.classBuilder(codeLanguage, database.implTypeName).apply {
+            addOriginatingElement(database.element)
+            superclass(database.typeName)
+            setVisibility(VisibilityModifier.PUBLIC)
+            addFunction(createCreateOpenHelper())
+            addFunction(createCreateInvalidationTracker())
+            addFunction(createClearAllTables())
+            addFunction(createCreateTypeConvertersMap())
+            addFunction(createCreateAutoMigrationSpecsSet())
+            addFunction(getAutoMigrations())
+            addDaoImpls(this)
+        }
     }
 
-    private fun createCreateTypeConvertersMap(): MethodSpec {
+    private fun createCreateTypeConvertersMap(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("getRequiredTypeConverters").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PROTECTED)
-            returns(
-                ParameterizedTypeName.get(
-                    CommonTypeNames.MAP,
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(Object::class.java)
-                    ),
-                    ParameterizedTypeName.get(
-                        CommonTypeNames.LIST,
-                        ParameterizedTypeName.get(
-                            ClassName.get(Class::class.java),
-                            WildcardTypeName.subtypeOf(Object::class.java)
-                        )
-                    )
-                )
-            )
+        val classOfAnyTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(Any::class.asClassName())
+        )
+        val typeConvertersTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
+            classOfAnyTypeName,
+            CommonTypeNames.LIST.parametrizedBy(classOfAnyTypeName)
+        )
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val typeConvertersVar = scope.getTmpVar("_typeConvertersMap")
-            val typeConvertersTypeName = ParameterizedTypeName.get(
-                ClassName.get(HashMap::class.java),
-                ParameterizedTypeName.get(
-                    ClassName.get(Class::class.java),
-                    WildcardTypeName.subtypeOf(Object::class.java)
-                ),
-                ParameterizedTypeName.get(
-                    ClassName.get(List::class.java),
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(Object::class.java)
-                    )
-                )
-            )
-            addStatement(
-                "final $T $L = new $T()",
-                typeConvertersTypeName,
-                typeConvertersVar,
-                typeConvertersTypeName
+            addLocalVariable(
+                name = typeConvertersVar,
+                typeName = typeConvertersTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, typeConvertersTypeName)
             )
             database.daoMethods.forEach {
                 addStatement(
-                    "$L.put($T.class, $T.$L())",
+                    "%L.put(%L, %T.%L())",
                     typeConvertersVar,
-                    it.dao.typeName.toJavaPoet(),
-                    it.dao.implTypeName.toJavaPoet(),
+                    XCodeBlock.ofJavaClassLiteral(codeLanguage, it.dao.typeName),
+                    it.dao.implTypeName,
                     DaoWriter.GET_LIST_OF_TYPE_CONVERTERS_METHOD
                 )
             }
-            addStatement("return $L", typeConvertersVar)
+            addStatement("return %L", typeConvertersVar)
         }.build()
-    }
-
-    private fun createCreateAutoMigrationSpecsSet(): MethodSpec {
-        val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("getRequiredAutoMigrationSpecs").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PUBLIC)
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getRequiredTypeConverters",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true
+        ).apply {
             returns(
-                ParameterizedTypeName.get(
-                    CommonTypeNames.SET,
-                    ParameterizedTypeName.get(
-                        ClassName.get(Class::class.java),
-                        WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                    )
+                CommonTypeNames.MAP.parametrizedBy(
+                    classOfAnyTypeName,
+                    CommonTypeNames.LIST.parametrizedBy(classOfAnyTypeName)
                 )
             )
-            val autoMigrationSpecsVar = scope.getTmpVar("_autoMigrationSpecsSet")
-            val autoMigrationSpecsTypeName = ParameterizedTypeName.get(
-                ClassName.get(HashSet::class.java),
-                ParameterizedTypeName.get(
-                    ClassName.get(Class::class.java),
-                    WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                )
-            )
-            addStatement(
-                "final $T $L = new $T()",
-                autoMigrationSpecsTypeName,
-                autoMigrationSpecsVar,
-                autoMigrationSpecsTypeName
-            )
-            database.autoMigrations.map { autoMigrationResult ->
-                if (autoMigrationResult.isSpecProvided) {
-                    addStatement(
-                        "$L.add($T.class)",
-                        autoMigrationSpecsVar,
-                        autoMigrationResult.specClassName
-                    )
-                }
-            }
-            addStatement("return $L", autoMigrationSpecsVar)
+            addCode(body)
         }.build()
     }
 
-    private fun createClearAllTables(): MethodSpec {
+    private fun createCreateAutoMigrationSpecsSet(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("clearAllTables").apply {
+        val classOfAutoMigrationSpecTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(RoomTypeNames.AUTO_MIGRATION_SPEC)
+        )
+        val autoMigrationSpecsTypeName =
+            CommonTypeNames.HASH_SET.parametrizedBy(classOfAutoMigrationSpecTypeName)
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            val autoMigrationSpecsVar = scope.getTmpVar("_autoMigrationSpecsSet")
+            addLocalVariable(
+                name = autoMigrationSpecsVar,
+                typeName = autoMigrationSpecsTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, autoMigrationSpecsTypeName)
+            )
+            database.autoMigrations.filter { it.isSpecProvided }.map { autoMigration ->
+                val specClassName = checkNotNull(autoMigration.specClassName)
+                addStatement(
+                    "%L.add(%L)",
+                    autoMigrationSpecsVar,
+                    XCodeBlock.ofJavaClassLiteral(codeLanguage, specClassName)
+                )
+            }
+            addStatement("return %L", autoMigrationSpecsVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getRequiredAutoMigrationSpecs",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true,
+        ).apply {
+            returns(CommonTypeNames.SET.parametrizedBy(classOfAutoMigrationSpecTypeName))
+            addCode(body)
+        }.build()
+    }
+
+    private fun createClearAllTables(): XFunSpec {
+        val scope = CodeGenScope(this)
+        val body = XCodeBlock.builder(codeLanguage).apply {
             addStatement("super.assertNotMainThread()")
             val dbVar = scope.getTmpVar("_db")
-            addStatement(
-                "final $T $L = super.getOpenHelper().getWritableDatabase()",
-                SupportDbTypeNames.DB, dbVar
+            addLocalVal(
+                dbVar,
+                SupportDbTypeNames.DB,
+                when (language) {
+                    CodeLanguage.JAVA -> "super.getOpenHelper().getWritableDatabase()"
+                    CodeLanguage.KOTLIN -> "super.openHelper.writableDatabase"
+                }
             )
             val deferVar = scope.getTmpVar("_supportsDeferForeignKeys")
             if (database.enableForeignKeys) {
-                addStatement(
-                    "boolean $L = $L.VERSION.SDK_INT >= $L.VERSION_CODES.LOLLIPOP",
-                    deferVar, AndroidTypeNames.BUILD, AndroidTypeNames.BUILD
+                addLocalVal(
+                    deferVar,
+                    XTypeName.PRIMITIVE_BOOLEAN,
+                    "%L.VERSION.SDK_INT >= %L.VERSION_CODES.LOLLIPOP",
+                    AndroidTypeNames.BUILD,
+                    AndroidTypeNames.BUILD
                 )
             }
-            addAnnotation(Override::class.java)
-            addModifiers(PUBLIC)
-            returns(TypeName.VOID)
             beginControlFlow("try").apply {
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if (!$L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = FALSE")
+                    beginControlFlow("if (!%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = FALSE")
                     }
                     endControlFlow()
                 }
                 addStatement("super.beginTransaction()")
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if ($L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
+                    beginControlFlow("if (%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA defer_foreign_keys = TRUE")
                     }
                     endControlFlow()
                 }
                 database.entities.sortedWith(EntityDeleteComparator()).forEach {
-                    addStatement("$L.execSQL($S)", dbVar, "DELETE FROM `${it.tableName}`")
+                    addStatement("%L.execSQL(%S)", dbVar, "DELETE FROM `${it.tableName}`")
                 }
                 addStatement("super.setTransactionSuccessful()")
             }
             nextControlFlow("finally").apply {
                 addStatement("super.endTransaction()")
                 if (database.enableForeignKeys) {
-                    beginControlFlow("if (!$L)", deferVar).apply {
-                        addStatement("$L.execSQL($S)", dbVar, "PRAGMA foreign_keys = TRUE")
+                    beginControlFlow("if (!%L)", deferVar).apply {
+                        addStatement("%L.execSQL(%S)", dbVar, "PRAGMA foreign_keys = TRUE")
                     }
                     endControlFlow()
                 }
-                addStatement("$L.query($S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
-                beginControlFlow("if (!$L.inTransaction())", dbVar).apply {
-                    addStatement("$L.execSQL($S)", dbVar, "VACUUM")
+                addStatement("%L.query(%S).close()", dbVar, "PRAGMA wal_checkpoint(FULL)")
+                beginControlFlow("if (!%L.inTransaction())", dbVar).apply {
+                    addStatement("%L.execSQL(%S)", dbVar, "VACUUM")
                 }
                 endControlFlow()
             }
             endControlFlow()
         }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "clearAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addCode(body)
+        }.build()
     }
 
-    private fun createCreateInvalidationTracker(): MethodSpec {
+    private fun createCreateInvalidationTracker(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("createInvalidationTracker").apply {
-            addAnnotation(Override::class.java)
-            addModifiers(PROTECTED)
-            returns(RoomTypeNames.INVALIDATION_TRACKER)
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val shadowTablesVar = "_shadowTablesMap"
-            val shadowTablesTypeName = ParameterizedTypeName.get(
-                HashMap::class.typeName,
+            val shadowTablesTypeName = CommonTypeNames.HASH_MAP.parametrizedBy(
                 CommonTypeNames.STRING, CommonTypeNames.STRING
             )
             val tableNames = database.entities.joinToString(",") {
@@ -255,154 +225,266 @@
             }.map {
                 it.tableName to it.shadowTableName
             }
-            addStatement(
-                "final $T $L = new $T($L)", shadowTablesTypeName, shadowTablesVar,
-                shadowTablesTypeName, shadowTableNames.size
-            )
-            shadowTableNames.forEach { (tableName, shadowTableName) ->
-                addStatement("$L.put($S, $S)", shadowTablesVar, tableName, shadowTableName)
-            }
-            val viewTablesVar = scope.getTmpVar("_viewTables")
-            val tablesType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                CommonTypeNames.STRING
-            )
-            val viewTablesType = ParameterizedTypeName.get(
-                HashMap::class.typeName,
-                CommonTypeNames.STRING,
-                ParameterizedTypeName.get(
-                    CommonTypeNames.SET,
-                    CommonTypeNames.STRING
+            addLocalVariable(
+                name = shadowTablesVar,
+                typeName = shadowTablesTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    shadowTablesTypeName,
+                    "%L",
+                    shadowTableNames.size
                 )
             )
-            addStatement(
-                "$T $L = new $T($L)", viewTablesType, viewTablesVar, viewTablesType,
-                database.views.size
+            shadowTableNames.forEach { (tableName, shadowTableName) ->
+                addStatement("%L.put(%S, %S)", shadowTablesVar, tableName, shadowTableName)
+            }
+            val viewTablesVar = scope.getTmpVar("_viewTables")
+            val tablesType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+            val viewTablesType = CommonTypeNames.HASH_MAP.parametrizedBy(
+                CommonTypeNames.STRING,
+                CommonTypeNames.SET.parametrizedBy(CommonTypeNames.STRING)
+            )
+            addLocalVariable(
+                name = viewTablesVar,
+                typeName = viewTablesType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    viewTablesType,
+                    "%L", database.views.size
+                )
             )
             for (view in database.views) {
                 val tablesVar = scope.getTmpVar("_tables")
-                addStatement(
-                    "$T $L = new $T($L)", tablesType, tablesVar, tablesType,
-                    view.tables.size
+                addLocalVariable(
+                    name = tablesVar,
+                    typeName = tablesType,
+                    assignExpr = XCodeBlock.ofNewInstance(
+                        codeLanguage,
+                        tablesType,
+                        "%L", view.tables.size
+                    )
                 )
                 for (table in view.tables) {
-                    addStatement("$L.add($S)", tablesVar, table)
+                    addStatement("%L.add(%S)", tablesVar, table)
                 }
                 addStatement(
-                    "$L.put($S, $L)", viewTablesVar,
-                    view.viewName.lowercase(Locale.US), tablesVar
+                    "%L.put(%S, %L)",
+                    viewTablesVar, view.viewName.lowercase(Locale.US), tablesVar
                 )
             }
             addStatement(
-                "return new $T(this, $L, $L, $L)",
-                RoomTypeNames.INVALIDATION_TRACKER, shadowTablesVar, viewTablesVar, tableNames
+                "return %L",
+                XCodeBlock.ofNewInstance(
+                    codeLanguage,
+                    RoomTypeNames.INVALIDATION_TRACKER,
+                    "this, %L, %L, %L",
+                    shadowTablesVar, viewTablesVar, tableNames
+                )
             )
         }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "createInvalidationTracker",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true
+        ).apply {
+            returns(RoomTypeNames.INVALIDATION_TRACKER)
+            addCode(body)
+        }.build()
     }
 
-    private fun addDaoImpls(builder: TypeSpec.Builder) {
+    private fun addDaoImpls(builder: XTypeSpec.Builder) {
         val scope = CodeGenScope(this)
-        builder.apply {
-            database.daoMethods.forEach { method ->
-                val name = method.dao.typeName.toJavaPoet().simpleName()
-                    .decapitalize(Locale.US)
-                    .stripNonJava()
-                val fieldName = scope.getTmpVar("_$name")
-                val field = FieldSpec.builder(
-                    method.dao.typeName.toJavaPoet(), fieldName,
-                    PRIVATE, VOLATILE
-                ).build()
-                addField(field)
-                addMethod(createDaoGetter(field, method))
+        database.daoMethods.forEach { method ->
+            val name = method.dao.typeName.simpleNames.first()
+                .decapitalize(Locale.US)
+                .stripNonJava()
+            val privateDaoProperty = XPropertySpec.builder(
+                language = codeLanguage,
+                name = scope.getTmpVar("_$name"),
+                typeName = if (codeLanguage == CodeLanguage.KOTLIN) {
+                    KotlinTypeNames.LAZY.parametrizedBy(method.dao.typeName)
+                } else {
+                    method.dao.typeName
+                },
+                visibility = VisibilityModifier.PRIVATE,
+                isMutable = codeLanguage == CodeLanguage.JAVA
+            ).apply {
+                // For Kotlin we rely on kotlin.Lazy while for Java we'll memoize the dao impl in
+                // the getter.
+                if (language == CodeLanguage.KOTLIN) {
+                   initializer(
+                       XCodeBlock.of(
+                           language,
+                           "lazy { %L }",
+                           XCodeBlock.ofNewInstance(language, method.dao.implTypeName, "this")
+                       )
+                   )
+                }
+            }.apply(
+                javaFieldBuilder = {
+                    // The volatile modifier is needed since in Java the memoization is generated.
+                    addModifiers(Modifier.VOLATILE)
+                },
+                kotlinPropertyBuilder = { }
+            ).build()
+            builder.addProperty(privateDaoProperty)
+            val overrideProperty =
+                codeLanguage == CodeLanguage.KOTLIN && method.element.isKotlinPropertyMethod()
+            if (overrideProperty) {
+                builder.addProperty(createDaoProperty(method, privateDaoProperty))
+            } else {
+                builder.addFunction(createDaoGetter(method, privateDaoProperty))
             }
         }
     }
 
-    private fun createDaoGetter(field: FieldSpec, method: DaoMethod): MethodSpec {
-        return MethodSpecHelper.overridingWithFinalParams(
-            elm = method.element,
-            owner = database.element.type
-        ).apply {
-            beginControlFlow("if ($N != null)", field).apply {
-                addStatement("return $N", field)
-            }
-            nextControlFlow("else").apply {
-                beginControlFlow("synchronized(this)").apply {
-                    beginControlFlow("if($N == null)", field).apply {
-                        addStatement(
-                            "$N = new $T(this)",
-                            field,
-                            method.dao.implTypeName.toJavaPoet()
-                        )
+    private fun createDaoGetter(method: DaoMethod, daoProperty: XPropertySpec): XFunSpec {
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            // For Java we implement the memoization logic in the Dao getter, meanwhile for Kotlin
+            // we rely on kotlin.Lazy to the getter just delegates to it.
+            when (codeLanguage) {
+                CodeLanguage.JAVA -> {
+                    beginControlFlow("if (%N != null)", daoProperty).apply {
+                        addStatement("return %N", daoProperty)
+                    }
+                    nextControlFlow("else").apply {
+                        beginControlFlow("synchronized(this)").apply {
+                            beginControlFlow("if(%N == null)", daoProperty).apply {
+                                addStatement(
+                                    "%N = %L",
+                                    daoProperty,
+                                    XCodeBlock.ofNewInstance(
+                                        language,
+                                        method.dao.implTypeName,
+                                        "this"
+                                    )
+
+                                )
+                            }
+                            endControlFlow()
+                            addStatement("return %N", daoProperty)
+                        }
+                        endControlFlow()
                     }
                     endControlFlow()
-                    addStatement("return $N", field)
                 }
-                endControlFlow()
+                CodeLanguage.KOTLIN -> {
+                    addStatement("return %N.value", daoProperty)
+                }
             }
-            endControlFlow()
+        }
+        return XFunSpec.overridingBuilder(
+            language = codeLanguage,
+            element = method.element,
+            owner = database.element.type
+        ).apply {
+            addCode(body.build())
         }.build()
     }
 
-    private fun createCreateOpenHelper(): MethodSpec {
+    private fun createDaoProperty(method: DaoMethod, daoProperty: XPropertySpec): XPropertySpec {
+        // TODO(b/257967987): This has a few flaws that need to be fixed.
+        return XPropertySpec.builder(
+            language = codeLanguage,
+            name = method.element.name.drop(3).replaceFirstChar { it.lowercase(Locale.US) },
+            typeName = method.dao.typeName,
+            visibility = when {
+                method.element.isPublic() -> VisibilityModifier.PUBLIC
+                method.element.isProtected() -> VisibilityModifier.PROTECTED
+                else -> VisibilityModifier.PUBLIC // Might be internal... ?
+            }
+        ).apply(
+            javaFieldBuilder = { error("Overriding a property in Java is impossible!") },
+            kotlinPropertyBuilder = {
+                addModifiers(KModifier.OVERRIDE)
+                getter(
+                    FunSpec.getterBuilder()
+                        .addStatement("return %L.value", daoProperty.name)
+                        .build()
+                )
+            }
+        ).build()
+    }
+
+    private fun createCreateOpenHelper(): XFunSpec {
         val scope = CodeGenScope(this)
-        return MethodSpec.methodBuilder("createOpenHelper").apply {
-            addModifiers(PROTECTED)
-            addAnnotation(Override::class.java)
-            returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
-
-            val configParam = ParameterSpec.builder(
-                RoomTypeNames.ROOM_DB_CONFIG,
-                "configuration"
-            ).build()
-            addParameter(configParam)
-
+        val configParamName = "config"
+        val body = XCodeBlock.builder(codeLanguage).apply {
             val openHelperVar = scope.getTmpVar("_helper")
             val openHelperCode = scope.fork()
             SQLiteOpenHelperWriter(database)
-                .write(openHelperVar, configParam, openHelperCode)
-            addCode(openHelperCode.builder().build())
-            addStatement("return $L", openHelperVar)
+                .write(openHelperVar, configParamName, openHelperCode)
+            add(openHelperCode.generate())
+            addStatement("return %L", openHelperVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "createOpenHelper",
+            visibility = VisibilityModifier.PROTECTED,
+            isOverride = true,
+        ).apply {
+            returns(SupportDbTypeNames.SQLITE_OPEN_HELPER)
+            addParameter(RoomTypeNames.ROOM_DB_CONFIG, configParamName)
+            addCode(body)
         }.build()
     }
 
-    private fun getAutoMigrations(): MethodSpec {
-        return MethodSpec.methodBuilder("getAutoMigrations").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(
-                ParameterSpec.builder(
-                    ParameterizedTypeName.get(
-                        CommonTypeNames.MAP,
-                        ParameterizedTypeName.get(
-                            ClassName.get(Class::class.java),
-                            WildcardTypeName.subtypeOf(RoomTypeNames.AUTO_MIGRATION_SPEC)
-                        ),
-                        RoomTypeNames.AUTO_MIGRATION_SPEC
-                    ),
-                    "autoMigrationSpecsMap"
-                ).addAnnotation(NonNull::class.java).build()
+    private fun getAutoMigrations(): XFunSpec {
+        val scope = CodeGenScope(this)
+        val classOfAutoMigrationSpecTypeName = CommonTypeNames.JAVA_CLASS.parametrizedBy(
+            XTypeName.getProducerExtendsName(RoomTypeNames.AUTO_MIGRATION_SPEC)
+        )
+        val autoMigrationsListTypeName =
+            CommonTypeNames.ARRAY_LIST.parametrizedBy(RoomTypeNames.MIGRATION)
+        val specsMapParamName = "autoMigrationSpecs"
+        val body = XCodeBlock.builder(codeLanguage).apply {
+            val listVar = scope.getTmpVar("_autoMigrations")
+            addLocalVariable(
+                name = listVar,
+                typeName = CommonTypeNames.LIST.parametrizedBy(RoomTypeNames.MIGRATION),
+                assignExpr = XCodeBlock.ofNewInstance(codeLanguage, autoMigrationsListTypeName)
             )
-
-            returns(ParameterizedTypeName.get(CommonTypeNames.LIST, RoomTypeNames.MIGRATION))
-            val autoMigrationsList = database.autoMigrations.map { autoMigrationResult ->
+            database.autoMigrations.forEach { autoMigrationResult ->
                 val implTypeName =
                     autoMigrationResult.getImplTypeName(database.typeName)
-                if (autoMigrationResult.isSpecProvided) {
-                    CodeBlock.of(
-                        "new $T(autoMigrationSpecsMap.get($T.class))",
-                        implTypeName.toJavaPoet(),
-                        autoMigrationResult.specClassName
+                val newInstanceCode = if (autoMigrationResult.isSpecProvided) {
+                    val specClassName = checkNotNull(autoMigrationResult.specClassName)
+                    // For Kotlin use getValue() as the Map's values are never null.
+                    val getFunction = when (language) {
+                        CodeLanguage.JAVA -> "get"
+                        CodeLanguage.KOTLIN -> "getValue"
+                    }
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        implTypeName,
+                        "%L.%L(%L)",
+                        specsMapParamName,
+                        getFunction,
+                        XCodeBlock.ofJavaClassLiteral(language, specClassName)
                     )
                 } else {
-                    CodeBlock.of("new $T()", implTypeName.toJavaPoet())
+                    XCodeBlock.ofNewInstance(language, implTypeName)
                 }
+                addStatement("%L.add(%L)", listVar, newInstanceCode)
             }
-            addStatement(
-                "return $T.asList($L)",
-                CommonTypeNames.ARRAYS,
-                CodeBlock.join(autoMigrationsList, ",$W")
+            addStatement("return %L", listVar)
+        }.build()
+        return XFunSpec.builder(
+            language = codeLanguage,
+            name = "getAutoMigrations",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true,
+        ).apply {
+            returns(CommonTypeNames.LIST.parametrizedBy(RoomTypeNames.MIGRATION))
+            addParameter(
+                CommonTypeNames.MAP.parametrizedBy(
+                    classOfAutoMigrationSpecTypeName,
+                    RoomTypeNames.AUTO_MIGRATION_SPEC,
+                ),
+                specsMapParamName
             )
+            addCode(body)
         }.build()
     }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
index c839722..8b856c3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FieldReadWriteWriter.kt
@@ -16,9 +16,10 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
 import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.codegen.XTypeName
-import androidx.room.compiler.codegen.toJavaPoet
+import androidx.room.compiler.processing.XNullability
 import androidx.room.ext.capitalize
 import androidx.room.ext.defaultValue
 import androidx.room.solver.CodeGenScope
@@ -366,7 +367,20 @@
                 field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
             } else {
                 beginControlFlow("if (%L == -1)", indexVar).apply {
-                    addStatement("%L = %L", tmpField, typeName.toJavaPoet().defaultValue())
+                    val defaultValue = typeName.defaultValue()
+                    if (
+                        language == CodeLanguage.KOTLIN &&
+                        typeName.nullability == XNullability.NONNULL &&
+                        defaultValue == "null"
+                    ) {
+                        // TODO(b/249984504): Generate / output a better message.
+                        addStatement(
+                            "error(%S)",
+                            "Missing column '${field.columnName}' for a non null value."
+                        )
+                    } else {
+                        addStatement("%L = %L", tmpField, defaultValue)
+                    }
                 }
                 nextControlFlow("else").apply {
                     field.cursorValueReader?.readFromCursor(tmpField, cursorVar, indexVar, scope)
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
index dcfd472..1c0dbbd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/FtsTableInfoValidationWriter.kt
@@ -16,58 +16,69 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.vo.FtsEntity
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
 import java.util.Locale
 
 class FtsTableInfoValidationWriter(val entity: FtsEntity) : ValidationWriter() {
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = entity.tableName.stripNonJava().capitalize(Locale.US)
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
-        scope.builder().apply {
-            val columnListVar = scope.getTmpVar("_columns$suffix")
-            val columnListType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                CommonTypeNames.STRING
-            )
-
-            addStatement(
-                "final $T $L = new $T($L)", columnListType, columnListVar,
-                columnListType, entity.fields.size
+        scope.builder.apply {
+            val columnSetVar = scope.getTmpVar("_columns$suffix")
+            val columnsSetType = CommonTypeNames.HASH_SET.parametrizedBy(CommonTypeNames.STRING)
+            addLocalVariable(
+                name = columnSetVar,
+                typeName = columnsSetType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    columnsSetType,
+                    "%L",
+                    entity.fields.size
+                )
             )
             entity.nonHiddenFields.forEach {
-                addStatement("$L.add($S)", columnListVar, it.columnName)
+                addStatement("%L.add(%S)", columnSetVar, it.columnName)
             }
 
-            addStatement(
-                "final $T $L = new $T($S, $L, $S)",
-                RoomTypeNames.FTS_TABLE_INFO, expectedInfoVar, RoomTypeNames.FTS_TABLE_INFO,
-                entity.tableName, columnListVar, entity.createTableQuery
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.FTS_TABLE_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.FTS_TABLE_INFO,
+                    "%S, %L, %S",
+                    entity.tableName, columnSetVar, entity.createTableQuery
+
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.FTS_TABLE_INFO, existingVar, RoomTypeNames.FTS_TABLE_INFO,
-                dbParam, entity.tableName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.FTS_TABLE_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.FTS_TABLE_INFO_READ, dbParamName, entity.tableName
             )
 
-            beginControlFlow("if (!$L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
index 3f06b01..b76318d 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/RelationCollectorFunctionWriter.kt
@@ -16,29 +16,27 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
 import androidx.room.compiler.codegen.XFunSpec
+import androidx.room.compiler.codegen.XFunSpec.Builder.Companion.addStatement
+import androidx.room.compiler.codegen.XMemberName.Companion.packageMember
+import androidx.room.compiler.codegen.XTypeName
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.compiler.codegen.toXTypeName
-import androidx.room.ext.AndroidTypeNames.CURSOR
+import androidx.room.ext.AndroidTypeNames
 import androidx.room.ext.CollectionTypeNames
+import androidx.room.ext.CollectionsSizeExprCode
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
-import androidx.room.ext.RoomTypeNames.CURSOR_UTIL
-import androidx.room.ext.RoomTypeNames.DB_UTIL
-import androidx.room.ext.RoomTypeNames.ROOM_DB
-import androidx.room.ext.S
-import androidx.room.ext.T
+import androidx.room.ext.Function1TypeSpec
+import androidx.room.ext.MapKeySetExprCode
+import androidx.room.ext.RoomMemberNames
+import androidx.room.ext.RoomTypeNames
 import androidx.room.ext.stripNonJava
 import androidx.room.solver.CodeGenScope
 import androidx.room.solver.query.result.PojoRowAdapter
 import androidx.room.vo.RelationCollector
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import javax.lang.model.element.Modifier
 
 /**
  * Writes the function that fetches the relations of a POJO and assigns them into the given map.
@@ -47,12 +45,18 @@
     private val collector: RelationCollector
 ) : TypeWriter.SharedFunctionSpec(
     "fetchRelationship${collector.relation.entity.tableName.stripNonJava()}" +
-        "As${collector.relation.pojoTypeName.toString().stripNonJava()}"
+        "As${collector.relation.pojoTypeName.toJavaPoet().toString().stripNonJava()}"
 ) {
     companion object {
         const val PARAM_MAP_VARIABLE = "_map"
         const val KEY_SET_VARIABLE = "__mapKeySet"
     }
+
+    private val usingLongSparseArray =
+        collector.mapTypeName.rawTypeName == CollectionTypeNames.LONG_SPARSE_ARRAY
+    private val usingArrayMap =
+        collector.mapTypeName.rawTypeName == CollectionTypeNames.ARRAY_MAP
+
     override fun getUniqueKey(): String {
         val relation = collector.relation
         return "RelationCollectorMethodWriter" +
@@ -65,196 +69,113 @@
 
     override fun prepare(methodName: String, writer: TypeWriter, builder: XFunSpec.Builder) {
         val scope = CodeGenScope(writer)
-        val relation = collector.relation
+        scope.builder.apply {
+            // Check the input map key set for emptiness, returning early as no fetching is needed.
+            addIsInputEmptyCheck()
 
-        val param = ParameterSpec.builder(collector.mapTypeName, PARAM_MAP_VARIABLE)
-            .addModifiers(Modifier.FINAL)
-            .build()
-        val sqlQueryVar = scope.getTmpVar("_sql")
-
-        val cursorVar = "_cursor"
-        val itemKeyIndexVar = "_itemKeyIndex"
-        val stmtVar = scope.getTmpVar("_stmt")
-        scope.builder().apply {
-            val usingLongSparseArray =
-                collector.mapTypeName.rawType == CollectionTypeNames.LONG_SPARSE_ARRAY
-            val usingArrayMap =
-                collector.mapTypeName.rawType == CollectionTypeNames.ARRAY_MAP
-            fun CodeBlock.Builder.addBatchPutAllStatement(tmpMapVar: String) {
-                if (usingArrayMap) {
-                    // When using ArrayMap there is ambiguity in the putAll() method, clear the
-                    // confusion by casting the temporary map.
-                    val disambiguityTypeName =
-                        ParameterizedTypeName.get(
-                            CommonTypeNames.MAP,
-                            collector.mapTypeName.typeArguments[0],
-                            collector.mapTypeName.typeArguments[1]
-                        )
-                    addStatement(
-                        "$N.putAll(($T) $L)",
-                        param, disambiguityTypeName, tmpMapVar
-                    )
-                } else {
-                    addStatement("$N.putAll($L)", param, tmpMapVar)
-                }
-            }
-            if (usingLongSparseArray) {
-                beginControlFlow("if ($N.isEmpty())", param)
-            } else {
-                val keySetType = ParameterizedTypeName.get(
-                    ClassName.get(Set::class.java), collector.keyTypeName
-                )
-                addStatement("final $T $L = $N.keySet()", keySetType, KEY_SET_VARIABLE, param)
-                beginControlFlow("if ($L.isEmpty())", KEY_SET_VARIABLE)
-            }.apply {
-                addStatement("return")
-            }
-            endControlFlow()
-            addStatement("// check if the size is too big, if so divide")
+            // Check if the input map key set exceeds MAX_BIND_PARAMETER_CNT, if so do a recursive
+            // fetch.
             beginControlFlow(
-                "if($N.size() > $T.MAX_BIND_PARAMETER_CNT)",
-                param, ROOM_DB.toJavaPoet()
-            ).apply {
-                // divide it into chunks
-                val tmpMapVar = scope.getTmpVar("_tmpInnerMap")
-                addStatement(
-                    "$T $L = new $T($L.MAX_BIND_PARAMETER_CNT)",
-                    collector.mapTypeName, tmpMapVar,
-                    collector.mapTypeName, ROOM_DB.toJavaPoet()
-                )
-                val tmpIndexVar = scope.getTmpVar("_tmpIndex")
-                addStatement("$T $L = 0", TypeName.INT, tmpIndexVar)
-                if (usingLongSparseArray || usingArrayMap) {
-                    val mapIndexVar = scope.getTmpVar("_mapIndex")
-                    val limitVar = scope.getTmpVar("_limit")
-                    addStatement("$T $L = 0", TypeName.INT, mapIndexVar)
-                    addStatement("final $T $L = $N.size()", TypeName.INT, limitVar, param)
-                    beginControlFlow("while($L < $L)", mapIndexVar, limitVar).apply {
-                        if (collector.relationTypeIsCollection) {
-                            addStatement(
-                                "$L.put($N.keyAt($L), $N.valueAt($L))",
-                                tmpMapVar, param, mapIndexVar, param, mapIndexVar
-                            )
-                        } else {
-                            addStatement(
-                                "$L.put($N.keyAt($L), null)",
-                                tmpMapVar, param, mapIndexVar
-                            )
-                        }
-                        addStatement("$L++", mapIndexVar)
-                    }
+                "if (%L > %T.MAX_BIND_PARAMETER_CNT)",
+                if (usingLongSparseArray) {
+                    XCodeBlock.of(language, "%L.size()", PARAM_MAP_VARIABLE)
                 } else {
-                    val mapKeyVar = scope.getTmpVar("_mapKey")
-                    beginControlFlow(
-                        "for($T $L : $L)",
-                        collector.keyTypeName, mapKeyVar, KEY_SET_VARIABLE
-                    ).apply {
-                        if (collector.relationTypeIsCollection) {
-                            addStatement(
-                                "$L.put($L, $N.get($L))",
-                                tmpMapVar, mapKeyVar, param, mapKeyVar
-                            )
-                        } else {
-                            addStatement("$L.put($L, null)", tmpMapVar, mapKeyVar)
-                        }
-                    }
-                }.apply {
-                    addStatement("$L++", tmpIndexVar)
-                    beginControlFlow(
-                        "if($L == $T.MAX_BIND_PARAMETER_CNT)",
-                        tmpIndexVar, ROOM_DB.toJavaPoet()
-                    ).apply {
-                        // recursively load that batch
-                        addStatement("$L($L)", methodName, tmpMapVar)
-                        // for non collection relation, put the loaded batch in the original map,
-                        // not needed when dealing with collections since references are passed
-                        if (!collector.relationTypeIsCollection) {
-                            addBatchPutAllStatement(tmpMapVar)
-                        }
-                        // clear nukes the backing data hence we create a new one
-                        addStatement(
-                            "$L = new $T($T.MAX_BIND_PARAMETER_CNT)",
-                            tmpMapVar, collector.mapTypeName, ROOM_DB.toJavaPoet()
-                        )
-                        addStatement("$L = 0", tmpIndexVar)
-                    }.endControlFlow()
-                }.endControlFlow()
-                beginControlFlow("if($L > 0)", tmpIndexVar).apply {
-                    // load the last batch
-                    addStatement("$L($L)", methodName, tmpMapVar)
-                    // for non collection relation, put the last batch in the original map
-                    if (!collector.relationTypeIsCollection) {
-                        addBatchPutAllStatement(tmpMapVar)
-                    }
-                }.endControlFlow()
+                    CollectionsSizeExprCode(language, PARAM_MAP_VARIABLE)
+                },
+                RoomTypeNames.ROOM_DB
+            ).apply {
+                addRecursiveFetchCall(methodName)
                 addStatement("return")
             }.endControlFlow()
+
+            // Create SQL query, acquire statement and bind parameters.
+            val stmtVar = scope.getTmpVar("_stmt")
+            val sqlQueryVar = scope.getTmpVar("_sql")
             collector.queryWriter.prepareReadAndBind(sqlQueryVar, stmtVar, scope)
 
+            // Perform query and get a Cursor
+            val cursorVar = "_cursor"
             val shouldCopyCursor = collector.rowAdapter.let {
                 it is PojoRowAdapter && it.relationCollectors.isNotEmpty()
             }
-            addStatement(
-                "final $T $L = $T.query($N, $L, $L, $L)",
-                CURSOR.toJavaPoet(),
-                cursorVar,
-                DB_UTIL.toJavaPoet(),
-                DaoWriter.DB_PROPERTY_NAME,
-                stmtVar,
-                if (shouldCopyCursor) "true" else "false",
-                "null"
+            addLocalVariable(
+                name = cursorVar,
+                typeName = AndroidTypeNames.CURSOR,
+                assignExpr = XCodeBlock.of(
+                    language,
+                    "%M(%N, %L, %L, %L)",
+                    RoomMemberNames.DB_UTIL_QUERY,
+                    DaoWriter.DB_PROPERTY_NAME,
+                    stmtVar,
+                    if (shouldCopyCursor) "true" else "false",
+                    "null"
+                )
             )
 
+            val relation = collector.relation
             beginControlFlow("try").apply {
+                // Gets index of the column to be used as key
+                val itemKeyIndexVar = "_itemKeyIndex"
                 if (relation.junction != null) {
-                    // when using a junction table the relationship map is keyed on the parent
+                    // When using a junction table the relationship map is keyed on the parent
                     // reference column of the junction table, the same column used in the WHERE IN
                     // clause, this column is the rightmost column in the generated SELECT
                     // clause.
                     val junctionParentColumnIndex = relation.projection.size
-                    addStatement(
-                        "final $T $L = $L; // _junction.$L",
-                        TypeName.INT, itemKeyIndexVar, junctionParentColumnIndex,
-                        relation.junction.parentField.columnName
+                    addStatement("// _junction.%L", relation.junction.parentField.columnName)
+                    addLocalVal(
+                        itemKeyIndexVar,
+                        XTypeName.PRIMITIVE_INT,
+                        "%L",
+                        junctionParentColumnIndex
                     )
                 } else {
-                    addStatement(
-                        "final $T $L = $T.getColumnIndex($L, $S)",
-                        TypeName.INT, itemKeyIndexVar, CURSOR_UTIL.toJavaPoet(), cursorVar,
+                    addLocalVal(
+                        itemKeyIndexVar,
+                        XTypeName.PRIMITIVE_INT,
+                        "%M(%L, %S)",
+                        RoomMemberNames.CURSOR_UTIL_GET_COLUMN_INDEX,
+                        cursorVar,
                         relation.entityField.columnName
                     )
                 }
-
-                beginControlFlow("if ($L == -1)", itemKeyIndexVar).apply {
+                // Check if index of column is not -1, indicating the column for the key is not in
+                // the result, can happen if the user specified a bad projection in @Relation.
+                beginControlFlow("if (%L == -1)", itemKeyIndexVar).apply {
                     addStatement("return")
                 }
                 endControlFlow()
 
+                // Prepare item column indices
                 collector.rowAdapter.onCursorReady(cursorVarName = cursorVar, scope = scope)
+
                 val tmpVarName = scope.getTmpVar("_item")
-                beginControlFlow("while($L.moveToNext())", cursorVar).apply {
-                    // read key from the cursor
+                beginControlFlow("while (%L.moveToNext())", cursorVar).apply {
+                    // Read key from the cursor, convert row to item and place it on map
                     collector.readKey(
                         cursorVarName = cursorVar,
                         indexVar = itemKeyIndexVar,
+                        keyReader = collector.entityKeyColumnReader,
                         scope = scope
                     ) { keyVar ->
                         if (collector.relationTypeIsCollection) {
                             val relationVar = scope.getTmpVar("_tmpRelation")
-                            addStatement(
-                                "$T $L = $N.get($L)", collector.relationTypeName,
-                                relationVar, param, keyVar
+                            addLocalVal(
+                                relationVar,
+                                collector.relationTypeName.copy(nullable = true),
+                                "%L.get(%L)",
+                                PARAM_MAP_VARIABLE, keyVar
                             )
-                            beginControlFlow("if ($L != null)", relationVar)
-                            addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
+                            beginControlFlow("if (%L != null)", relationVar)
+                            addLocalVariable(tmpVarName, relation.pojoTypeName)
                             collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("$L.add($L)", relationVar, tmpVarName)
+                            addStatement("%L.add(%L)", relationVar, tmpVarName)
                             endControlFlow()
                         } else {
-                            beginControlFlow("if ($N.containsKey($L))", param, keyVar)
-                            addStatement("final $T $L", relation.pojoTypeName, tmpVarName)
+                            beginControlFlow("if (%N.containsKey(%L))", PARAM_MAP_VARIABLE, keyVar)
+                            addLocalVariable(tmpVarName, relation.pojoTypeName)
                             collector.rowAdapter.convert(tmpVarName, cursorVar, scope)
-                            addStatement("$N.put($L, $L)", param, keyVar, tmpVarName)
+                            addStatement("%N.put(%L, %L)", PARAM_MAP_VARIABLE, keyVar, tmpVarName)
                             endControlFlow()
                         }
                     }
@@ -262,13 +183,89 @@
                 endControlFlow()
             }
             nextControlFlow("finally").apply {
-                addStatement("$L.close()", cursorVar)
+                addStatement("%L.close()", cursorVar)
             }
             endControlFlow()
         }
         builder.apply {
-            addParameter(param.type.toXTypeName(), param.name)
+            addParameter(collector.mapTypeName, PARAM_MAP_VARIABLE)
             addCode(scope.generate())
         }
     }
+
+    private fun XCodeBlock.Builder.addIsInputEmptyCheck() {
+        if (usingLongSparseArray) {
+            beginControlFlow("if (%L.isEmpty())", PARAM_MAP_VARIABLE)
+        } else {
+            val keySetType = CommonTypeNames.SET.parametrizedBy(collector.keyTypeName)
+            addLocalVariable(
+                name = KEY_SET_VARIABLE,
+                typeName = keySetType,
+                assignExpr = MapKeySetExprCode(language, PARAM_MAP_VARIABLE)
+            )
+            beginControlFlow("if (%L.isEmpty())", KEY_SET_VARIABLE)
+        }.apply {
+            addStatement("return")
+        }
+        endControlFlow()
+    }
+
+    private fun XCodeBlock.Builder.addRecursiveFetchCall(methodName: String) {
+        fun getRecursiveCall(itVarName: String) =
+            XCodeBlock.of(
+                language,
+                "%L(%L)",
+                methodName, itVarName
+            )
+        val utilFunction =
+            RoomTypeNames.RELATION_UTIL.let {
+                when {
+                    usingLongSparseArray ->
+                        it.packageMember("recursiveFetchLongSparseArray")
+                    usingArrayMap ->
+                        it.packageMember("recursiveFetchArrayMap")
+                    else ->
+                        it.packageMember("recursiveFetchHashMap")
+                }
+            }
+        when (language) {
+            CodeLanguage.JAVA -> {
+                val paramName = "map"
+                if (collector.javaLambdaSyntaxAvailable) {
+                    add("%M(%L, %L, (%L) -> {\n",
+                        utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection,
+                        paramName
+                    )
+                    indent()
+                    addStatement("%L", getRecursiveCall(paramName))
+                    addStatement("return %T.INSTANCE", Unit::class.asClassName())
+                    unindent()
+                    addStatement("})")
+                } else {
+                    val functionImpl = Function1TypeSpec(
+                        language = language,
+                        parameterTypeName = collector.mapTypeName,
+                        parameterName = paramName,
+                        returnTypeName = Unit::class.asClassName(),
+                    ) {
+                        addStatement("%L", getRecursiveCall(paramName))
+                        addStatement("return %T.INSTANCE", Unit::class.asClassName())
+                    }
+                    addStatement(
+                        "%M(%L, %L, %L)",
+                        utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection,
+                        functionImpl
+                    )
+                }
+            }
+            CodeLanguage.KOTLIN -> {
+                beginControlFlow(
+                    "%M(%L, %L)",
+                    utilFunction, PARAM_MAP_VARIABLE, collector.relationTypeIsCollection
+                )
+                addStatement("%L", getRecursiveCall("it"))
+                endControlFlow()
+            }
+        }
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
index dd62e1c..dc152fb 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/SQLiteOpenHelperWriter.kt
@@ -17,25 +17,25 @@
 package androidx.room.writer
 
 import androidx.annotation.VisibleForTesting
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.VisibilityModifier
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.beginForEachControlFlow
+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.ext.CommonTypeNames
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.RoomTypeNames.DB_UTIL
-import androidx.room.ext.S
 import androidx.room.ext.SupportDbTypeNames
-import androidx.room.ext.T
 import androidx.room.solver.CodeGenScope
 import androidx.room.vo.Database
 import androidx.room.vo.DatabaseView
 import androidx.room.vo.Entity
 import androidx.room.vo.FtsEntity
-import com.squareup.javapoet.MethodSpec
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.TypeSpec
 import java.util.ArrayDeque
-import javax.lang.model.element.Modifier.PRIVATE
-import javax.lang.model.element.Modifier.PUBLIC
 
 /**
  * The threshold amount of statements in a validateMigration() method before creating additional
@@ -47,215 +47,279 @@
  * Create an open helper using SupportSQLiteOpenHelperFactory
  */
 class SQLiteOpenHelperWriter(val database: Database) {
-    fun write(outVar: String, configuration: ParameterSpec, scope: CodeGenScope) {
-        scope.builder().apply {
+
+    private val dbParamName = "db"
+
+    fun write(outVar: String, configParamName: String, scope: CodeGenScope) {
+        scope.builder.apply {
             val sqliteConfigVar = scope.getTmpVar("_sqliteConfig")
             val callbackVar = scope.getTmpVar("_openCallback")
-            addStatement(
-                "final $T $L = new $T($N, $L, $S, $S)",
-                SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
-                callbackVar, RoomTypeNames.OPEN_HELPER, configuration,
-                createOpenCallback(scope), database.identityHash, database.legacyIdentityHash
+            addLocalVariable(
+                name = callbackVar,
+                typeName = SupportDbTypeNames.SQLITE_OPEN_HELPER_CALLBACK,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.OPEN_HELPER,
+                    "%L, %L, %S, %S",
+                    configParamName,
+                    createOpenCallback(scope),
+                    database.identityHash,
+                    database.legacyIdentityHash
+                )
             )
             // build configuration
-            addStatement(
-                """
-                    final $T $L = $T.builder($N.context)
-                    .name($N.name)
-                    .callback($L)
-                    .build()
-                """.trimIndent(),
-                SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG, sqliteConfigVar,
+            addLocalVal(
+                sqliteConfigVar,
                 SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
-                configuration, configuration, callbackVar
+                "%T.builder(%L.context).name(%L.name).callback(%L).build()",
+                SupportDbTypeNames.SQLITE_OPEN_HELPER_CONFIG,
+                configParamName,
+                configParamName,
+                callbackVar
             )
-            addStatement(
-                "final $T $N = $N.sqliteOpenHelperFactory.create($L)",
-                SupportDbTypeNames.SQLITE_OPEN_HELPER, outVar,
-                configuration, sqliteConfigVar
+            addLocalVal(
+                outVar,
+                SupportDbTypeNames.SQLITE_OPEN_HELPER,
+                "%L.sqliteOpenHelperFactory.create(%L)",
+                configParamName,
+                sqliteConfigVar
             )
         }
     }
 
-    private fun createOpenCallback(scope: CodeGenScope): TypeSpec {
-        return TypeSpec.anonymousClassBuilder(L, database.version).apply {
+    private fun createOpenCallback(scope: CodeGenScope): XTypeSpec {
+        return XTypeSpec.anonymousClassBuilder(
+            scope.language, "%L", database.version
+        ).apply {
             superclass(RoomTypeNames.OPEN_HELPER_DELEGATE)
-            addMethod(createCreateAllTables())
-            addMethod(createDropAllTables(scope.fork()))
-            addMethod(createOnCreate(scope.fork()))
-            addMethod(createOnOpen(scope.fork()))
-            addMethod(createOnPreMigrate())
-            addMethod(createOnPostMigrate())
-            addMethods(createValidateMigration(scope.fork()))
+            addFunction(createCreateAllTables(scope))
+            addFunction(createDropAllTables(scope.fork()))
+            addFunction(createOnCreate(scope.fork()))
+            addFunction(createOnOpen(scope.fork()))
+            addFunction(createOnPreMigrate(scope))
+            addFunction(createOnPostMigrate(scope))
+            createValidateMigration(scope.fork()).forEach {
+                addFunction(it)
+            }
         }.build()
     }
 
-    private fun createValidateMigration(scope: CodeGenScope): List<MethodSpec> {
-        val methodSpecs = mutableListOf<MethodSpec>()
+    private fun createValidateMigration(scope: CodeGenScope): List<XFunSpec> {
+        val methodBuilders = mutableListOf<XFunSpec.Builder>()
         val entities = ArrayDeque(database.entities)
         val views = ArrayDeque(database.views)
-        val dbParam = ParameterSpec.builder(SupportDbTypeNames.DB, "_db").build()
         while (!entities.isEmpty() || !views.isEmpty()) {
-            val isPrimaryMethod = methodSpecs.isEmpty()
+            val isPrimaryMethod = methodBuilders.isEmpty()
             val methodName = if (isPrimaryMethod) {
                 "onValidateSchema"
             } else {
-                "onValidateSchema${methodSpecs.size + 1}"
+                "onValidateSchema${methodBuilders.size + 1}"
             }
-            methodSpecs.add(
-                MethodSpec.methodBuilder(methodName).apply {
-                    if (isPrimaryMethod) {
-                        addModifiers(PUBLIC)
-                        addAnnotation(Override::class.java)
-                    } else {
-                        addModifiers(PRIVATE)
+            val validateMethod = XFunSpec.builder(
+                language = scope.language,
+                name = methodName,
+                visibility = if (isPrimaryMethod) {
+                    VisibilityModifier.PUBLIC
+                } else {
+                    VisibilityModifier.PRIVATE
+                },
+                isOverride = isPrimaryMethod
+            ).apply {
+                returns(RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT)
+                addParameter(SupportDbTypeNames.DB, dbParamName)
+                var statementCount = 0
+                while (!entities.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
+                    val methodScope = scope.fork()
+                    val entity = entities.poll()
+                    val validationWriter = when (entity) {
+                        is FtsEntity -> FtsTableInfoValidationWriter(entity)
+                        else -> TableInfoValidationWriter(entity)
                     }
-                    returns(RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT)
-                    addParameter(dbParam)
-                    var statementCount = 0
-                    while (!entities.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
-                        val methodScope = scope.fork()
-                        val entity = entities.poll()
-                        val validationWriter = when (entity) {
-                            is FtsEntity -> FtsTableInfoValidationWriter(entity)
-                            else -> TableInfoValidationWriter(entity)
-                        }
-                        validationWriter.write(dbParam, methodScope)
-                        addCode(methodScope.builder().build())
-                        statementCount += validationWriter.statementCount()
-                    }
-                    while (!views.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
-                        val methodScope = scope.fork()
-                        val view = views.poll()
-                        val validationWriter = ViewInfoValidationWriter(view)
-                        validationWriter.write(dbParam, methodScope)
-                        addCode(methodScope.builder().build())
-                        statementCount += validationWriter.statementCount()
-                    }
-                    if (!isPrimaryMethod) {
-                        addStatement(
-                            "return new $T(true, null)",
-                            RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+                    validationWriter.write(dbParamName, methodScope)
+                    addCode(methodScope.generate())
+                    statementCount += validationWriter.statementCount()
+                }
+                while (!views.isEmpty() && statementCount < VALIDATE_CHUNK_SIZE) {
+                    val methodScope = scope.fork()
+                    val view = views.poll()
+                    val validationWriter = ViewInfoValidationWriter(view)
+                    validationWriter.write(dbParamName, methodScope)
+                    addCode(methodScope.generate())
+                    statementCount += validationWriter.statementCount()
+                }
+                if (!isPrimaryMethod) {
+                    addStatement(
+                        "return %L",
+                        XCodeBlock.ofNewInstance(
+                            scope.language,
+                            RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                            "true, null"
                         )
-                    }
-                }.build()
-            )
+                    )
+                }
+            }
+            methodBuilders.add(validateMethod)
         }
 
         // If there are secondary validate methods then add invocation statements to all of them
         // from the primary method.
-        if (methodSpecs.size > 1) {
-            methodSpecs[0] = methodSpecs[0].toBuilder().apply {
+        if (methodBuilders.size > 1) {
+            val body = XCodeBlock.builder(scope.language).apply {
                 val resultVar = scope.getTmpVar("_result")
-                addStatement("$T $L", RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT, resultVar)
-                methodSpecs.drop(1).forEach {
-                    addStatement("$L = ${it.name}($N)", resultVar, dbParam)
-                    beginControlFlow("if (!$L.isValid)", resultVar)
-                    addStatement("return $L", resultVar)
+                addLocalVariable(
+                    name = resultVar,
+                    typeName = RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                    isMutable = true
+                )
+                methodBuilders.drop(1).forEach {
+                    addStatement("%L = %L(%L)", resultVar, it.name, dbParamName)
+                    beginControlFlow("if (!%L.isValid)", resultVar).apply {
+                        addStatement("return %L", resultVar)
+                    }
                     endControlFlow()
                 }
                 addStatement(
-                    "return new $T(true, null)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        scope.language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "true, null"
+                    )
                 )
             }.build()
-        } else if (methodSpecs.size == 1) {
-            methodSpecs[0] = methodSpecs[0].toBuilder().apply {
-                addStatement(
-                    "return new $T(true, null)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT
+            methodBuilders.first().addCode(body)
+        } else if (methodBuilders.size == 1) {
+            methodBuilders.first().addStatement(
+                "return %L",
+                XCodeBlock.ofNewInstance(
+                    scope.language,
+                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                    "true, null"
                 )
-            }.build()
+            )
         }
-
-        return methodSpecs
+        return methodBuilders.map { it.build() }
     }
 
-    private fun createOnCreate(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("onCreate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            invokeCallbacks(scope, "onCreate")
+    private fun createOnCreate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onCreate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addCode(createInvokeCallbacksCode(scope, "onCreate"))
         }.build()
     }
 
-    private fun createOnOpen(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("onOpen").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("mDatabase = _db")
+    private fun createOnOpen(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onOpen",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addStatement("mDatabase = %L", dbParamName)
             if (database.enableForeignKeys) {
-                addStatement("_db.execSQL($S)", "PRAGMA foreign_keys = ON")
+                addStatement("%L.execSQL(%S)", dbParamName, "PRAGMA foreign_keys = ON")
             }
-            addStatement("internalInitInvalidationTracker(_db)")
-            invokeCallbacks(scope, "onOpen")
+            addStatement("internalInitInvalidationTracker(%L)", dbParamName)
+            addCode(createInvokeCallbacksCode(scope, "onOpen"))
         }.build()
     }
 
-    private fun createCreateAllTables(): MethodSpec {
-        return MethodSpec.methodBuilder("createAllTables").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createCreateAllTables(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "createAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.bundle.buildCreateQueries().forEach {
-                addStatement("_db.execSQL($S)", it)
+                addStatement("%L.execSQL(%S)", dbParamName, it)
             }
         }.build()
     }
 
-    private fun createDropAllTables(scope: CodeGenScope): MethodSpec {
-        return MethodSpec.methodBuilder("dropAllTables").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createDropAllTables(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "dropAllTables",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.entities.forEach {
-                addStatement("_db.execSQL($S)", createDropTableQuery(it))
+                addStatement("%L.execSQL(%S)", dbParamName, createDropTableQuery(it))
             }
             database.views.forEach {
-                addStatement("_db.execSQL($S)", createDropViewQuery(it))
+                addStatement("%L.execSQL(%S)", dbParamName, createDropViewQuery(it))
             }
-            invokeCallbacks(scope, "onDestructiveMigration")
+            addCode(createInvokeCallbacksCode(scope, "onDestructiveMigration"))
         }.build()
     }
 
-    private fun createOnPreMigrate(): MethodSpec {
-        return MethodSpec.methodBuilder("onPreMigrate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
-            addStatement("$T.dropFtsSyncTriggers($L)", DB_UTIL.toJavaPoet(), "_db")
+    private fun createOnPreMigrate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onPreMigrate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
+            addStatement("%M(%L)", RoomMemberNames.DB_UTIL_DROP_FTS_SYNC_TRIGGERS, dbParamName)
         }.build()
     }
 
-    private fun createOnPostMigrate(): MethodSpec {
-        return MethodSpec.methodBuilder("onPostMigrate").apply {
-            addModifiers(PUBLIC)
-            addAnnotation(Override::class.java)
-            addParameter(SupportDbTypeNames.DB, "_db")
+    private fun createOnPostMigrate(scope: CodeGenScope): XFunSpec {
+        return XFunSpec.builder(
+            language = scope.language,
+            name = "onPostMigrate",
+            visibility = VisibilityModifier.PUBLIC,
+            isOverride = true
+        ).apply {
+            addParameter(SupportDbTypeNames.DB, dbParamName)
             database.entities.filterIsInstance(FtsEntity::class.java)
                 .filter { it.ftsOptions.contentEntity != null }
                 .flatMap { it.contentSyncTriggerCreateQueries }
                 .forEach { syncTriggerQuery ->
-                    addStatement("_db.execSQL($S)", syncTriggerQuery)
+                    addStatement("%L.execSQL(%S)", dbParamName, syncTriggerQuery)
                 }
         }.build()
     }
 
-    private fun MethodSpec.Builder.invokeCallbacks(scope: CodeGenScope, methodName: String) {
-        val iVar = scope.getTmpVar("_i")
-        val sizeVar = scope.getTmpVar("_size")
-        beginControlFlow("if (mCallbacks != null)").apply {
-            beginControlFlow(
-                "for (int $N = 0, $N = mCallbacks.size(); $N < $N; $N++)",
-                iVar, sizeVar, iVar, sizeVar, iVar
-            ).apply {
-                addStatement("mCallbacks.get($N).$N(_db)", iVar, methodName)
+    private fun createInvokeCallbacksCode(scope: CodeGenScope, methodName: String): XCodeBlock {
+        val localCallbackListVarName = scope.getTmpVar("_callbacks")
+        val callbackVarName = scope.getTmpVar("_callback")
+        return XCodeBlock.builder(scope.language).apply {
+            addLocalVal(
+                localCallbackListVarName,
+                CommonTypeNames.LIST.parametrizedBy(
+                    // For Kotlin, the variance is redundant, but for Java, due to `mCallbacks`
+                    // not having @JvmSuppressWildcards, we use a wildcard name.
+                    if (language == CodeLanguage.KOTLIN) {
+                        RoomTypeNames.ROOM_DB_CALLBACK
+                    } else {
+                        XTypeName.getProducerExtendsName(RoomTypeNames.ROOM_DB_CALLBACK)
+                    }
+                ).copy(nullable = true),
+                "mCallbacks"
+            )
+            beginControlFlow("if (%L != null)", localCallbackListVarName).apply {
+                beginForEachControlFlow(
+                    itemVarName = callbackVarName,
+                    typeName = RoomTypeNames.ROOM_DB_CALLBACK,
+                    iteratorVarName = localCallbackListVarName
+                ).apply {
+                    addStatement("%L.%L(%L)", callbackVarName, methodName, dbParamName)
+                }
+                endControlFlow()
             }
             endControlFlow()
-        }
-        endControlFlow()
+        }.build()
     }
 
     @VisibleForTesting
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
index 1edc663..c7c18fd 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/TableInfoValidationWriter.kt
@@ -16,138 +16,177 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.CodeLanguage
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.compiler.codegen.asClassName
 import androidx.room.ext.CommonTypeNames
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
-import androidx.room.ext.typeName
 import androidx.room.parser.SQLTypeAffinity
 import androidx.room.vo.Entity
 import androidx.room.vo.columnNames
-import com.squareup.javapoet.ParameterSpec
-import com.squareup.javapoet.ParameterizedTypeName
 import java.util.Arrays
-import java.util.HashMap
-import java.util.HashSet
 import java.util.Locale
 
 class TableInfoValidationWriter(val entity: Entity) : ValidationWriter() {
 
     companion object {
         const val CREATED_FROM_ENTITY = "CREATED_FROM_ENTITY"
+        val ARRAY_TYPE_NAME = Arrays::class.asClassName()
     }
 
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = entity.tableName.stripNonJava().capitalize(Locale.US)
         val expectedInfoVar = scope.getTmpVar("_info$suffix")
-        scope.builder().apply {
+        scope.builder.apply {
             val columnListVar = scope.getTmpVar("_columns$suffix")
-            val columnListType = ParameterizedTypeName.get(
-                HashMap::class.typeName,
-                CommonTypeNames.STRING, RoomTypeNames.TABLE_INFO_COLUMN
+            val columnListType = CommonTypeNames.HASH_MAP.parametrizedBy(
+                CommonTypeNames.STRING,
+                RoomTypeNames.TABLE_INFO_COLUMN
             )
-
-            addStatement(
-                "final $T $L = new $T($L)", columnListType, columnListVar,
-                columnListType, entity.fields.size
+            addLocalVariable(
+                name = columnListVar,
+                typeName = columnListType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    columnListType,
+                    "%L",
+                    entity.fields.size
+                )
             )
             entity.fields.forEach { field ->
                 addStatement(
-                    "$L.put($S, new $T($S, $S, $L, $L, $S, $T.$L))",
-                    columnListVar, field.columnName, RoomTypeNames.TABLE_INFO_COLUMN,
-                    /*name*/ field.columnName,
-                    /*type*/ field.affinity?.name ?: SQLTypeAffinity.TEXT.name,
-                    /*nonNull*/ field.nonNull,
-                    /*pkeyPos*/ entity.primaryKey.fields.indexOf(field) + 1,
-                    /*defaultValue*/ field.defaultValue,
-                    /*createdFrom*/ RoomTypeNames.TABLE_INFO, CREATED_FROM_ENTITY
+                    "%L.put(%S, %L)",
+                    columnListVar,
+                    field.columnName,
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_COLUMN,
+                        "%S, %S, %L, %L, %S, %T.%L",
+                        field.columnName, // name
+                        field.affinity?.name ?: SQLTypeAffinity.TEXT.name, // type
+                        field.nonNull, // nonNull
+                        entity.primaryKey.fields.indexOf(field) + 1, // pkeyPos
+                        field.defaultValue, // defaultValue
+                        RoomTypeNames.TABLE_INFO, CREATED_FROM_ENTITY // createdFrom
+                    )
                 )
             }
 
             val foreignKeySetVar = scope.getTmpVar("_foreignKeys$suffix")
-            val foreignKeySetType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                RoomTypeNames.TABLE_INFO_FOREIGN_KEY
-            )
-            addStatement(
-                "final $T $L = new $T($L)", foreignKeySetType, foreignKeySetVar,
-                foreignKeySetType, entity.foreignKeys.size
+            val foreignKeySetType =
+                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_FOREIGN_KEY)
+            addLocalVariable(
+                name = foreignKeySetVar,
+                typeName = foreignKeySetType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    foreignKeySetType,
+                    "%L",
+                    entity.foreignKeys.size
+                )
             )
             entity.foreignKeys.forEach {
-                val myColumnNames = it.childFields
-                    .joinToString(",") { "\"${it.columnName}\"" }
-                val refColumnNames = it.parentColumns
-                    .joinToString(",") { "\"$it\"" }
                 addStatement(
-                    "$L.add(new $T($S, $S, $S," +
-                        "$T.asList($L), $T.asList($L)))",
+                    "%L.add(%L)",
                     foreignKeySetVar,
-                    RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
-                    /*parent table*/ it.parentTable,
-                    /*on delete*/ it.onDelete.sqlName,
-                    /*on update*/ it.onUpdate.sqlName,
-                    Arrays::class.typeName,
-                    /*parent names*/ myColumnNames,
-                    Arrays::class.typeName,
-                    /*parent column names*/ refColumnNames
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_FOREIGN_KEY,
+                        "%S, %S, %S, %L, %L",
+                        it.parentTable, // parent table
+                        it.onDelete.sqlName, // on delete
+                        it.onUpdate.sqlName, // on update
+                        listOfStrings(it.childFields.map { it.columnName }), // parent names
+                        listOfStrings(it.parentColumns) // parent column names
+                    )
                 )
             }
 
             val indicesSetVar = scope.getTmpVar("_indices$suffix")
-            val indicesType = ParameterizedTypeName.get(
-                HashSet::class.typeName,
-                RoomTypeNames.TABLE_INFO_INDEX
-            )
-            addStatement(
-                "final $T $L = new $T($L)", indicesType, indicesSetVar,
-                indicesType, entity.indices.size
+            val indicesType =
+                CommonTypeNames.HASH_SET.parametrizedBy(RoomTypeNames.TABLE_INFO_INDEX)
+            addLocalVariable(
+                name = indicesSetVar,
+                typeName = indicesType,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    indicesType,
+                    "%L",
+                    entity.indices.size
+                )
             )
             entity.indices.forEach { index ->
-                val columnNames = index.columnNames.joinToString(",") { "\"$it\"" }
                 val orders = if (index.orders.isEmpty()) {
-                    index.columnNames.map { "ASC" }.joinToString(",") { "\"$it\"" }
+                    index.columnNames.map { "ASC" }
                 } else {
-                    index.orders.joinToString(",") { "\"$it\"" }
+                    index.orders.map { it.name }
                 }
                 addStatement(
-                    "$L.add(new $T($S, $L, $T.asList($L), $T.asList($L)))",
+                    "%L.add(%L)",
                     indicesSetVar,
-                    RoomTypeNames.TABLE_INFO_INDEX,
-                    index.name,
-                    index.unique,
-                    Arrays::class.typeName,
-                    columnNames,
-                    Arrays::class.typeName,
-                    orders,
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.TABLE_INFO_INDEX,
+                        "%S, %L, %L, %L",
+                        index.name, // name
+                        index.unique, // unique
+                        listOfStrings(index.columnNames), // columns
+                        listOfStrings(orders) // orders
+                    )
                 )
             }
 
-            addStatement(
-                "final $T $L = new $T($S, $L, $L, $L)",
-                RoomTypeNames.TABLE_INFO, expectedInfoVar, RoomTypeNames.TABLE_INFO,
-                entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.TABLE_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.TABLE_INFO,
+                    "%S, %L, %L, %L",
+                    entity.tableName, columnListVar, foreignKeySetVar, indicesSetVar
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.TABLE_INFO, existingVar, RoomTypeNames.TABLE_INFO,
-                dbParam, entity.tableName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.TABLE_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.TABLE_INFO_READ, dbParamName, entity.tableName
             )
 
-            beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${entity.tableName}(${entity.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
         }
     }
+
+    private fun CodeBlockWrapper.listOfStrings(strings: List<String>): XCodeBlock {
+        val placeholders = List(strings.size) { "%S" }.joinToString()
+        val function: Any = when (language) {
+            CodeLanguage.JAVA -> XCodeBlock.of(language, "%T.asList", ARRAY_TYPE_NAME)
+            CodeLanguage.KOTLIN -> "listOf"
+        }
+        return XCodeBlock.of(
+            language,
+            "%L($placeholders)",
+            function, *strings.toTypedArray()
+        )
+    }
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
index 7499770..cede6be 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/ValidationWriter.kt
@@ -16,9 +16,9 @@
 
 package androidx.room.writer
 
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XTypeName
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.CodeBlock
-import com.squareup.javapoet.ParameterSpec
 
 /**
  * Common interface for database validation witters.
@@ -27,53 +27,65 @@
 
     private lateinit var countingScope: CountingCodeGenScope
 
-    fun write(dbParam: ParameterSpec, scope: CodeGenScope) {
+    fun write(dbParamName: String, scope: CodeGenScope) {
         countingScope = CountingCodeGenScope(scope)
-        write(dbParam, countingScope)
+        write(dbParamName, countingScope)
     }
 
-    protected abstract fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope)
+    protected abstract fun write(dbParamName: String, scope: CountingCodeGenScope)
 
     /**
      * The estimated amount of statements this writer will write.
      */
     fun statementCount() = countingScope.statementCount()
 
-    protected class CountingCodeGenScope(val scope: CodeGenScope) {
+    protected class CountingCodeGenScope(private val scope: CodeGenScope) {
 
-        private var builder: CodeBlockWrapper? = null
+        val builder = CodeBlockWrapper(scope.builder)
 
         fun getTmpVar(prefix: String) = scope.getTmpVar(prefix)
 
-        fun builder(): CodeBlockWrapper {
-            if (builder == null) {
-                builder = CodeBlockWrapper(scope.builder())
-            }
-            return builder!!
-        }
-
-        fun statementCount() = builder?.statementCount ?: 0
+        fun statementCount() = builder.statementCount
     }
 
     // A wrapper class that counts statements added to a CodeBlock
-    protected class CodeBlockWrapper(val builder: CodeBlock.Builder) {
+    protected class CodeBlockWrapper(
+        private val builder: XCodeBlock.Builder
+    ) : XCodeBlock.Builder by builder {
 
         var statementCount = 0
             private set
 
-        fun addStatement(format: String, vararg args: Any?): CodeBlockWrapper {
+        override fun add(format: String, vararg args: Any?): XCodeBlock.Builder {
+            statementCount++
+            builder.add(format, *args)
+            return this
+        }
+
+        override fun addLocalVariable(
+            name: String,
+            typeName: XTypeName,
+            isMutable: Boolean,
+            assignExpr: XCodeBlock?
+        ): XCodeBlock.Builder {
+            statementCount++
+            builder.addLocalVariable(name, typeName, isMutable, assignExpr)
+            return this
+        }
+
+        override fun addStatement(format: String, vararg args: Any?): CodeBlockWrapper {
             statementCount++
             builder.addStatement(format, *args)
             return this
         }
 
-        fun beginControlFlow(controlFlow: String, vararg args: Any): CodeBlockWrapper {
+        override fun beginControlFlow(controlFlow: String, vararg args: Any?): CodeBlockWrapper {
             statementCount++
             builder.beginControlFlow(controlFlow, *args)
             return this
         }
 
-        fun endControlFlow(): CodeBlockWrapper {
+        override fun endControlFlow(): CodeBlockWrapper {
             builder.endControlFlow()
             return this
         }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt b/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
index 5e2b463..25fd562 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/writer/ViewInfoValidationWriter.kt
@@ -16,42 +16,52 @@
 
 package androidx.room.writer
 
-import androidx.room.ext.L
-import androidx.room.ext.N
+import androidx.room.compiler.codegen.XCodeBlock
+import androidx.room.compiler.codegen.XCodeBlock.Builder.Companion.addLocalVal
+import androidx.room.ext.RoomMemberNames
 import androidx.room.ext.RoomTypeNames
-import androidx.room.ext.S
-import androidx.room.ext.T
 import androidx.room.ext.capitalize
 import androidx.room.ext.stripNonJava
 import androidx.room.vo.DatabaseView
-import com.squareup.javapoet.ParameterSpec
 import java.util.Locale
 
 class ViewInfoValidationWriter(val view: DatabaseView) : ValidationWriter() {
 
-    override fun write(dbParam: ParameterSpec, scope: CountingCodeGenScope) {
+    override fun write(dbParamName: String, scope: CountingCodeGenScope) {
         val suffix = view.viewName.stripNonJava().capitalize(Locale.US)
-        scope.builder().apply {
+        scope.builder.apply {
             val expectedInfoVar = scope.getTmpVar("_info$suffix")
-            addStatement(
-                "final $T $L = new $T($S, $S)",
-                RoomTypeNames.VIEW_INFO, expectedInfoVar, RoomTypeNames.VIEW_INFO,
-                view.viewName, view.createViewQuery
+            addLocalVariable(
+                name = expectedInfoVar,
+                typeName = RoomTypeNames.VIEW_INFO,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    RoomTypeNames.VIEW_INFO,
+                    "%S, %S",
+                    view.viewName, view.createViewQuery
+                )
             )
 
             val existingVar = scope.getTmpVar("_existing$suffix")
-            addStatement(
-                "final $T $L = $T.read($N, $S)",
-                RoomTypeNames.VIEW_INFO, existingVar, RoomTypeNames.VIEW_INFO,
-                dbParam, view.viewName
+            addLocalVal(
+                existingVar,
+                RoomTypeNames.VIEW_INFO,
+                "%M(%L, %S)",
+                RoomMemberNames.VIEW_INFO_READ, dbParamName, view.viewName
             )
 
-            beginControlFlow("if (! $L.equals($L))", expectedInfoVar, existingVar).apply {
+            beginControlFlow("if (!%L.equals(%L))", expectedInfoVar, existingVar).apply {
                 addStatement(
-                    "return new $T(false, $S + $L + $S + $L)",
-                    RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
-                    "${view.viewName}(${view.element.qualifiedName}).\n Expected:\n",
-                    expectedInfoVar, "\n Found:\n", existingVar
+                    "return %L",
+                    XCodeBlock.ofNewInstance(
+                        language,
+                        RoomTypeNames.OPEN_HELPER_VALIDATION_RESULT,
+                        "false, %S + %L + %S + %L",
+                        "${view.viewName}(${view.element.qualifiedName}).\n Expected:\n",
+                        expectedInfoVar,
+                        "\n Found:\n",
+                        existingVar
+                    )
                 )
             }
             endControlFlow()
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
index ee95322..6c0353f 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/DeleteOrUpdateShortcutMethodProcessorTest.kt
@@ -27,7 +27,7 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.STRING
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -308,7 +308,7 @@
                 `is`(
                     ParameterizedTypeName.get(
                         ClassName.get("foo.bar", "MyClass.MyList"),
-                        CommonTypeNames.STRING, COMMON.USER_TYPE_NAME
+                        STRING.toJavaPoet(), COMMON.USER_TYPE_NAME
                     ) as TypeName
                 )
             )
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
index cf952dd..1c3792d 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/EntityNameMatchingVariationsTest.kt
@@ -76,7 +76,7 @@
             assertThat(field.setter)
                 .isEqualTo(FieldSetter(field.name, setterName, intType, CallType.METHOD))
             assertThat(field.getter)
-                .isEqualTo(FieldGetter(field.name, getterName, intType, CallType.METHOD))
+                .isEqualTo(FieldGetter(field.name, getterName, intType, CallType.METHOD, true))
         }
     }
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
index 2f48c05..bf4a2d5 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts3TableEntityProcessorTest.kt
@@ -82,7 +82,7 @@
             assertThat(field.setter,
                 `is`(FieldSetter("rowId", "setRowId", intType, CallType.METHOD)))
             assertThat(field.getter,
-                `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD)))
+                `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD, true)))
             assertThat(entity.primaryKey.fields, `is`(Fields(field)))
             assertThat(entity.shadowTableName, `is`("MyEntity_content"))
             assertThat(entity.ftsVersion, `is`(FtsVersion.FTS3))
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
index 2b220be..3434fdf 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/Fts4TableEntityProcessorTest.kt
@@ -69,7 +69,7 @@
             assertThat(field.setter,
                 `is`(FieldSetter("rowId", "setRowId", intType, CallType.METHOD)))
             assertThat(field.getter,
-                `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD)))
+                `is`(FieldGetter("rowId", "getRowId", intType, CallType.METHOD, true)))
             assertThat(entity.primaryKey.fields, `is`(Fields(field)))
             assertThat(entity.shadowTableName, `is`("MyEntity_content"))
             assertThat(entity.ftsVersion, `is`(FtsVersion.FTS4))
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
index 9964afe..4458240 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/InsertOrUpsertShortcutMethodProcessorTest.kt
@@ -340,7 +340,7 @@
                 .isEqualTo(
                     ParameterizedTypeName.get(
                         ClassName.get("foo.bar", "MyClass.MyList"),
-                        CommonTypeNames.STRING, USER_TYPE_NAME
+                        CommonTypeNames.STRING.toJavaPoet(), USER_TYPE_NAME
                     ) as TypeName
                 )
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
index b45185b..2785605 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/PojoProcessorTest.kt
@@ -2100,7 +2100,8 @@
                     fieldName = "isbn",
                     jvmName = "getIsbn",
                     type = stringType,
-                    callType = CallType.SYNTHETIC_METHOD
+                    callType = CallType.SYNTHETIC_METHOD,
+                    isMutableField = true
                 )
             )
             Truth.assertThat(
@@ -2121,7 +2122,8 @@
                     fieldName = "isbn2",
                     jvmName = "getIsbn2",
                     type = stringType.makeNullable(),
-                    callType = CallType.SYNTHETIC_METHOD
+                    callType = CallType.SYNTHETIC_METHOD,
+                    isMutableField = true
                 )
             )
             Truth.assertThat(
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
index caedbe7..78038eb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/QueryMethodProcessorTest.kt
@@ -19,12 +19,13 @@
 import COMMON
 import androidx.room.Dao
 import androidx.room.Query
+import androidx.room.compiler.codegen.toJavaPoet
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
 import androidx.room.compiler.processing.util.runProcessorTest
-import androidx.room.ext.CommonTypeNames
+import androidx.room.ext.CommonTypeNames.LIST
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -332,7 +333,7 @@
         singleQueryMethod<ReadQueryMethod>(
             """
                 @Query("select * from User")
-                abstract public <T> ${CommonTypeNames.LIST}<T> foo(int x);
+                abstract public <T> ${LIST.toJavaPoet()}<T> foo(int x);
                 """
         ) { parsedQuery, invocation ->
             val expected: TypeName = ParameterizedTypeName.get(
@@ -369,7 +370,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable, User")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${LIST.toJavaPoet()}<Integer>>
                 getFactorialLiveData();
                 """
         ) { parsedQuery, _ ->
@@ -404,7 +405,7 @@
             """
                 @Query("WITH RECURSIVE tempTable(n, fact) AS (SELECT 0, 1 UNION ALL SELECT n+1,"
                 + " (n+1)*fact FROM tempTable WHERE n < 9) SELECT fact FROM tempTable")
-                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${CommonTypeNames.LIST}<Integer>>
+                abstract public ${LifecyclesTypeNames.LIVE_DATA}<${LIST.toJavaPoet()}<Integer>>
                 getFactorialLiveData();
                 """
         ) { _, invocation ->
@@ -872,7 +873,7 @@
             val pojoRowAdapter = listAdapter.rowAdapters.single() as PojoRowAdapter
             assertThat(pojoRowAdapter.relationCollectors.size, `is`(1))
             assertThat(
-                pojoRowAdapter.relationCollectors[0].relationTypeName,
+                pojoRowAdapter.relationCollectors[0].relationTypeName.toJavaPoet(),
                 `is`(
                     ParameterizedTypeName.get(
                         ClassName.get(ArrayList::class.java),
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
index 8d41129..7a4b1ae 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/processor/TableEntityProcessorTest.kt
@@ -73,7 +73,8 @@
                 )
             )
             assertThat(field.setter, `is`(FieldSetter("id", "setId", intType, CallType.METHOD)))
-            assertThat(field.getter, `is`(FieldGetter("id", "getId", intType, CallType.METHOD)))
+            assertThat(field.getter,
+                `is`(FieldGetter("id", "getId", intType, CallType.METHOD, true)))
             assertThat(entity.primaryKey.fields, `is`(Fields(field)))
         }
     }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
index 702663d..93a15df 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/BuiltInConverterFlagsTest.kt
@@ -60,6 +60,19 @@
     }
 
     @Test
+    fun byteBuffer_disabledInDb() {
+        compile(
+            dbAnnotation = createTypeConvertersCode(
+                byteBuffer = DISABLED
+            )
+        ) {
+            hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val blob: ByteBuffer")
+            hasError(CANNOT_FIND_CURSOR_READER, "val blob: ByteBuffer")
+            hasErrorCount(2)
+        }
+    }
+
+    @Test
     fun all_disabledInDb_enabledInDao_enabledInEntity() {
         compile(
             dbAnnotation = createTypeConvertersCode(
@@ -84,16 +97,19 @@
         compile(
             entityAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             )
         ) {
             hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val uuid: UUID")
             hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val myEnum: MyEnum")
+            hasError(CANNOT_FIND_COLUMN_TYPE_ADAPTER, "val blob: ByteBuffer")
             // even though it is enabled in dao or db, since pojo processing will visit the pojo,
-            // we'll still get erros for these because entity disabled them
+            // we'll still get errors for these because entity disabled them
             hasError(CANNOT_FIND_CURSOR_READER, "val uuid: UUID")
             hasError(CANNOT_FIND_CURSOR_READER, "val myEnum: MyEnum")
-            hasErrorCount(4)
+            hasError(CANNOT_FIND_CURSOR_READER, "val blob: ByteBuffer")
+            hasErrorCount(6)
         }
     }
 
@@ -102,15 +118,18 @@
         compile(
             dbAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             ),
             daoAnnotation = createTypeConvertersCode(
                 enums = DISABLED,
-                uuid = DISABLED
+                uuid = DISABLED,
+                byteBuffer = DISABLED
             ),
             entityAnnotation = createTypeConvertersCode(
                 enums = ENABLED,
-                uuid = ENABLED
+                uuid = ENABLED,
+                byteBuffer = ENABLED
             )
         ) {
             // success since we only fetch full objects.
@@ -184,7 +203,7 @@
         return Source.kotlin(
             "Foo.kt",
             """
-            import androidx.room.*
+            import androidx.room.*import java.nio.ByteBuffer
             import java.util.UUID
             enum class MyEnum {
                 VAL_1,
@@ -197,7 +216,8 @@
                 @PrimaryKey
                 val id:Int,
                 val uuid: UUID,
-                val myEnum: MyEnum
+                val myEnum: MyEnum,
+                val blob: ByteBuffer
             )
 
             $daoAnnotation
@@ -218,11 +238,13 @@
 
     private fun createTypeConvertersCode(
         enums: BuiltInTypeConverters.State? = null,
-        uuid: BuiltInTypeConverters.State? = null
+        uuid: BuiltInTypeConverters.State? = null,
+        byteBuffer: BuiltInTypeConverters.State? = null
     ): String {
         val builtIns = listOfNotNull(
             enums?.let { "enums = BuiltInTypeConverters.State.${enums.name}" },
             uuid?.let { "uuid = BuiltInTypeConverters.State.${uuid.name}" },
+            byteBuffer?.let { "byteBuffer = BuiltInTypeConverters.State.${byteBuffer.name}" },
         ).joinToString(",")
         return if (builtIns.isBlank()) {
             ""
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 84a98ce..45d6eab 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
@@ -25,6 +25,7 @@
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.ext.CollectionTypeNames
 import androidx.room.ext.GuavaUtilConcurrentTypeNames
 import androidx.room.ext.KotlinTypeNames
 import androidx.room.ext.LifecyclesTypeNames
@@ -306,6 +307,20 @@
     val ROOM_DATABASE_KTX by lazy {
         loadKotlinCode("common/input/RoomDatabaseExt.kt")
     }
+
+    val LONG_SPARSE_ARRAY by lazy {
+        loadJavaCode(
+            "common/input/collection/LongSparseArray.java",
+            CollectionTypeNames.LONG_SPARSE_ARRAY.canonicalName
+        )
+    }
+
+    val ARRAY_MAP by lazy {
+        loadJavaCode(
+            "common/input/collection/ArrayMap.java",
+            CollectionTypeNames.ARRAY_MAP.canonicalName
+        )
+    }
 }
 
 fun testCodeGenScope(): CodeGenScope {
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
index 2be73c3..73af03b 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/verifier/DatabaseVerifierTest.kt
@@ -435,7 +435,7 @@
     }
 
     private fun assignGetterSetter(f: Field, name: String, type: XType) {
-        f.getter = FieldGetter(f.name, name, type, CallType.FIELD)
+        f.getter = FieldGetter(f.name, name, type, CallType.FIELD, true)
         f.setter = FieldSetter(f.name, name, type, CallType.FIELD)
     }
 
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.kt
new file mode 100644
index 0000000..602825b
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/BaseDaoKotlinCodeGenTest.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.room.writer
+
+import androidx.room.DatabaseProcessingStep
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.processor.Context
+import java.io.File
+import loadTestSource
+import org.jetbrains.kotlin.config.JvmDefaultMode
+
+abstract class BaseDaoKotlinCodeGenTest {
+    protected fun getTestGoldenPath(testName: String): String {
+        return "kotlinCodeGen/$testName.kt"
+    }
+
+    protected fun runTest(
+        sources: List<Source>,
+        expectedFilePath: String,
+        compiledFiles: List<File> = emptyList(),
+        jvmDefaultMode: JvmDefaultMode = JvmDefaultMode.DEFAULT,
+        handler: (XTestInvocation) -> Unit = { }
+    ) {
+        runKspTest(
+            sources = sources,
+            classpath = compiledFiles,
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+            kotlincArguments = listOf("-Xjvm-default=${jvmDefaultMode.description}")
+        ) {
+            val databaseFqn = "androidx.room.Database"
+            DatabaseProcessingStep().process(
+                it.processingEnv,
+                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
+                it.roundEnv.isProcessingOver
+            )
+            it.assertCompilationResult {
+                this.generatedSource(
+                    loadTestSource(
+                        expectedFilePath,
+                        "MyDao_Impl"
+                    )
+                )
+                this.hasNoWarnings()
+            }
+            handler.invoke(it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
similarity index 74%
rename from room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
rename to room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index c58df04..3525dfcd 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -17,21 +17,15 @@
 package androidx.room.writer
 
 import COMMON
-import androidx.room.DatabaseProcessingStep
 import androidx.room.compiler.processing.util.Source
-import androidx.room.compiler.processing.util.XTestInvocation
-import androidx.room.compiler.processing.util.runKspTest
-import androidx.room.processor.Context
 import com.google.testing.junit.testparameterinjector.TestParameter
 import com.google.testing.junit.testparameterinjector.TestParameterInjector
-import loadTestSource
 import org.jetbrains.kotlin.config.JvmDefaultMode
 import org.junit.Test
 import org.junit.runner.RunWith
 
-// Dany's Kotlin codegen test playground (and tests too)
 @RunWith(TestParameterInjector::class)
-class KotlinCodeGenTest {
+class DaoKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
 
     val databaseSrc = Source.kotlin(
         "MyDatabase.kt",
@@ -67,7 +61,9 @@
                 @PrimaryKey
                 var pk: Int
             ) {
-                var variable: Long = 0
+                var variablePrimitive: Long = 0
+                var variableString: String = ""
+                var variableNullableString: String? = null
             }
             """.trimIndent()
         )
@@ -397,7 +393,6 @@
             "MyDao.kt",
             """
             import androidx.room.*
-            import java.util.UUID
 
             @Dao
             interface MyDao {
@@ -437,7 +432,6 @@
             "MyDao.kt",
             """
             import androidx.room.*
-            import java.util.UUID
 
             @Dao
             interface MyDao {
@@ -473,6 +467,143 @@
     }
 
     @Test
+    fun pojoRowAdapter_customTypeConverter_provided() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val foo: Foo,
+            )
+
+            data class Foo(val data: String)
+
+            @ProvidedTypeConverter
+            class FooConverter(val default: String) {
+                @TypeConverter
+                fun fromString(data: String?): Foo = Foo(data ?: default)
+                @TypeConverter
+                fun toString(foo: Foo): String = foo.data
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun pojoRowAdapter_customTypeConverter_composite() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooBarConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val bar: Bar,
+            )
+
+            data class Foo(val data: String)
+            data class Bar(val data: String)
+
+            object FooBarConverter {
+                @TypeConverter
+                fun fromString(data: String): Foo = Foo(data)
+                @TypeConverter
+                fun toString(foo: Foo): String = foo.data
+
+                @TypeConverter
+                fun fromFoo(foo: Foo): Bar = Bar(foo.data)
+                @TypeConverter
+                fun toFoo(bar: Bar): Foo = Foo(bar.data)
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun pojoRowAdapter_customTypeConverter_nullAware() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            @TypeConverters(FooBarConverter::class)
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val foo: Foo,
+                val bar: Bar
+            )
+
+            data class Foo(val data: String)
+            data class Bar(val data: String)
+
+            object FooBarConverter {
+                @TypeConverter
+                fun fromString(data: String?): Foo? = data?.let { Foo(it) }
+                @TypeConverter
+                fun toString(foo: Foo?): String? = foo?.data
+
+                @TypeConverter
+                fun fromFoo(foo: Foo): Bar = Bar(foo.data)
+                @TypeConverter
+                fun toFoo(bar: Bar): Foo = Foo(bar.data)
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
     fun coroutineResultBinder() {
         val testName = object {}.javaClass.enclosingMethod!!.name
         val src = Source.kotlin(
@@ -1009,37 +1140,178 @@
         )
     }
 
-    private fun getTestGoldenPath(testName: String): String {
-        return "kotlinCodeGen/$testName.kt"
+    @Test
+    fun abstractClassWithParam() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            abstract class MyDao(val db: RoomDatabase) {
+              @Query("SELECT * FROM MyEntity")
+              abstract fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
     }
 
-    private fun runTest(
-        sources: List<Source>,
-        expectedFilePath: String,
-        jvmDefaultMode: JvmDefaultMode = JvmDefaultMode.DEFAULT,
-        handler: (XTestInvocation) -> Unit = { }
-    ) {
-        runKspTest(
-            sources = sources,
-            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
-            kotlincArguments = listOf("-Xjvm-default=${jvmDefaultMode.description}")
-        ) {
-            val databaseFqn = "androidx.room.Database"
-            DatabaseProcessingStep().process(
-                it.processingEnv,
-                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
-                it.roundEnv.isProcessingOver
-            )
-            it.assertCompilationResult {
-                this.generatedSource(
-                    loadTestSource(
-                        expectedFilePath,
-                        "MyDao_Impl"
-                    )
-                )
-                this.hasNoWarnings()
+    @Test
+    fun queryResultAdapter_map() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [Artist::class, Song::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
             }
-            handler.invoke(it)
-        }
+
+            @Dao
+            interface MyDao {
+                @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+                fun getSongsWithArtist(): Map<Song, Artist>
+
+                @Query("SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId")
+                fun getSongsWithNullableArtist(): Map<Song, Artist?>
+
+                @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+                fun getArtistWithSongs(): Map<Artist, List<Song>>
+
+                @MapInfo(valueColumn = "songCount")
+                @Query(
+                    "SELECT Artist.*, COUNT(songId) as songCount " +
+                    "FROM Artist JOIN Song ON Artist.artistId = Song.artistKey " +
+                    "GROUP BY artistId"
+                )
+                fun getArtistSongCount(): Map<Artist, Int>
+
+                @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
+                @MapInfo(valueColumn = "songId")
+                @Query("SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey")
+                fun getArtistWithSongIds(): Map<Artist, List<String>>
+            }
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: String
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: String,
+                val artistKey: String
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun queryResultAdapter_map_ambiguousIndexAdapter() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [User::class, Comment::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            interface MyDao {
+                @Query("SELECT * FROM User JOIN Comment ON User.id = Comment.userId")
+                fun getUserCommentMap(): Map<User, List<Comment>>
+
+                @Query(
+                    "SELECT User.id, name, Comment.id, userId, text " +
+                    "FROM User JOIN Comment ON User.id = Comment.userId"
+                )
+                fun getUserCommentMapWithoutStarProjection(): Map<User, List<Comment>>
+
+                @SkipQueryVerification
+                @Query("SELECT * FROM User JOIN Comment ON User.id = Comment.userId")
+                fun getUserCommentMapWithoutQueryVerification(): Map<User, List<Comment>>
+            }
+
+            @Entity
+            data class User(
+                @PrimaryKey val id: Int,
+                val name: String,
+            )
+
+            @Entity
+            data class Comment(
+                @PrimaryKey val id: Int,
+                val userId: Int,
+                val text: String,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun entityRowAdapter() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+
+              @SkipQueryVerification // To make Room use EntityRowAdapter
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+
+              @SkipQueryVerification // To make Room use EntityRowAdapter
+              @Insert
+              fun addEntity(item: MyEntity)
+            }
+
+            @Entity
+            class MyEntity(
+                @PrimaryKey
+                val valuePrimitive: Long,
+                val valueBoolean: Boolean,
+                val valueString: String,
+                val valueNullableString: String?
+            ) {
+                var variablePrimitive: Long = 0
+                var variableNullableBoolean: Boolean? = null
+                var variableString: String = ""
+                var variableNullableString: String? = null
+            }
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
     }
 }
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
new file mode 100644
index 0000000..6e12f2c
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoRelationshipKotlinCodeGenTest.kt
@@ -0,0 +1,460 @@
+/*
+ * 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.room.writer
+
+import COMMON
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compileFiles
+import org.junit.Test
+
+class DaoRelationshipKotlinCodeGenTest : BaseDaoKotlinCodeGenTest() {
+
+    val databaseSrc = Source.kotlin(
+        "MyDatabase.kt",
+        """
+        import androidx.room.*
+
+        @Database(
+            entities = [
+                Artist::class,
+                Song::class,
+                Playlist::class,
+                PlaylistSongXRef::class
+            ],
+            version = 1,
+            exportSchema = false
+        )
+        abstract class MyDatabase : RoomDatabase() {
+          abstract fun getDao(): MyDao
+        }
+        """.trimIndent()
+    )
+
+    @Test
+    fun relations() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_nullable() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist?
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long?
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_longSparseArray() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            compiledFiles = compileFiles(listOf(COMMON.LONG_SPARSE_ARRAY)),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_arrayMap() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            interface MyDao {
+                // 1 to 1
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+
+                // 1 to many
+                @Query("SELECT * FROM Artist")
+                fun getArtistAndSongs(): ArtistAndSongs
+
+                // many to many
+                @Query("SELECT * FROM Playlist")
+                fun getPlaylistAndSongs(): PlaylistAndSongs
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            data class ArtistAndSongs(
+                @Embedded
+                val artist: Artist,
+                @Relation(parentColumn = "artistId", entityColumn = "artistKey")
+                val songs: List<Song>
+            )
+
+            data class PlaylistAndSongs(
+                @Embedded
+                val playlist: Playlist,
+                @Relation(
+                    parentColumn = "playlistId",
+                    entityColumn = "songId",
+                    associateBy = Junction(
+                        value = PlaylistSongXRef::class,
+                        parentColumn = "playlistKey",
+                        entityColumn = "songKey",
+                    )
+                )
+                val songs: List<Song>
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: Long
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: Long
+            )
+
+            @Entity
+            data class Playlist(
+                @PrimaryKey
+                val playlistId: Long,
+            )
+
+            @Entity(primaryKeys = ["playlistKey", "songKey"])
+            data class PlaylistSongXRef(
+                val playlistKey: Long,
+                val songKey: Long,
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, databaseSrc),
+            compiledFiles = compileFiles(listOf(COMMON.ARRAY_MAP)),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun relations_byteBufferKey() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Database(
+                entities = [Artist::class, Song::class],
+                version = 1,
+                exportSchema = false
+            )
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            @Suppress(
+                RoomWarnings.RELATION_QUERY_WITHOUT_TRANSACTION,
+                RoomWarnings.MISSING_INDEX_ON_JUNCTION
+            )
+            // To validate ByteBuffer converter is forced
+            @TypeConverters(
+                builtInTypeConverters = BuiltInTypeConverters(
+                    byteBuffer = BuiltInTypeConverters.State.DISABLED
+                )
+            )
+            interface MyDao {
+                @Query("SELECT * FROM Song")
+                fun getSongsWithArtist(): SongWithArtist
+            }
+
+            data class SongWithArtist(
+                @Embedded
+                val song: Song,
+                @Relation(parentColumn = "artistKey", entityColumn = "artistId")
+                val artist: Artist
+            )
+
+            @Entity
+            data class Artist(
+                @PrimaryKey
+                val artistId: ByteArray
+            )
+
+            @Entity
+            data class Song(
+                @PrimaryKey
+                val songId: Long,
+                val artistKey: ByteArray
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
new file mode 100644
index 0000000..5a51e79
--- /dev/null
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DatabaseKotlinCodeGenTest.kt
@@ -0,0 +1,191 @@
+/*
+ * 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.room.writer
+
+import androidx.room.DatabaseProcessingStep
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.processor.Context
+import loadTestSource
+import org.junit.Test
+
+class DatabaseKotlinCodeGenTest {
+
+    @Test
+    fun database_simple() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [MyEntity::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract fun getDao(): MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun database_propertyDao() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [MyEntity::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+              abstract val dao: MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                var pk: Int
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    @Test
+    fun database_withFtsAndView() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val src = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(
+                entities = [
+                    MyParentEntity::class,
+                    MyEntity::class,
+                    MyFtsEntity::class,
+                ],
+                views = [ MyView::class ],
+                version = 1,
+                exportSchema = false
+            )
+            abstract class MyDatabase : RoomDatabase() {
+              abstract val dao: MyDao
+            }
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun getEntity(): MyEntity
+            }
+
+            @Entity
+            data class MyParentEntity(@PrimaryKey val parentKey: Long)
+
+            @Entity(
+                foreignKeys = [
+                    ForeignKey(
+                        entity = MyParentEntity::class,
+                        parentColumns = ["parentKey"],
+                        childColumns = ["indexedCol"],
+                        onDelete = ForeignKey.CASCADE
+                    )
+                ],
+                indices = [Index("indexedCol")]
+            )
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val indexedCol: String
+            )
+
+            @Fts4
+            @Entity
+            data class MyFtsEntity(
+                @PrimaryKey
+                @ColumnInfo(name = "rowid")
+                val pk: Int,
+                val text: String
+            )
+
+            @DatabaseView("SELECT text FROM MyFtsEntity")
+            data class MyView(val text: String)
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
+    private fun getTestGoldenPath(testName: String): String {
+        return "kotlinCodeGen/$testName.kt"
+    }
+
+    private fun runTest(
+        sources: List<Source>,
+        expectedFilePath: String,
+        handler: (XTestInvocation) -> Unit = { }
+    ) {
+        runKspTest(
+            sources = sources,
+            options = mapOf(Context.BooleanProcessorOptions.GENERATE_KOTLIN.argName to "true"),
+        ) {
+            val databaseFqn = "androidx.room.Database"
+            DatabaseProcessingStep().process(
+                it.processingEnv,
+                mapOf(databaseFqn to it.roundEnv.getElementsAnnotatedWith(databaseFqn)),
+                it.roundEnv.isProcessingOver
+            )
+            it.assertCompilationResult {
+                this.generatedSource(
+                    loadTestSource(
+                        expectedFilePath,
+                        "MyDatabase_Impl"
+                    )
+                )
+                this.hasNoWarnings()
+            }
+            handler.invoke(it)
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java b/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java
new file mode 100644
index 0000000..2b7666f
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/collection/ArrayMap.java
@@ -0,0 +1,111 @@
+package androidx.collection;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+public class ArrayMap<K, V> implements Map<K, V> {
+    public ArrayMap() {
+
+    }
+
+    public ArrayMap(int capacity) {
+
+    }
+
+    @Override
+    public void putAll(@NonNull Map<? extends K, ? extends V> map) {
+
+    }
+
+    @NonNull
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public Set<K> keySet() {
+        return null;
+    }
+
+    @NonNull
+    @Override
+    public Collection<V> values() {
+        return null;
+    }
+
+    @Override
+    public void clear() {
+
+    }
+
+    @Override
+    public boolean containsKey(@Nullable Object key) {
+        return false;
+    }
+
+    @Override
+    public boolean containsValue(Object value) {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V get(Object key) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V getOrDefault(Object key, V defaultValue) {
+        return null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V put(K key, V value) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V putIfAbsent(K key, V value) {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public V remove(Object key) {
+        return null;
+    }
+
+    @Override
+    public boolean remove(Object key, Object value) {
+        return false;
+    }
+
+    @Nullable
+    @Override
+    public V replace(K key, V value) {
+        return null;
+    }
+
+    @Override
+    public boolean replace(K key, V oldValue, V newValue) {
+        return false;
+    }
+
+    @Override
+    public int size() {
+        return 0;
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java b/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java
new file mode 100644
index 0000000..3e48191
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/common/input/collection/LongSparseArray.java
@@ -0,0 +1,52 @@
+package androidx.collection;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.NonNull;
+
+public class LongSparseArray<E> {
+
+    public LongSparseArray() {
+
+    }
+
+    public LongSparseArray(int initialCapacity) {
+
+    }
+
+    @Nullable
+    public E get(long key) {
+        return null;
+    }
+
+    public void put(long key, E value) {
+
+    }
+
+    public void putAll(@NonNull LongSparseArray<? extends E> other) {
+
+    }
+
+    public int size() {
+        return 0;
+    }
+
+    public boolean isEmpty() {
+        return false;
+    }
+
+    public long keyAt(int index) {
+        return 0;
+    }
+
+    public E valueAt(int index) {
+        return null;
+    }
+
+    public boolean containsKey(long key) {
+        return false;
+    }
+
+    public void clear() {
+
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
index c4529bd..f56e512 100644
--- a/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
+++ b/room/room-compiler/src/test/test-data/databasewriter/output/ComplexDatabase.java
@@ -3,26 +3,20 @@
 import androidx.annotation.NonNull;
 import androidx.room.DatabaseConfiguration;
 import androidx.room.InvalidationTracker;
+import androidx.room.RoomDatabase;
 import androidx.room.RoomOpenHelper;
-import androidx.room.RoomOpenHelper.Delegate;
-import androidx.room.RoomOpenHelper.ValidationResult;
 import androidx.room.migration.AutoMigrationSpec;
 import androidx.room.migration.Migration;
 import androidx.room.util.DBUtil;
 import androidx.room.util.TableInfo;
-import androidx.room.util.TableInfo.Column;
-import androidx.room.util.TableInfo.ForeignKey;
-import androidx.room.util.TableInfo.Index;
 import androidx.room.util.ViewInfo;
 import androidx.sqlite.db.SupportSQLiteDatabase;
 import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.SupportSQLiteOpenHelper.Callback;
-import androidx.sqlite.db.SupportSQLiteOpenHelper.Configuration;
 import java.lang.Class;
 import java.lang.Override;
 import java.lang.String;
 import java.lang.SuppressWarnings;
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -36,62 +30,68 @@
     private volatile ComplexDao _complexDao;
 
     @Override
-    protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
-        final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1923) {
+    @NonNull
+    protected SupportSQLiteOpenHelper createOpenHelper(@NonNull final DatabaseConfiguration config) {
+        final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(config, new RoomOpenHelper.Delegate(1923) {
             @Override
-            public void createAllTables(SupportSQLiteDatabase _db) {
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `Child1` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS `Child2` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
-                _db.execSQL("CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
-                _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
-                _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12b646c55443feeefb567521e2bece85')");
+            public void createAllTables(@NonNull final SupportSQLiteDatabase db) {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER NOT NULL, `name` TEXT, `lastName` TEXT, `ageColumn` INTEGER NOT NULL, PRIMARY KEY(`uid`))");
+                db.execSQL("CREATE TABLE IF NOT EXISTS `Child1` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
+                db.execSQL("CREATE TABLE IF NOT EXISTS `Child2` (`id` INTEGER NOT NULL, `name` TEXT, `serial` INTEGER, `code` TEXT, PRIMARY KEY(`id`))");
+                db.execSQL("CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '12b646c55443feeefb567521e2bece85')");
             }
 
             @Override
-            public void dropAllTables(SupportSQLiteDatabase _db) {
-                _db.execSQL("DROP TABLE IF EXISTS `User`");
-                _db.execSQL("DROP TABLE IF EXISTS `Child1`");
-                _db.execSQL("DROP TABLE IF EXISTS `Child2`");
-                _db.execSQL("DROP VIEW IF EXISTS `UserSummary`");
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onDestructiveMigration(_db);
+            public void dropAllTables(@NonNull final SupportSQLiteDatabase db) {
+                db.execSQL("DROP TABLE IF EXISTS `User`");
+                db.execSQL("DROP TABLE IF EXISTS `Child1`");
+                db.execSQL("DROP TABLE IF EXISTS `Child2`");
+                db.execSQL("DROP VIEW IF EXISTS `UserSummary`");
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onDestructiveMigration(db);
                     }
                 }
             }
 
             @Override
-            public void onCreate(SupportSQLiteDatabase _db) {
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onCreate(_db);
+            public void onCreate(@NonNull final SupportSQLiteDatabase db) {
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onCreate(db);
                     }
                 }
             }
 
             @Override
-            public void onOpen(SupportSQLiteDatabase _db) {
-                mDatabase = _db;
-                internalInitInvalidationTracker(_db);
-                if (mCallbacks != null) {
-                    for (int _i = 0, _size = mCallbacks.size(); _i < _size; _i++) {
-                        mCallbacks.get(_i).onOpen(_db);
+            public void onOpen(@NonNull final SupportSQLiteDatabase db) {
+                mDatabase = db;
+                internalInitInvalidationTracker(db);
+                final List<? extends RoomDatabase.Callback> _callbacks = mCallbacks;
+                if (_callbacks != null) {
+                    for (RoomDatabase.Callback _callback : _callbacks) {
+                        _callback.onOpen(db);
                     }
                 }
             }
 
             @Override
-            public void onPreMigrate(SupportSQLiteDatabase _db) {
-                DBUtil.dropFtsSyncTriggers(_db);
+            public void onPreMigrate(@NonNull final SupportSQLiteDatabase db) {
+                DBUtil.dropFtsSyncTriggers(db);
             }
 
             @Override
-            public void onPostMigrate(SupportSQLiteDatabase _db) {
+            public void onPostMigrate(@NonNull final SupportSQLiteDatabase db) {
             }
 
             @Override
-            public RoomOpenHelper.ValidationResult onValidateSchema(SupportSQLiteDatabase _db) {
+            @NonNull
+            public RoomOpenHelper.ValidationResult onValidateSchema(
+                    @NonNull final SupportSQLiteDatabase db) {
                 final HashMap<String, TableInfo.Column> _columnsUser = new HashMap<String, TableInfo.Column>(4);
                 _columnsUser.put("uid", new TableInfo.Column("uid", "INTEGER", true, 1, null, TableInfo.CREATED_FROM_ENTITY));
                 _columnsUser.put("name", new TableInfo.Column("name", "TEXT", false, 0, null, TableInfo.CREATED_FROM_ENTITY));
@@ -100,8 +100,8 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysUser = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesUser = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoUser = new TableInfo("User", _columnsUser, _foreignKeysUser, _indicesUser);
-                final TableInfo _existingUser = TableInfo.read(_db, "User");
-                if (! _infoUser.equals(_existingUser)) {
+                final TableInfo _existingUser = TableInfo.read(db, "User");
+                if (!_infoUser.equals(_existingUser)) {
                     return new RoomOpenHelper.ValidationResult(false, "User(foo.bar.User).\n"
                             + " Expected:\n" + _infoUser + "\n"
                             + " Found:\n" + _existingUser);
@@ -114,8 +114,8 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysChild1 = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesChild1 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild1 = new TableInfo("Child1", _columnsChild1, _foreignKeysChild1, _indicesChild1);
-                final TableInfo _existingChild1 = TableInfo.read(_db, "Child1");
-                if (! _infoChild1.equals(_existingChild1)) {
+                final TableInfo _existingChild1 = TableInfo.read(db, "Child1");
+                if (!_infoChild1.equals(_existingChild1)) {
                     return new RoomOpenHelper.ValidationResult(false, "Child1(foo.bar.Child1).\n"
                             + " Expected:\n" + _infoChild1 + "\n"
                             + " Found:\n" + _existingChild1);
@@ -128,15 +128,15 @@
                 final HashSet<TableInfo.ForeignKey> _foreignKeysChild2 = new HashSet<TableInfo.ForeignKey>(0);
                 final HashSet<TableInfo.Index> _indicesChild2 = new HashSet<TableInfo.Index>(0);
                 final TableInfo _infoChild2 = new TableInfo("Child2", _columnsChild2, _foreignKeysChild2, _indicesChild2);
-                final TableInfo _existingChild2 = TableInfo.read(_db, "Child2");
-                if (! _infoChild2.equals(_existingChild2)) {
+                final TableInfo _existingChild2 = TableInfo.read(db, "Child2");
+                if (!_infoChild2.equals(_existingChild2)) {
                     return new RoomOpenHelper.ValidationResult(false, "Child2(foo.bar.Child2).\n"
                             + " Expected:\n" + _infoChild2 + "\n"
                             + " Found:\n" + _existingChild2);
                 }
                 final ViewInfo _infoUserSummary = new ViewInfo("UserSummary", "CREATE VIEW `UserSummary` AS SELECT uid, name FROM User");
-                final ViewInfo _existingUserSummary = ViewInfo.read(_db, "UserSummary");
-                if (! _infoUserSummary.equals(_existingUserSummary)) {
+                final ViewInfo _existingUserSummary = ViewInfo.read(db, "UserSummary");
+                if (!_infoUserSummary.equals(_existingUserSummary)) {
                     return new RoomOpenHelper.ValidationResult(false, "UserSummary(foo.bar.UserSummary).\n"
                             + " Expected:\n" + _infoUserSummary + "\n"
                             + " Found:\n" + _existingUserSummary);
@@ -144,19 +144,17 @@
                 return new RoomOpenHelper.ValidationResult(true, null);
             }
         }, "12b646c55443feeefb567521e2bece85", "2f1dbf49584f5d6c91cb44f8a6ecfee2");
-        final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
-                .name(configuration.name)
-                .callback(_openCallback)
-                .build();
-        final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
+        final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build();
+        final SupportSQLiteOpenHelper _helper = config.sqliteOpenHelperFactory.create(_sqliteConfig);
         return _helper;
     }
 
     @Override
+    @NonNull
     protected InvalidationTracker createInvalidationTracker() {
         final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
-        HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
-        HashSet<String> _tables = new HashSet<String>(1);
+        final HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(1);
+        final HashSet<String> _tables = new HashSet<String>(1);
         _tables.add("User");
         _viewTables.put("usersummary", _tables);
         return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User","Child1","Child2");
@@ -182,6 +180,7 @@
     }
 
     @Override
+    @NonNull
     protected Map<Class<?>, List<Class<?>>> getRequiredTypeConverters() {
         final HashMap<Class<?>, List<Class<?>>> _typeConvertersMap = new HashMap<Class<?>, List<Class<?>>>();
         _typeConvertersMap.put(ComplexDao.class, ComplexDao_Impl.getRequiredConverters());
@@ -189,15 +188,18 @@
     }
 
     @Override
+    @NonNull
     public Set<Class<? extends AutoMigrationSpec>> getRequiredAutoMigrationSpecs() {
         final HashSet<Class<? extends AutoMigrationSpec>> _autoMigrationSpecsSet = new HashSet<Class<? extends AutoMigrationSpec>>();
         return _autoMigrationSpecsSet;
     }
 
     @Override
+    @NonNull
     public List<Migration> getAutoMigrations(
-            @NonNull Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> autoMigrationSpecsMap) {
-        return Arrays.asList();
+            @NonNull final Map<Class<? extends AutoMigrationSpec>, AutoMigrationSpec> autoMigrationSpecs) {
+        final List<Migration> _autoMigrations = new ArrayList<Migration>();
+        return _autoMigrations;
     }
 
     @Override
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
new file mode 100644
index 0000000..52f9323
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/abstractClassWithParam.kt
@@ -0,0 +1,51 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao(__db) {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                _result = MyEntity(_tmpPk)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    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/arrayParameterAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
index 2d6cc5f..83e8aee 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/arrayParameterAdapter.kt
@@ -19,10 +19,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
index 11211aa..ebeb1fe 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/basicParameterAdapter_string.kt
@@ -14,10 +14,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
index 2a605b7..c2c2d0d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/collectionParameterAdapter_string.kt
@@ -18,10 +18,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
index a23b4769..629b4d5 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/coroutineResultBinder.kt
@@ -18,10 +18,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt
new file mode 100644
index 0000000..75cb1b2
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_propertyDao.kt
@@ -0,0 +1,150 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    public override val dao: MyDao
+        get() = _myDao.value
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '195d7974660177325bd1a32d2c7b8b8c')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        try {
+            super.beginTransaction()
+            _db.execSQL("DELETE FROM `MyEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
new file mode 100644
index 0000000..e22f870
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_simple.kt
@@ -0,0 +1,149 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, PRIMARY KEY(`pk`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '195d7974660177325bd1a32d2c7b8b8c')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "195d7974660177325bd1a32d2c7b8b8c", "7458a901120796c5bbc554e2fefd262f")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(0)
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(0)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables, "MyEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        try {
+            super.beginTransaction()
+            _db.execSQL("DELETE FROM `MyEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+
+    public override fun getDao(): MyDao = _myDao.value
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
new file mode 100644
index 0000000..fa8e301
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/database_withFtsAndView.kt
@@ -0,0 +1,230 @@
+import androidx.room.DatabaseConfiguration
+import androidx.room.InvalidationTracker
+import androidx.room.RoomDatabase
+import androidx.room.RoomOpenHelper
+import androidx.room.migration.AutoMigrationSpec
+import androidx.room.migration.Migration
+import androidx.room.util.FtsTableInfo
+import androidx.room.util.TableInfo
+import androidx.room.util.TableInfo.Companion.read
+import androidx.room.util.ViewInfo
+import androidx.room.util.dropFtsSyncTriggers
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import java.lang.Class
+import java.util.ArrayList
+import java.util.HashMap
+import java.util.HashSet
+import javax.`annotation`.processing.Generated
+import kotlin.Any
+import kotlin.Boolean
+import kotlin.Lazy
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.Set
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDatabase_Impl : MyDatabase() {
+    private val _myDao: Lazy<MyDao> = lazy { MyDao_Impl(this) }
+
+    public override val dao: MyDao
+        get() = _myDao.value
+
+    protected override fun createOpenHelper(config: DatabaseConfiguration): SupportSQLiteOpenHelper {
+        val _openCallback: SupportSQLiteOpenHelper.Callback = RoomOpenHelper(config, object :
+            RoomOpenHelper.Delegate(1) {
+            public override fun createAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyParentEntity` (`parentKey` INTEGER NOT NULL, PRIMARY KEY(`parentKey`))")
+                db.execSQL("CREATE TABLE IF NOT EXISTS `MyEntity` (`pk` INTEGER NOT NULL, `indexedCol` TEXT NOT NULL, PRIMARY KEY(`pk`), FOREIGN KEY(`indexedCol`) REFERENCES `MyParentEntity`(`parentKey`) ON UPDATE NO ACTION ON DELETE CASCADE )")
+                db.execSQL("CREATE INDEX IF NOT EXISTS `index_MyEntity_indexedCol` ON `MyEntity` (`indexedCol`)")
+                db.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
+                db.execSQL("CREATE VIEW `MyView` AS SELECT text FROM MyFtsEntity")
+                db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)")
+                db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '89ba16fb8b062b50acf0eb06c853efcb')")
+            }
+
+            public override fun dropAllTables(db: SupportSQLiteDatabase): Unit {
+                db.execSQL("DROP TABLE IF EXISTS `MyParentEntity`")
+                db.execSQL("DROP TABLE IF EXISTS `MyEntity`")
+                db.execSQL("DROP TABLE IF EXISTS `MyFtsEntity`")
+                db.execSQL("DROP VIEW IF EXISTS `MyView`")
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onDestructiveMigration(db)
+                    }
+                }
+            }
+
+            public override fun onCreate(db: SupportSQLiteDatabase): Unit {
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onCreate(db)
+                    }
+                }
+            }
+
+            public override fun onOpen(db: SupportSQLiteDatabase): Unit {
+                mDatabase = db
+                db.execSQL("PRAGMA foreign_keys = ON")
+                internalInitInvalidationTracker(db)
+                val _callbacks: List<RoomDatabase.Callback>? = mCallbacks
+                if (_callbacks != null) {
+                    for (_callback: RoomDatabase.Callback in _callbacks) {
+                        _callback.onOpen(db)
+                    }
+                }
+            }
+
+            public override fun onPreMigrate(db: SupportSQLiteDatabase): Unit {
+                dropFtsSyncTriggers(db)
+            }
+
+            public override fun onPostMigrate(db: SupportSQLiteDatabase): Unit {
+            }
+
+            public override fun onValidateSchema(db: SupportSQLiteDatabase):
+                RoomOpenHelper.ValidationResult {
+                val _columnsMyParentEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(1)
+                _columnsMyParentEntity.put("parentKey", TableInfo.Column("parentKey", "INTEGER", true, 1,
+                    null, TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyParentEntity: HashSet<TableInfo.ForeignKey> =
+                    HashSet<TableInfo.ForeignKey>(0)
+                val _indicesMyParentEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(0)
+                val _infoMyParentEntity: TableInfo = TableInfo("MyParentEntity", _columnsMyParentEntity,
+                    _foreignKeysMyParentEntity, _indicesMyParentEntity)
+                val _existingMyParentEntity: TableInfo = read(db, "MyParentEntity")
+                if (!_infoMyParentEntity.equals(_existingMyParentEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyParentEntity(MyParentEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyParentEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyParentEntity)
+                }
+                val _columnsMyEntity: HashMap<String, TableInfo.Column> =
+                    HashMap<String, TableInfo.Column>(2)
+                _columnsMyEntity.put("pk", TableInfo.Column("pk", "INTEGER", true, 1, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                _columnsMyEntity.put("indexedCol", TableInfo.Column("indexedCol", "TEXT", true, 0, null,
+                    TableInfo.CREATED_FROM_ENTITY))
+                val _foreignKeysMyEntity: HashSet<TableInfo.ForeignKey> = HashSet<TableInfo.ForeignKey>(1)
+                _foreignKeysMyEntity.add(TableInfo.ForeignKey("MyParentEntity", "CASCADE", "NO ACTION",
+                    listOf("indexedCol"), listOf("parentKey")))
+                val _indicesMyEntity: HashSet<TableInfo.Index> = HashSet<TableInfo.Index>(1)
+                _indicesMyEntity.add(TableInfo.Index("index_MyEntity_indexedCol", false,
+                    listOf("indexedCol"), listOf("ASC")))
+                val _infoMyEntity: TableInfo = TableInfo("MyEntity", _columnsMyEntity, _foreignKeysMyEntity,
+                    _indicesMyEntity)
+                val _existingMyEntity: TableInfo = read(db, "MyEntity")
+                if (!_infoMyEntity.equals(_existingMyEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyEntity(MyEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyEntity)
+                }
+                val _columnsMyFtsEntity: HashSet<String> = HashSet<String>(2)
+                _columnsMyFtsEntity.add("text")
+                val _infoMyFtsEntity: FtsTableInfo = FtsTableInfo("MyFtsEntity", _columnsMyFtsEntity,
+                    "CREATE VIRTUAL TABLE IF NOT EXISTS `MyFtsEntity` USING FTS4(`text` TEXT NOT NULL)")
+                val _existingMyFtsEntity: FtsTableInfo = FtsTableInfo.Companion.read(db, "MyFtsEntity")
+                if (!_infoMyFtsEntity.equals(_existingMyFtsEntity)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyFtsEntity(MyFtsEntity).
+                  | Expected:
+                  |""".trimMargin() + _infoMyFtsEntity + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyFtsEntity)
+                }
+                val _infoMyView: ViewInfo = ViewInfo("MyView",
+                    "CREATE VIEW `MyView` AS SELECT text FROM MyFtsEntity")
+                val _existingMyView: ViewInfo = ViewInfo.Companion.read(db, "MyView")
+                if (!_infoMyView.equals(_existingMyView)) {
+                    return RoomOpenHelper.ValidationResult(false, """
+                  |MyView(MyView).
+                  | Expected:
+                  |""".trimMargin() + _infoMyView + """
+                  |
+                  | Found:
+                  |""".trimMargin() + _existingMyView)
+                }
+                return RoomOpenHelper.ValidationResult(true, null)
+            }
+        }, "89ba16fb8b062b50acf0eb06c853efcb", "8a71a68e07bdd62aa8c8324d870cf804")
+        val _sqliteConfig: SupportSQLiteOpenHelper.Configuration =
+            SupportSQLiteOpenHelper.Configuration.builder(config.context).name(config.name).callback(_openCallback).build()
+        val _helper: SupportSQLiteOpenHelper = config.sqliteOpenHelperFactory.create(_sqliteConfig)
+        return _helper
+    }
+
+    protected override fun createInvalidationTracker(): InvalidationTracker {
+        val _shadowTablesMap: HashMap<String, String> = HashMap<String, String>(1)
+        _shadowTablesMap.put("MyFtsEntity", "MyFtsEntity_content")
+        val _viewTables: HashMap<String, Set<String>> = HashMap<String, Set<String>>(1)
+        val _tables: HashSet<String> = HashSet<String>(1)
+        _tables.add("MyFtsEntity")
+        _viewTables.put("myview", _tables)
+        return InvalidationTracker(this, _shadowTablesMap, _viewTables,
+            "MyParentEntity","MyEntity","MyFtsEntity")
+    }
+
+    public override fun clearAllTables(): Unit {
+        super.assertNotMainThread()
+        val _db: SupportSQLiteDatabase = super.openHelper.writableDatabase
+        val _supportsDeferForeignKeys: Boolean = android.os.Build.VERSION.SDK_INT >=
+            android.os.Build.VERSION_CODES.LOLLIPOP
+        try {
+            if (!_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA foreign_keys = FALSE")
+            }
+            super.beginTransaction()
+            if (_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA defer_foreign_keys = TRUE")
+            }
+            _db.execSQL("DELETE FROM `MyParentEntity`")
+            _db.execSQL("DELETE FROM `MyEntity`")
+            _db.execSQL("DELETE FROM `MyFtsEntity`")
+            super.setTransactionSuccessful()
+        } finally {
+            super.endTransaction()
+            if (!_supportsDeferForeignKeys) {
+                _db.execSQL("PRAGMA foreign_keys = TRUE")
+            }
+            _db.query("PRAGMA wal_checkpoint(FULL)").close()
+            if (!_db.inTransaction()) {
+                _db.execSQL("VACUUM")
+            }
+        }
+    }
+
+    protected override fun getRequiredTypeConverters(): Map<Class<out Any>, List<Class<out Any>>> {
+        val _typeConvertersMap: HashMap<Class<out Any>, List<Class<out Any>>> =
+            HashMap<Class<out Any>, List<Class<out Any>>>()
+        _typeConvertersMap.put(MyDao::class.java, MyDao_Impl.getRequiredConverters())
+        return _typeConvertersMap
+    }
+
+    public override fun getRequiredAutoMigrationSpecs(): Set<Class<out AutoMigrationSpec>> {
+        val _autoMigrationSpecsSet: HashSet<Class<out AutoMigrationSpec>> =
+            HashSet<Class<out AutoMigrationSpec>>()
+        return _autoMigrationSpecsSet
+    }
+
+    public override
+    fun getAutoMigrations(autoMigrationSpecs: Map<Class<out AutoMigrationSpec>, AutoMigrationSpec>):
+        List<Migration> {
+        val _autoMigrations: List<Migration> = ArrayList<Migration>()
+        return _autoMigrations
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
index 74c1fe5..da53b2a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_boxedPrimitiveBridge.kt
@@ -17,12 +17,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __preparedStmtOfInsertEntity: SharedSQLiteStatement
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__preparedStmtOfInsertEntity = object : SharedSQLiteStatement(__db) {
             public override fun createQuery(): String {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
index 4fdc4e8..3e6fa5a 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/delegatingFunctions_defaultImplBridge.kt
@@ -15,10 +15,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
index da2116d4..376ee20 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/deleteOrUpdateMethodAdapter.kt
@@ -12,14 +12,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __deletionAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
 
     private val __updateAdapterOfMyEntity: EntityDeletionOrUpdateAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__deletionAdapterOfMyEntity = object : EntityDeletionOrUpdateAdapter<MyEntity>(__db) {
             public override fun createQuery(): String = "DELETE FROM `MyEntity` WHERE `pk` = ?"
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt
new file mode 100644
index 0000000..a14eefe
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/entityRowAdapter.kt
@@ -0,0 +1,164 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndex
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Boolean
+import kotlin.Int
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`valuePrimitive`,`valueBoolean`,`valueString`,`valueNullableString`,`variablePrimitive`,`variableNullableBoolean`,`variableString`,`variableNullableString`) VALUES (?,?,?,?,?,?,?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.valuePrimitive)
+                val _tmp: Int = if (entity.valueBoolean) 1 else 0
+                statement.bindLong(2, _tmp.toLong())
+                statement.bindString(3, entity.valueString)
+                if (entity.valueNullableString == null) {
+                    statement.bindNull(4)
+                } else {
+                    statement.bindString(4, entity.valueNullableString)
+                }
+                statement.bindLong(5, entity.variablePrimitive)
+                val _tmpVariableNullableBoolean: Boolean? = entity.variableNullableBoolean
+                val _tmp_1: Int? = _tmpVariableNullableBoolean?.let { if (it) 1 else 0 }
+                if (_tmp_1 == null) {
+                    statement.bindNull(6)
+                } else {
+                    statement.bindLong(6, _tmp_1.toLong())
+                }
+                statement.bindString(7, entity.variableString)
+                val _tmpVariableNullableString: String? = entity.variableNullableString
+                if (_tmpVariableNullableString == null) {
+                    statement.bindNull(8)
+                } else {
+                    statement.bindString(8, _tmpVariableNullableString)
+                }
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                _result = __entityCursorConverter_MyEntity(_cursor)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __entityCursorConverter_MyEntity(cursor: Cursor): MyEntity {
+        val _entity: MyEntity
+        val _cursorIndexOfValuePrimitive: Int = getColumnIndex(cursor, "valuePrimitive")
+        val _cursorIndexOfValueBoolean: Int = getColumnIndex(cursor, "valueBoolean")
+        val _cursorIndexOfValueString: Int = getColumnIndex(cursor, "valueString")
+        val _cursorIndexOfValueNullableString: Int = getColumnIndex(cursor, "valueNullableString")
+        val _cursorIndexOfVariablePrimitive: Int = getColumnIndex(cursor, "variablePrimitive")
+        val _cursorIndexOfVariableNullableBoolean: Int = getColumnIndex(cursor,
+            "variableNullableBoolean")
+        val _cursorIndexOfVariableString: Int = getColumnIndex(cursor, "variableString")
+        val _cursorIndexOfVariableNullableString: Int = getColumnIndex(cursor, "variableNullableString")
+        val _tmpValuePrimitive: Long
+        if (_cursorIndexOfValuePrimitive == -1) {
+            _tmpValuePrimitive = 0
+        } else {
+            _tmpValuePrimitive = cursor.getLong(_cursorIndexOfValuePrimitive)
+        }
+        val _tmpValueBoolean: Boolean
+        if (_cursorIndexOfValueBoolean == -1) {
+            _tmpValueBoolean = false
+        } else {
+            val _tmp: Int
+            _tmp = cursor.getInt(_cursorIndexOfValueBoolean)
+            _tmpValueBoolean = _tmp != 0
+        }
+        val _tmpValueString: String
+        if (_cursorIndexOfValueString == -1) {
+            error("Missing column 'valueString' for a non null value.")
+        } else {
+            _tmpValueString = cursor.getString(_cursorIndexOfValueString)
+        }
+        val _tmpValueNullableString: String?
+        if (_cursorIndexOfValueNullableString == -1) {
+            _tmpValueNullableString = null
+        } else {
+            if (cursor.isNull(_cursorIndexOfValueNullableString)) {
+                _tmpValueNullableString = null
+            } else {
+                _tmpValueNullableString = cursor.getString(_cursorIndexOfValueNullableString)
+            }
+        }
+        _entity = MyEntity(_tmpValuePrimitive,_tmpValueBoolean,_tmpValueString,_tmpValueNullableString)
+        if (_cursorIndexOfVariablePrimitive != -1) {
+            _entity.variablePrimitive = cursor.getLong(_cursorIndexOfVariablePrimitive)
+        }
+        if (_cursorIndexOfVariableNullableBoolean != -1) {
+            val _tmp_1: Int?
+            if (cursor.isNull(_cursorIndexOfVariableNullableBoolean)) {
+                _tmp_1 = null
+            } else {
+                _tmp_1 = cursor.getInt(_cursorIndexOfVariableNullableBoolean)
+            }
+            _entity.variableNullableBoolean = _tmp_1?.let { it != 0 }
+        }
+        if (_cursorIndexOfVariableString != -1) {
+            _entity.variableString = cursor.getString(_cursorIndexOfVariableString)
+        }
+        if (_cursorIndexOfVariableNullableString != -1) {
+            if (cursor.isNull(_cursorIndexOfVariableNullableString)) {
+                _entity.variableNullableString = null
+            } else {
+                _entity.variableNullableString = cursor.getString(_cursorIndexOfVariableNullableString)
+            }
+        }
+        return _entity
+    }
+
+    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
index 7e60516..8b0298b 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/insertOrUpsertMethodAdapter.kt
@@ -14,14 +14,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
 
     private val __upsertionAdapterOfMyEntity: EntityUpsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
index 576955b..d647d87 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_boolean.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
index 5fe939f..259d8b0 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_byteArray.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
index e63b2d2..418314d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter.kt
@@ -17,14 +17,15 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
 
     private val __fooConverter: FooConverter = FooConverter()
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
new file mode 100644
index 0000000..d717c5e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_composite.kt
@@ -0,0 +1,84 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`bar`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: Foo = FooBarConverter.toFoo(entity.bar)
+                val _tmp_1: String = FooBarConverter.toString(_tmp)
+                statement.bindString(2, _tmp_1)
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpBar: Bar
+                val _tmp: String
+                _tmp = _cursor.getString(_cursorIndexOfBar)
+                val _tmp_1: Foo = FooBarConverter.fromString(_tmp)
+                _tmpBar = FooBarConverter.fromFoo(_tmp_1)
+                _result = MyEntity(_tmpPk,_tmpBar)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    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/pojoRowAdapter_customTypeConverter_nullAware.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
new file mode 100644
index 0000000..792a573
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_nullAware.kt
@@ -0,0 +1,122 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`,`bar`) VALUES (?,?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String? = FooBarConverter.toString(entity.foo)
+                if (_tmp == null) {
+                    statement.bindNull(2)
+                } else {
+                    statement.bindString(2, _tmp)
+                }
+                val _tmp_1: Foo = FooBarConverter.toFoo(entity.bar)
+                val _tmp_2: String? = FooBarConverter.toString(_tmp_1)
+                if (_tmp_2 == null) {
+                    statement.bindNull(3)
+                } else {
+                    statement.bindString(3, _tmp_2)
+                }
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _cursorIndexOfBar: Int = getColumnIndexOrThrow(_cursor, "bar")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo
+                val _tmp: String?
+                if (_cursor.isNull(_cursorIndexOfFoo)) {
+                    _tmp = null
+                } else {
+                    _tmp = _cursor.getString(_cursorIndexOfFoo)
+                }
+                val _tmp_1: Foo? = FooBarConverter.fromString(_tmp)
+                if (_tmp_1 == null) {
+                    error("Expected non-null Foo, but it was null.")
+                } else {
+                    _tmpFoo = _tmp_1
+                }
+                val _tmpBar: Bar
+                val _tmp_2: String?
+                if (_cursor.isNull(_cursorIndexOfBar)) {
+                    _tmp_2 = null
+                } else {
+                    _tmp_2 = _cursor.getString(_cursorIndexOfBar)
+                }
+                val _tmp_3: Foo? = FooBarConverter.fromString(_tmp_2)
+                val _tmp_4: Bar?
+                if (_tmp_3 == null) {
+                    _tmp_4 = null
+                } else {
+                    _tmp_4 = FooBarConverter.fromFoo(_tmp_3)
+                }
+                if (_tmp_4 == null) {
+                    error("Expected non-null Bar, but it was null.")
+                } else {
+                    _tmpBar = _tmp_4
+                }
+                _result = MyEntity(_tmpPk,_tmpFoo,_tmpBar)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    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/pojoRowAdapter_customTypeConverter_provided.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
new file mode 100644
index 0000000..3e7273a
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_provided.kt
@@ -0,0 +1,90 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import androidx.sqlite.db.SupportSQLiteStatement
+import java.lang.Class
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Lazy
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+
+    private val __fooConverter: Lazy<FooConverter> = lazy {
+        checkNotNull(__db.getTypeConverter(FooConverter::class.java))
+    }
+
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String = __fooConverter().toString(entity.foo)
+                statement.bindString(2, _tmp)
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo
+                val _tmp: String
+                _tmp = _cursor.getString(_cursorIndexOfFoo)
+                _tmpFoo = __fooConverter().fromString(_tmp)
+                _result = MyEntity(_tmpPk,_tmpFoo)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fooConverter(): FooConverter = __fooConverter.value
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = listOf(FooConverter::class.java)
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt
new file mode 100644
index 0000000..ac5044e
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_customTypeConverter_upcast.kt
@@ -0,0 +1,91 @@
+import android.database.Cursor
+import androidx.room.EntityInsertionAdapter
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+
+    private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
+    init {
+        this.__db = __db
+        this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
+            public override fun createQuery(): String =
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`foo`) VALUES (?,?)"
+
+            public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
+                statement.bindLong(1, entity.pk.toLong())
+                val _tmp: String = FooConverter.nullableFooToString(entity.foo)
+                if (_tmp == null) {
+                    statement.bindNull(2)
+                } else {
+                    statement.bindString(2, _tmp)
+                }
+            }
+        }
+    }
+
+    public override fun addEntity(item: MyEntity): Unit {
+        __db.assertNotSuspendingTransaction()
+        __db.beginTransaction()
+        try {
+            __insertionAdapterOfMyEntity.insert(item)
+            __db.setTransactionSuccessful()
+        } finally {
+            __db.endTransaction()
+        }
+    }
+
+    public override fun getEntity(): MyEntity {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfFoo: Int = getColumnIndexOrThrow(_cursor, "foo")
+            val _result: MyEntity
+            if (_cursor.moveToFirst()) {
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpFoo: Foo?
+                val _tmp: String?
+                if (_cursor.isNull(_cursorIndexOfFoo)) {
+                    _tmp = null
+                } else {
+                    _tmp = _cursor.getString(_cursorIndexOfFoo)
+                }
+                val _tmp_1: Foo = FooConverter.nullableStringToFoo(_tmp)
+                _tmpFoo = _tmp_1
+                _result = MyEntity(_tmpPk,_tmpFoo)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    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/pojoRowAdapter_embedded.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
index 104d7ff..0326a4c 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_embedded.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
index eb1927d..8eca87d 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_enum.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
index accb714..7180121 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_internalVisibility.kt
@@ -18,12 +18,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
index 6c30089..f1b3503 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives.kt
@@ -23,12 +23,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
index 74db90a..f1a820e 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_primitives_nullable.kt
@@ -23,12 +23,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
index 32d3380..4a47bc6 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_string.kt
@@ -17,12 +17,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
index b0f3f3b..5b24625 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_uuid.kt
@@ -20,12 +20,13 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index 0b825dd..ce7bf9f 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
@@ -17,20 +17,28 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __insertionAdapterOfMyEntity: EntityInsertionAdapter<MyEntity>
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__insertionAdapterOfMyEntity = object : EntityInsertionAdapter<MyEntity>(__db) {
             public override fun createQuery(): String =
-                "INSERT OR ABORT INTO `MyEntity` (`pk`,`variable`) VALUES (?,?)"
+                "INSERT OR ABORT INTO `MyEntity` (`pk`,`variablePrimitive`,`variableString`,`variableNullableString`) VALUES (?,?,?,?)"
 
             public override fun bind(statement: SupportSQLiteStatement, entity: MyEntity): Unit {
                 statement.bindLong(1, entity.pk.toLong())
-                statement.bindLong(2, entity.variable)
+                statement.bindLong(2, entity.variablePrimitive)
+                statement.bindString(3, entity.variableString)
+                val _tmpVariableNullableString: String? = entity.variableNullableString
+                if (_tmpVariableNullableString == null) {
+                    statement.bindNull(4)
+                } else {
+                    statement.bindString(4, _tmpVariableNullableString)
+                }
             }
         }
     }
@@ -53,13 +61,22 @@
         val _cursor: Cursor = query(__db, _statement, false, null)
         try {
             val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
-            val _cursorIndexOfVariable: Int = getColumnIndexOrThrow(_cursor, "variable")
+            val _cursorIndexOfVariablePrimitive: Int = getColumnIndexOrThrow(_cursor, "variablePrimitive")
+            val _cursorIndexOfVariableString: Int = getColumnIndexOrThrow(_cursor, "variableString")
+            val _cursorIndexOfVariableNullableString: Int = getColumnIndexOrThrow(_cursor,
+                "variableNullableString")
             val _result: MyEntity
             if (_cursor.moveToFirst()) {
                 val _tmpPk: Int
                 _tmpPk = _cursor.getInt(_cursorIndexOfPk)
                 _result = MyEntity(_tmpPk)
-                _result.variable = _cursor.getLong(_cursorIndexOfVariable)
+                _result.variablePrimitive = _cursor.getLong(_cursorIndexOfVariablePrimitive)
+                _result.variableString = _cursor.getString(_cursorIndexOfVariableString)
+                if (_cursor.isNull(_cursorIndexOfVariableNullableString)) {
+                    _result.variableNullableString = null
+                } else {
+                    _result.variableNullableString = _cursor.getString(_cursorIndexOfVariableNullableString)
+                }
             } else {
                 error("Cursor was empty, but expected a single item.")
             }
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
index cafdfc6..e5e7baa 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty_java.kt
@@ -15,10 +15,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
index b545e8b..cd5c218 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/preparedQueryAdapter.kt
@@ -13,7 +13,9 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
 
     private val __preparedStmtOfInsertEntity: SharedSQLiteStatement
@@ -23,8 +25,7 @@
     private val __preparedStmtOfUpdateEntityReturnInt: SharedSQLiteStatement
 
     private val __preparedStmtOfDeleteEntity: SharedSQLiteStatement
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
         this.__preparedStmtOfInsertEntity = object : SharedSQLiteStatement(__db) {
             public override fun createQuery(): String {
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
index 98c21c1..11a7cef 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
@@ -16,10 +16,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt
new file mode 100644
index 0000000..4d5d708
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map.kt
@@ -0,0 +1,215 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.query
+import java.lang.Class
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): Map<Song, Artist> {
+        val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _result: MutableMap<Song, Artist> = LinkedHashMap<Song, Artist>()
+            while (_cursor.moveToNext()) {
+                val _key: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _key = Song(_tmpSongId,_tmpArtistKey)
+                if (_cursor.isNull(_cursorIndexOfArtistId)) {
+                    error("Missing value for a key.")
+                }
+                val _value: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _value = Artist(_tmpArtistId)
+                if (!_result.containsKey(_key)) {
+                    _result.put(_key, _value)
+                }
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getSongsWithNullableArtist(): Map<Song, Artist?> {
+        val _sql: String = "SELECT * FROM Song JOIN Artist ON Song.artistKey = Artist.artistId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _result: MutableMap<Song, Artist?> = LinkedHashMap<Song, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _key: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _key = Song(_tmpSongId,_tmpArtistKey)
+                if (_cursor.isNull(_cursorIndexOfArtistId)) {
+                    _result.put(_key, null)
+                    continue
+                }
+                val _value: Artist?
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _value = Artist(_tmpArtistId)
+                if (!_result.containsKey(_key)) {
+                    _result.put(_key, _value)
+                }
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistWithSongs(): Map<Artist, List<Song>> {
+        val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _result: MutableMap<Artist, MutableList<Song>> =
+                LinkedHashMap<Artist, MutableList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _key: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _key = Artist(_tmpArtistId)
+                val _values: MutableList<Song>
+                if (_result.containsKey(_key)) {
+                    _values = _result.getValue(_key)
+                } else {
+                    _values = ArrayList<Song>()
+                    _result.put(_key, _values)
+                }
+                if (_cursor.isNull(_cursorIndexOfSongId) && _cursor.isNull(_cursorIndexOfArtistKey)) {
+                    continue
+                }
+                val _value: Song
+                val _tmpSongId: String
+                _tmpSongId = _cursor.getString(_cursorIndexOfSongId)
+                val _tmpArtistKey: String
+                _tmpArtistKey = _cursor.getString(_cursorIndexOfArtistKey)
+                _value = Song(_tmpSongId,_tmpArtistKey)
+                _values.add(_value)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistSongCount(): Map<Artist, Int> {
+        val _sql: String =
+            "SELECT Artist.*, COUNT(songId) as songCount FROM Artist JOIN Song ON Artist.artistId = Song.artistKey GROUP BY artistId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _columnIndexOfSongCount: Int = getColumnIndexOrThrow(_cursor, "songCount")
+            val _result: MutableMap<Artist, Int> = LinkedHashMap<Artist, Int>()
+            while (_cursor.moveToNext()) {
+                val _key: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _key = Artist(_tmpArtistId)
+                if (_cursor.isNull(_columnIndexOfSongCount)) {
+                    error("Missing value for a key.")
+                }
+                val _value: Int
+                val _tmp: Int
+                _tmp = _cursor.getInt(_columnIndexOfSongCount)
+                _value = _tmp
+                if (!_result.containsKey(_key)) {
+                    _result.put(_key, _value)
+                }
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistWithSongIds(): Map<Artist, List<String>> {
+        val _sql: String = "SELECT * FROM Artist JOIN Song ON Artist.artistId = Song.artistKey"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _columnIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _result: MutableMap<Artist, MutableList<String>> =
+                LinkedHashMap<Artist, MutableList<String>>()
+            while (_cursor.moveToNext()) {
+                val _key: Artist
+                val _tmpArtistId: String
+                _tmpArtistId = _cursor.getString(_cursorIndexOfArtistId)
+                _key = Artist(_tmpArtistId)
+                val _values: MutableList<String>
+                if (_result.containsKey(_key)) {
+                    _values = _result.getValue(_key)
+                } else {
+                    _values = ArrayList<String>()
+                    _result.put(_key, _values)
+                }
+                if (_cursor.isNull(_columnIndexOfSongId)) {
+                    continue
+                }
+                val _value: String
+                _value = _cursor.getString(_columnIndexOfSongId)
+                _values.add(_value)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    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/queryResultAdapter_map_ambiguousIndexAdapter.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map_ambiguousIndexAdapter.kt
new file mode 100644
index 0000000..9217ac6
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_map_ambiguousIndexAdapter.kt
@@ -0,0 +1,216 @@
+import android.database.Cursor
+import androidx.room.AmbiguousColumnResolver
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.getColumnIndex
+import androidx.room.util.query
+import androidx.room.util.wrapMappedColumns
+import java.lang.Class
+import java.util.ArrayList
+import java.util.LinkedHashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Array
+import kotlin.Int
+import kotlin.IntArray
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.Map
+import kotlin.collections.MutableList
+import kotlin.collections.MutableMap
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getUserCommentMap(): Map<User, List<Comment>> {
+        val _sql: String = "SELECT * FROM User JOIN Comment ON User.id = Comment.userId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndices: Array<IntArray> =
+                AmbiguousColumnResolver.resolve(_cursor.getColumnNames(), arrayOf(arrayOf("id", "name"),
+                    arrayOf("id", "userId", "text")))
+            val _result: MutableMap<User, MutableList<Comment>> =
+                LinkedHashMap<User, MutableList<Comment>>()
+            while (_cursor.moveToNext()) {
+                val _key: User
+                val _tmpId: Int
+                _tmpId = _cursor.getInt(_cursorIndices[0][0])
+                val _tmpName: String
+                _tmpName = _cursor.getString(_cursorIndices[0][1])
+                _key = User(_tmpId,_tmpName)
+                val _values: MutableList<Comment>
+                if (_result.containsKey(_key)) {
+                    _values = _result.getValue(_key)
+                } else {
+                    _values = ArrayList<Comment>()
+                    _result.put(_key, _values)
+                }
+                if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+                    _cursor.isNull(_cursorIndices[1][2])) {
+                    continue
+                }
+                val _value: Comment
+                val _tmpId_1: Int
+                _tmpId_1 = _cursor.getInt(_cursorIndices[1][0])
+                val _tmpUserId: Int
+                _tmpUserId = _cursor.getInt(_cursorIndices[1][1])
+                val _tmpText: String
+                _tmpText = _cursor.getString(_cursorIndices[1][2])
+                _value = Comment(_tmpId_1,_tmpUserId,_tmpText)
+                _values.add(_value)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getUserCommentMapWithoutStarProjection(): Map<User, List<Comment>> {
+        val _sql: String =
+            "SELECT User.id, name, Comment.id, userId, text FROM User JOIN Comment ON User.id = Comment.userId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndices: Array<IntArray> = arrayOf(intArrayOf(0, 1), intArrayOf(2, 3, 4))
+            val _result: MutableMap<User, MutableList<Comment>> =
+                LinkedHashMap<User, MutableList<Comment>>()
+            while (_cursor.moveToNext()) {
+                val _key: User
+                val _tmpId: Int
+                _tmpId = _cursor.getInt(_cursorIndices[0][0])
+                val _tmpName: String
+                _tmpName = _cursor.getString(_cursorIndices[0][1])
+                _key = User(_tmpId,_tmpName)
+                val _values: MutableList<Comment>
+                if (_result.containsKey(_key)) {
+                    _values = _result.getValue(_key)
+                } else {
+                    _values = ArrayList<Comment>()
+                    _result.put(_key, _values)
+                }
+                if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+                    _cursor.isNull(_cursorIndices[1][2])) {
+                    continue
+                }
+                val _value: Comment
+                val _tmpId_1: Int
+                _tmpId_1 = _cursor.getInt(_cursorIndices[1][0])
+                val _tmpUserId: Int
+                _tmpUserId = _cursor.getInt(_cursorIndices[1][1])
+                val _tmpText: String
+                _tmpText = _cursor.getString(_cursorIndices[1][2])
+                _value = Comment(_tmpId_1,_tmpUserId,_tmpText)
+                _values.add(_value)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getUserCommentMapWithoutQueryVerification(): Map<User, List<Comment>> {
+        val _sql: String = "SELECT * FROM User JOIN Comment ON User.id = Comment.userId"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndices: Array<IntArray> =
+                AmbiguousColumnResolver.resolve(_cursor.getColumnNames(), arrayOf(arrayOf("id", "name"),
+                    arrayOf("id", "userId", "text")))
+            val _wrappedCursor: Cursor = wrapMappedColumns(_cursor, arrayOf("id", "name"),
+                intArrayOf(_cursorIndices[0][0], _cursorIndices[0][1]))
+            val _wrappedCursor_1: Cursor = wrapMappedColumns(_cursor, arrayOf("id", "userId", "text"),
+                intArrayOf(_cursorIndices[1][0], _cursorIndices[1][1], _cursorIndices[1][2]))
+            val _result: MutableMap<User, MutableList<Comment>> =
+                LinkedHashMap<User, MutableList<Comment>>()
+            while (_cursor.moveToNext()) {
+                val _key: User
+                _key = __entityCursorConverter_User(_wrappedCursor)
+                val _values: MutableList<Comment>
+                if (_result.containsKey(_key)) {
+                    _values = _result.getValue(_key)
+                } else {
+                    _values = ArrayList<Comment>()
+                    _result.put(_key, _values)
+                }
+                if (_cursor.isNull(_cursorIndices[1][0]) && _cursor.isNull(_cursorIndices[1][1]) &&
+                    _cursor.isNull(_cursorIndices[1][2])) {
+                    continue
+                }
+                val _value: Comment
+                _value = __entityCursorConverter_Comment(_wrappedCursor_1)
+                _values.add(_value)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __entityCursorConverter_User(cursor: Cursor): User {
+        val _entity: User
+        val _cursorIndexOfId: Int = getColumnIndex(cursor, "id")
+        val _cursorIndexOfName: Int = getColumnIndex(cursor, "name")
+        val _tmpId: Int
+        if (_cursorIndexOfId == -1) {
+            _tmpId = 0
+        } else {
+            _tmpId = cursor.getInt(_cursorIndexOfId)
+        }
+        val _tmpName: String
+        if (_cursorIndexOfName == -1) {
+            error("Missing column 'name' for a non null value.")
+        } else {
+            _tmpName = cursor.getString(_cursorIndexOfName)
+        }
+        _entity = User(_tmpId,_tmpName)
+        return _entity
+    }
+
+    private fun __entityCursorConverter_Comment(cursor: Cursor): Comment {
+        val _entity: Comment
+        val _cursorIndexOfId: Int = getColumnIndex(cursor, "id")
+        val _cursorIndexOfUserId: Int = getColumnIndex(cursor, "userId")
+        val _cursorIndexOfText: Int = getColumnIndex(cursor, "text")
+        val _tmpId: Int
+        if (_cursorIndexOfId == -1) {
+            _tmpId = 0
+        } else {
+            _tmpId = cursor.getInt(_cursorIndexOfId)
+        }
+        val _tmpUserId: Int
+        if (_cursorIndexOfUserId == -1) {
+            _tmpUserId = 0
+        } else {
+            _tmpUserId = cursor.getInt(_cursorIndexOfUserId)
+        }
+        val _tmpText: String
+        if (_cursorIndexOfText == -1) {
+            error("Missing column 'text' for a non null value.")
+        } else {
+            _tmpText = cursor.getString(_cursorIndexOfText)
+        }
+        _entity = Comment(_tmpId,_tmpUserId,_tmpText)
+        return _entity
+    }
+
+    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/rawQuery.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
index 688d74c..b42aaa4 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/rawQuery.kt
@@ -13,10 +13,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
new file mode 100644
index 0000000..4e5ad57
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations.kt
@@ -0,0 +1,307 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    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/relations_arrayMap.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
new file mode 100644
index 0000000..56c6b76
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_arrayMap.kt
@@ -0,0 +1,307 @@
+import android.database.Cursor
+import androidx.collection.ArrayMap
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchArrayMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: ArrayMap<Long, Artist?> = ArrayMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: ArrayMap<Long, ArrayList<Song>> = ArrayMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: ArrayMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: ArrayMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: ArrayMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchArrayMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    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/relations_byteBufferKey.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
new file mode 100644
index 0000000..498e033
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_byteBufferKey.kt
@@ -0,0 +1,129 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.nio.ByteBuffer
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.ByteArray
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<ByteBuffer, Artist?> = HashMap<ByteBuffer, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: ByteBuffer
+                _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_cursorIndexOfArtistKey))
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: ByteArray
+                _tmpArtistKey = _cursor.getBlob(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: ByteBuffer
+                _tmpKey_1 = ByteBuffer.wrap(_cursor.getBlob(_cursorIndexOfArtistKey))
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<ByteBuffer, Artist?>): Unit {
+        val __mapKeySet: Set<ByteBuffer> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: ByteBuffer in __mapKeySet) {
+            _stmt.bindBlob(_argIndex, _item.array())
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: ByteBuffer
+                _tmpKey = ByteBuffer.wrap(_cursor.getBlob(_itemKeyIndex))
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: ByteArray
+                    _tmpArtistId = _cursor.getBlob(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    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/relations_longSparseArray.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
new file mode 100644
index 0000000..f1b082d7
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_longSparseArray.kt
@@ -0,0 +1,306 @@
+import android.database.Cursor
+import androidx.collection.LongSparseArray
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchLongSparseArray
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+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(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: LongSparseArray<Artist?> = LongSparseArray<Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _collectionArtist.put(_tmpKey, null)
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long
+                _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                if (_tmpArtist == null) {
+                    error("Missing relationship item.")
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: LongSparseArray<ArrayList<Song>> = LongSparseArray<ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = checkNotNull(_collectionSongs.get(_tmpKey_1))
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: LongSparseArray<Artist?>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: LongSparseArray<ArrayList<Song>>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: LongSparseArray<ArrayList<Song>>): Unit {
+        if (_map.isEmpty()) {
+            return
+        }
+        if (_map.size() > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchLongSparseArray(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = _map.size()
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (i in 0 until _map.size()) {
+            val _item: Long = _map.keyAt(i)
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    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/relations_nullable.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
new file mode 100644
index 0000000..a01fe75
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/relations_nullable.kt
@@ -0,0 +1,336 @@
+import android.database.Cursor
+import androidx.room.RoomDatabase
+import androidx.room.RoomSQLiteQuery
+import androidx.room.RoomSQLiteQuery.Companion.acquire
+import androidx.room.util.appendPlaceholders
+import androidx.room.util.getColumnIndex
+import androidx.room.util.getColumnIndexOrThrow
+import androidx.room.util.newStringBuilder
+import androidx.room.util.query
+import androidx.room.util.recursiveFetchHashMap
+import java.lang.Class
+import java.lang.StringBuilder
+import java.util.ArrayList
+import java.util.HashMap
+import javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.Long
+import kotlin.String
+import kotlin.Suppress
+import kotlin.Unit
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
+    private val __db: RoomDatabase
+    init {
+        this.__db = __db
+    }
+
+    public override fun getSongsWithArtist(): SongWithArtist {
+        val _sql: String = "SELECT * FROM Song"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfSongId: Int = getColumnIndexOrThrow(_cursor, "songId")
+            val _cursorIndexOfArtistKey: Int = getColumnIndexOrThrow(_cursor, "artistKey")
+            val _collectionArtist: HashMap<Long, Artist?> = HashMap<Long, Artist?>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpKey = null
+                } else {
+                    _tmpKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                if (_tmpKey != null) {
+                    _collectionArtist.put(_tmpKey, null)
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipArtistAsArtist(_collectionArtist)
+            val _result: SongWithArtist
+            if (_cursor.moveToFirst()) {
+                val _tmpSong: Song
+                val _tmpSongId: Long
+                _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                val _tmpArtistKey: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpArtistKey = null
+                } else {
+                    _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                _tmpSong = Song(_tmpSongId,_tmpArtistKey)
+                val _tmpArtist: Artist?
+                val _tmpKey_1: Long?
+                if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                    _tmpKey_1 = null
+                } else {
+                    _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistKey)
+                }
+                if (_tmpKey_1 != null) {
+                    _tmpArtist = _collectionArtist.get(_tmpKey_1)
+                } else {
+                    _tmpArtist = null
+                }
+                _result = SongWithArtist(_tmpSong,_tmpArtist)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getArtistAndSongs(): ArtistAndSongs {
+        val _sql: String = "SELECT * FROM Artist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfArtistId: Int = getColumnIndexOrThrow(_cursor, "artistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfArtistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong(_collectionSongs)
+            val _result: ArtistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpArtist: Artist
+                val _tmpArtistId: Long
+                _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpArtist = Artist(_tmpArtistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfArtistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = ArtistAndSongs(_tmpArtist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun getPlaylistAndSongs(): PlaylistAndSongs {
+        val _sql: String = "SELECT * FROM Playlist"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, true, null)
+        try {
+            val _cursorIndexOfPlaylistId: Int = getColumnIndexOrThrow(_cursor, "playlistId")
+            val _collectionSongs: HashMap<Long, ArrayList<Song>> = HashMap<Long, ArrayList<Song>>()
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_cursorIndexOfPlaylistId)
+                if (!_collectionSongs.containsKey(_tmpKey)) {
+                    _collectionSongs.put(_tmpKey, ArrayList<Song>())
+                }
+            }
+            _cursor.moveToPosition(-1)
+            __fetchRelationshipSongAsSong_1(_collectionSongs)
+            val _result: PlaylistAndSongs
+            if (_cursor.moveToFirst()) {
+                val _tmpPlaylist: Playlist
+                val _tmpPlaylistId: Long
+                _tmpPlaylistId = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpPlaylist = Playlist(_tmpPlaylistId)
+                val _tmpSongsCollection: ArrayList<Song>
+                val _tmpKey_1: Long
+                _tmpKey_1 = _cursor.getLong(_cursorIndexOfPlaylistId)
+                _tmpSongsCollection = _collectionSongs.getValue(_tmpKey_1)
+                _result = PlaylistAndSongs(_tmpPlaylist,_tmpSongsCollection)
+            } else {
+                error("Cursor was empty, but expected a single item.")
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    private fun __fetchRelationshipArtistAsArtist(_map: HashMap<Long, Artist?>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, false) {
+                __fetchRelationshipArtistAsArtist(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `artistId` FROM `Artist` WHERE `artistId` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistId")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfArtistId: Int = 0
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                if (_map.containsKey(_tmpKey)) {
+                    val _item_1: Artist?
+                    val _tmpArtistId: Long
+                    _tmpArtistId = _cursor.getLong(_cursorIndexOfArtistId)
+                    _item_1 = Artist(_tmpArtistId)
+                    _map.put(_tmpKey, _item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `songId`,`artistKey` FROM `Song` WHERE `artistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            val _itemKeyIndex: Int = getColumnIndex(_cursor, "artistKey")
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long?
+                if (_cursor.isNull(_itemKeyIndex)) {
+                    _tmpKey = null
+                } else {
+                    _tmpKey = _cursor.getLong(_itemKeyIndex)
+                }
+                if (_tmpKey != null) {
+                    val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                    if (_tmpRelation != null) {
+                        val _item_1: Song
+                        val _tmpSongId: Long
+                        _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                        val _tmpArtistKey: Long?
+                        if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                            _tmpArtistKey = null
+                        } else {
+                            _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                        }
+                        _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                        _tmpRelation.add(_item_1)
+                    }
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    private fun __fetchRelationshipSongAsSong_1(_map: HashMap<Long, ArrayList<Song>>): Unit {
+        val __mapKeySet: Set<Long> = _map.keys
+        if (__mapKeySet.isEmpty()) {
+            return
+        }
+        if (_map.size > RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            recursiveFetchHashMap(_map, true) {
+                __fetchRelationshipSongAsSong_1(it)
+            }
+            return
+        }
+        val _stringBuilder: StringBuilder = newStringBuilder()
+        _stringBuilder.append("SELECT `Song`.`songId` AS `songId`,`Song`.`artistKey` AS `artistKey`,_junction.`playlistKey` FROM `PlaylistSongXRef` AS _junction INNER JOIN `Song` ON (_junction.`songKey` = `Song`.`songId`) WHERE _junction.`playlistKey` IN (")
+        val _inputSize: Int = __mapKeySet.size
+        appendPlaceholders(_stringBuilder, _inputSize)
+        _stringBuilder.append(")")
+        val _sql: String = _stringBuilder.toString()
+        val _argCount: Int = 0 + _inputSize
+        val _stmt: RoomSQLiteQuery = acquire(_sql, _argCount)
+        var _argIndex: Int = 1
+        for (_item: Long in __mapKeySet) {
+            _stmt.bindLong(_argIndex, _item)
+            _argIndex++
+        }
+        val _cursor: Cursor = query(__db, _stmt, false, null)
+        try {
+            // _junction.playlistKey
+            val _itemKeyIndex: Int = 2
+            if (_itemKeyIndex == -1) {
+                return
+            }
+            val _cursorIndexOfSongId: Int = 0
+            val _cursorIndexOfArtistKey: Int = 1
+            while (_cursor.moveToNext()) {
+                val _tmpKey: Long
+                _tmpKey = _cursor.getLong(_itemKeyIndex)
+                val _tmpRelation: ArrayList<Song>? = _map.get(_tmpKey)
+                if (_tmpRelation != null) {
+                    val _item_1: Song
+                    val _tmpSongId: Long
+                    _tmpSongId = _cursor.getLong(_cursorIndexOfSongId)
+                    val _tmpArtistKey: Long?
+                    if (_cursor.isNull(_cursorIndexOfArtistKey)) {
+                        _tmpArtistKey = null
+                    } else {
+                        _tmpArtistKey = _cursor.getLong(_cursorIndexOfArtistKey)
+                    }
+                    _item_1 = Song(_tmpSongId,_tmpArtistKey)
+                    _tmpRelation.add(_item_1)
+                }
+            }
+        } finally {
+            _cursor.close()
+        }
+    }
+
+    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
index 3658d37..fcd26fa 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_abstractClass.kt
@@ -9,10 +9,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao() {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
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
index 15e154e..2975e78 100644
--- a/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/transactionMethodAdapter_interface.kt
@@ -13,10 +13,11 @@
 
 @Generated(value = ["androidx.room.RoomProcessor"])
 @Suppress(names = ["unchecked", "deprecation"])
-public class MyDao_Impl : MyDao {
+public class MyDao_Impl(
+    __db: RoomDatabase,
+) : MyDao {
     private val __db: RoomDatabase
-
-    public constructor(__db: RoomDatabase) {
+    init {
         this.__db = __db
     }
 
diff --git a/room/room-runtime/api/public_plus_experimental_current.txt b/room/room-runtime/api/public_plus_experimental_current.txt
index 68990e9..a7b3c40 100644
--- a/room/room-runtime/api/public_plus_experimental_current.txt
+++ b/room/room-runtime/api/public_plus_experimental_current.txt
@@ -27,7 +27,7 @@
   public final class EntityUpsertionAdapterKt {
   }
 
-  @RequiresOptIn @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalRoomApi {
+  @RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalRoomApi {
   }
 
   public class InvalidationTracker {
diff --git a/room/room-runtime/api/restricted_current.ignore b/room/room-runtime/api/restricted_current.ignore
index a65fb24..eb7e8ba 100644
--- a/room/room-runtime/api/restricted_current.ignore
+++ b/room/room-runtime/api/restricted_current.ignore
@@ -1,3 +1,17 @@
 // Baseline format: 1.0
 InvalidNullConversion: androidx.room.EntityInsertionAdapter#bind(androidx.sqlite.db.SupportSQLiteStatement, T) parameter #0:
     Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter statement in androidx.room.EntityInsertionAdapter.bind(androidx.sqlite.db.SupportSQLiteStatement statement, T entity)
+
+
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#createAllTables(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.createAllTables
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.dropAllTables
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onCreate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onCreate
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onOpen(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onOpen
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onPostMigrate
+ParameterNameChange: androidx.room.RoomOpenHelper.Delegate#onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase) parameter #0:
+    Attempted to change parameter name from database to db in method androidx.room.RoomOpenHelper.Delegate.onPreMigrate
diff --git a/room/room-runtime/api/restricted_current.txt b/room/room-runtime/api/restricted_current.txt
index 715749d..3f55e9d 100644
--- a/room/room-runtime/api/restricted_current.txt
+++ b/room/room-runtime/api/restricted_current.txt
@@ -211,12 +211,12 @@
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public abstract static class RoomOpenHelper.Delegate {
     ctor public RoomOpenHelper.Delegate(int version);
-    method public abstract void createAllTables(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public abstract void onOpen(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public void onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase database);
-    method public void onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase database);
+    method public abstract void createAllTables(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void dropAllTables(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onCreate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public abstract void onOpen(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onPostMigrate(androidx.sqlite.db.SupportSQLiteDatabase db);
+    method public void onPreMigrate(androidx.sqlite.db.SupportSQLiteDatabase db);
     method public androidx.room.RoomOpenHelper.ValidationResult onValidateSchema(androidx.sqlite.db.SupportSQLiteDatabase db);
     method @Deprecated protected void validateMigration(androidx.sqlite.db.SupportSQLiteDatabase db);
     field public final int version;
@@ -341,6 +341,12 @@
     method public androidx.room.util.FtsTableInfo read(androidx.sqlite.db.SupportSQLiteDatabase database, String tableName);
   }
 
+  @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class RelationUtil {
+    method public static <K, V> void recursiveFetchArrayMap(androidx.collection.ArrayMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.ArrayMap<K,V>,kotlin.Unit> fetchBlock);
+    method public static <K, V> void recursiveFetchHashMap(java.util.HashMap<K,V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super java.util.HashMap<K,V>,kotlin.Unit> fetchBlock);
+    method public static <V> void recursiveFetchLongSparseArray(androidx.collection.LongSparseArray<V> map, boolean isRelationCollection, kotlin.jvm.functions.Function1<? super androidx.collection.LongSparseArray<V>,kotlin.Unit> fetchBlock);
+  }
+
   @RestrictTo({androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX}) public final class StringUtil {
     method public static void appendPlaceholders(StringBuilder builder, int count);
     method public static String? joinIntoString(java.util.List<java.lang.Integer>? input);
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index c9153f0..49b6c29 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -40,6 +40,7 @@
     api(project(":sqlite:sqlite-framework"))
     api(project(":sqlite:sqlite"))
     implementation("androidx.arch.core:core-runtime:2.0.1")
+    compileOnly("androidx.collection:collection:1.2.0")
     compileOnly("androidx.paging:paging-common:2.0.0")
     compileOnly("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
     implementation("androidx.annotation:annotation-experimental:1.1.0-rc01")
diff --git a/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt b/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
index 1535b4f..965bdde 100644
--- a/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
+++ b/room/room-runtime/src/main/java/androidx/room/ExperimentalRoomApi.kt
@@ -26,4 +26,5 @@
 )
 @Suppress("UnsafeOptInUsageError")
 @RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalRoomApi
\ No newline at end of file
diff --git a/room/room-runtime/src/main/java/androidx/room/Room.kt b/room/room-runtime/src/main/java/androidx/room/Room.kt
index fcd1527..3aa15d5 100644
--- a/room/room-runtime/src/main/java/androidx/room/Room.kt
+++ b/room/room-runtime/src/main/java/androidx/room/Room.kt
@@ -61,11 +61,11 @@
             )
         } catch (e: IllegalAccessException) {
             throw RuntimeException(
-                "Cannot access the constructor $klass.canonicalName"
+                "Cannot access the constructor ${klass.canonicalName}"
             )
         } catch (e: InstantiationException) {
             throw RuntimeException(
-                "Failed to create an instance of $klass.canonicalName"
+                "Failed to create an instance of ${klass.canonicalName}"
             )
         }
     }
diff --git a/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt b/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
index c3008b3..929dcca 100644
--- a/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
+++ b/room/room-runtime/src/main/java/androidx/room/RoomOpenHelper.kt
@@ -179,10 +179,10 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
     abstract class Delegate(@JvmField val version: Int) {
-        abstract fun dropAllTables(database: SupportSQLiteDatabase)
-        abstract fun createAllTables(database: SupportSQLiteDatabase)
-        abstract fun onOpen(database: SupportSQLiteDatabase)
-        abstract fun onCreate(database: SupportSQLiteDatabase)
+        abstract fun dropAllTables(db: SupportSQLiteDatabase)
+        abstract fun createAllTables(db: SupportSQLiteDatabase)
+        abstract fun onOpen(db: SupportSQLiteDatabase)
+        abstract fun onCreate(db: SupportSQLiteDatabase)
 
         /**
          * Called after a migration run to validate database integrity.
@@ -209,13 +209,13 @@
          * Called before migrations execute to perform preliminary work.
          * @param database The SQLite database.
          */
-        open fun onPreMigrate(database: SupportSQLiteDatabase) {}
+        open fun onPreMigrate(db: SupportSQLiteDatabase) {}
 
         /**
          * Called after migrations execute to perform additional work.
          * @param database The SQLite database.
          */
-        open fun onPostMigrate(database: SupportSQLiteDatabase) {}
+        open fun onPostMigrate(db: SupportSQLiteDatabase) {}
     }
 
     /**
diff --git a/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt b/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt
new file mode 100644
index 0000000..cefa581
--- /dev/null
+++ b/room/room-runtime/src/main/java/androidx/room/util/RelationUtil.kt
@@ -0,0 +1,149 @@
+/*
+ * 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:JvmName("RelationUtil")
+@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+
+package androidx.room.util
+
+import androidx.annotation.RestrictTo
+import androidx.collection.ArrayMap
+import androidx.collection.LongSparseArray
+import androidx.room.RoomDatabase
+
+/**
+ * Utility function used in generated code to recursively fetch relationships when the amount of
+ * keys exceed [RoomDatabase.MAX_BIND_PARAMETER_CNT].
+ *
+ * @param map - The map containing the relationship keys to fill-in.
+ * @param isRelationCollection - True if [V] is a [Collection] which means it is non null.
+ * @param fetchBlock - A lambda for calling the generated _fetchRelationship function.
+ */
+fun <K : Any, V> recursiveFetchHashMap(
+    map: HashMap<K, V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (HashMap<K, V>) -> Unit
+) {
+    val tmpMap = HashMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    for (key in map.keys) {
+        // Safe because `V` is a nullable type arg when isRelationCollection == false and vice versa
+        @Suppress("UNCHECKED_CAST")
+        if (isRelationCollection) {
+            tmpMap[key] = map[key] as V
+        } else {
+            tmpMap[key] = null as V
+        }
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            // recursively load that batch
+            fetchBlock(tmpMap)
+            // for non collection relation, put the loaded batch in the original map,
+            // not needed when dealing with collections since references are passed
+            if (!isRelationCollection) {
+                map.putAll(tmpMap)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        // load the last batch
+        fetchBlock(tmpMap)
+        // for non collection relation, put the last batch in the original map
+        if (!isRelationCollection) {
+            map.putAll(tmpMap)
+        }
+    }
+}
+
+/**
+ * Same as [recursiveFetchHashMap] but for [LongSparseArray].
+ */
+fun <V> recursiveFetchLongSparseArray(
+    map: LongSparseArray<V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (LongSparseArray<V>) -> Unit
+) {
+    val tmpMap = LongSparseArray<V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    var mapIndex = 0
+    val limit = map.size()
+    while (mapIndex < limit) {
+        if (isRelationCollection) {
+            tmpMap.put(map.keyAt(mapIndex), map.valueAt(mapIndex))
+        } else {
+            // Safe because `V` is a nullable type arg when isRelationCollection == false
+            @Suppress("UNCHECKED_CAST")
+            tmpMap.put(map.keyAt(mapIndex), null as V)
+        }
+        mapIndex++
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            fetchBlock(tmpMap)
+            if (!isRelationCollection) {
+                map.putAll(tmpMap)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        fetchBlock(tmpMap)
+        if (!isRelationCollection) {
+            map.putAll(tmpMap)
+        }
+    }
+}
+
+/**
+ * Same as [recursiveFetchHashMap] but for [ArrayMap].
+ */
+fun <K : Any, V> recursiveFetchArrayMap(
+    map: ArrayMap<K, V>,
+    isRelationCollection: Boolean,
+    fetchBlock: (ArrayMap<K, V>) -> Unit
+) {
+    val tmpMap = ArrayMap<K, V>(RoomDatabase.MAX_BIND_PARAMETER_CNT)
+    var count = 0
+    var mapIndex = 0
+    val limit = map.size
+    while (mapIndex < limit) {
+        if (isRelationCollection) {
+            tmpMap[map.keyAt(mapIndex)] = map.valueAt(mapIndex)
+        } else {
+            tmpMap[map.keyAt(mapIndex)] = null
+        }
+        mapIndex++
+        count++
+        if (count == RoomDatabase.MAX_BIND_PARAMETER_CNT) {
+            fetchBlock(tmpMap)
+            if (!isRelationCollection) {
+                // Cast needed to disambiguate from putAll(SimpleArrayMap)
+                map.putAll(tmpMap as Map<K, V>)
+            }
+            tmpMap.clear()
+            count = 0
+        }
+    }
+    if (count > 0) {
+        fetchBlock(tmpMap)
+        if (!isRelationCollection) {
+            // Cast needed to disambiguate from putAll(SimpleArrayMap)
+            map.putAll(tmpMap as Map<K, V>)
+        }
+    }
+}
diff --git a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
index 0f33171..1627a03 100644
--- a/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
+++ b/room/room-testing/src/main/java/androidx/room/testing/MigrationTestHelper.kt
@@ -458,7 +458,7 @@
         databaseBundle: DatabaseBundle,
         private val mVerifyDroppedTables: Boolean
     ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(database: SupportSQLiteDatabase) {
+        override fun createAllTables(db: SupportSQLiteDatabase) {
             throw UnsupportedOperationException(
                 "Was expecting to migrate but received create." +
                     "Make sure you have created the database first."
@@ -547,9 +547,9 @@
     internal class CreatingDelegate(
         databaseBundle: DatabaseBundle
     ) : RoomOpenHelperDelegate(databaseBundle) {
-        override fun createAllTables(database: SupportSQLiteDatabase) {
+        override fun createAllTables(db: SupportSQLiteDatabase) {
             mDatabaseBundle.buildCreateQueries().forEach { query ->
-                database.execSQL(query)
+                db.execSQL(query)
             }
         }
 
@@ -567,12 +567,12 @@
     ) : RoomOpenHelper.Delegate(
             mDatabaseBundle.version
         ) {
-        override fun dropAllTables(database: SupportSQLiteDatabase) {
+        override fun dropAllTables(db: SupportSQLiteDatabase) {
             throw UnsupportedOperationException("cannot drop all tables in the test")
         }
 
-        override fun onCreate(database: SupportSQLiteDatabase) {}
-        override fun onOpen(database: SupportSQLiteDatabase) {}
+        override fun onCreate(db: SupportSQLiteDatabase) {}
+        override fun onOpen(db: SupportSQLiteDatabase) {}
     }
 
     internal companion object {
diff --git a/samples/AndroidXDemos/build.gradle b/samples/AndroidXDemos/build.gradle
index f3e2a15..56df8c2 100644
--- a/samples/AndroidXDemos/build.gradle
+++ b/samples/AndroidXDemos/build.gradle
@@ -34,8 +34,4 @@
         abortOnError false
     }
     namespace "com.example.androidx"
-    compileOptions {
-        sourceCompatibility JavaVersion.VERSION_11
-        targetCompatibility JavaVersion.VERSION_11
-    }
 }
diff --git a/samples/Support4Demos/build.gradle b/samples/Support4Demos/build.gradle
index c282a18..cc2a5a6 100644
--- a/samples/Support4Demos/build.gradle
+++ b/samples/Support4Demos/build.gradle
@@ -15,6 +15,9 @@
     implementation(project(":viewpager:viewpager"))
     implementation(libs.kotlinStdlib)
     implementation(libs.kotlinCoroutinesAndroid)
+    implementation(project(":coordinatorlayout:coordinatorlayout"))
+    implementation("com.google.android.material:material:1.6.0")
+    implementation(project(":appcompat:appcompat"))
 }
 
 android {
diff --git a/samples/Support4Demos/src/main/AndroidManifest.xml b/samples/Support4Demos/src/main/AndroidManifest.xml
index a757919..6865741 100644
--- a/samples/Support4Demos/src/main/AndroidManifest.xml
+++ b/samples/Support4Demos/src/main/AndroidManifest.xml
@@ -427,6 +427,17 @@
         </activity>
 
         <activity
+            android:name=".widget.NestedScrollActivity3LevelsWithCollapsingToolbar"
+            android:exported="true"
+            android:theme="@style/Theme.AppCompat.Light"
+            android:label="@string/nested_scroll_3_levels_collapsing">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="com.example.android.supportv4.SUPPORT4_SAMPLE_CODE" />
+            </intent-filter>
+        </activity>
+
+        <activity
             android:name=".graphics.RoundedBitmapDrawableActivity"
             android:exported="true"
             android:label="Graphics/RoundedBitmapDrawable">
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
index b484606..6792cfb 100644
--- a/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/view/inputmethod/CommitContentSupport.java
@@ -26,10 +26,10 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 import android.webkit.WebView;
-import android.widget.EditText;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import androidx.appcompat.widget.AppCompatEditText;
 import androidx.core.view.inputmethod.EditorInfoCompat;
 import androidx.core.view.inputmethod.InputConnectionCompat;
 import androidx.core.view.inputmethod.InputContentInfoCompat;
@@ -188,17 +188,17 @@
     }
 
     /**
-     * Creates a new instance of {@link EditText} that is configured to specify the given content
-     * MIME types to {@link EditorInfo#contentMimeTypes} so that developers
+     * Creates a new instance of {@link AppCompatEditText} that is configured to specify the given
+     * content MIME types to {@link EditorInfo#contentMimeTypes} so that developers
      * can locally test how the current input method behaves for such content MIME types.
      *
      * @param contentMimeTypes A {@link String} array that indicates the supported content MIME
      *                         types
-     * @return a new instance of {@link EditText}, which specifies
+     * @return a new instance of {@link AppCompatEditText}, which specifies
      * {@link EditorInfo#contentMimeTypes} with the given content
      * MIME types
      */
-    private EditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
+    private AppCompatEditText createEditTextWithContentMimeTypes(String[] contentMimeTypes) {
         final CharSequence hintText;
         final String[] mimeTypes;  // our own copy of contentMimeTypes.
         if (contentMimeTypes == null || contentMimeTypes.length == 0) {
@@ -208,7 +208,7 @@
             hintText = "MIME: " + Arrays.toString(contentMimeTypes);
             mimeTypes = Arrays.copyOf(contentMimeTypes, contentMimeTypes.length);
         }
-        EditText exitText = new EditText(this) {
+        AppCompatEditText exitText = new AppCompatEditText(this) {
             @Override
             public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
                 final InputConnection ic = super.onCreateInputConnection(editorInfo);
diff --git a/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.java b/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.java
new file mode 100644
index 0000000..a6df13d
--- /dev/null
+++ b/samples/Support4Demos/src/main/java/com/example/android/supportv4/widget/NestedScrollActivity3LevelsWithCollapsingToolbar.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 com.example.android.supportv4.widget;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+
+import com.example.android.supportv4.R;
+import com.google.android.material.appbar.CollapsingToolbarLayout;
+
+/**
+ * This activity demonstrates the use of nested scrolling in the v4 support library along with
+ * a collapsing app bar. See the associated layout file for details.
+ */
+public class NestedScrollActivity3LevelsWithCollapsingToolbar extends AppCompatActivity {
+    @Override
+    protected void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.nested_scroll_3_levels_collapsing_toolbar);
+
+        CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar_layout);
+        collapsingToolbar.setTitle(
+                getResources().getString(R.string.nested_scroll_3_levels_collapsing_appbar_title)
+        );
+        collapsingToolbar.setContentScrimColor(getResources().getColor(R.color.color1));
+    }
+}
diff --git a/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml b/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml
new file mode 100644
index 0000000..6551ffb
--- /dev/null
+++ b/samples/Support4Demos/src/main/res/layout/nested_scroll_3_levels_collapsing_toolbar.xml
@@ -0,0 +1,109 @@
+<?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.
+-->
+<!--
+    A NestedScrollView behaves like a ScrollView, but it can be placed into
+    other nested scrolling containers or have other nested scrolling containers
+    placed into it. This demo places a NestedScrollView in a CollapsingToolbarLayout.
+-->
+<androidx.coordinatorlayout.widget.CoordinatorLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:fitsSystemWindows="true"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context="widget.NestedScrollActivity3LevelsWithCollapsingToolbar">
+
+    <!-- App Bar -->
+    <com.google.android.material.appbar.AppBarLayout
+        android:layout_width="match_parent"
+        android:layout_height="200dp">
+
+        <com.google.android.material.appbar.CollapsingToolbarLayout
+            android:id="@+id/collapsing_toolbar_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            app:expandedTitleMarginStart="48dp"
+            app:expandedTitleMarginEnd="64dp"
+            app:layout_scrollFlags="scroll|exitUntilCollapsed">
+
+            <View android:layout_width="match_parent"
+                android:layout_height="200dp"
+                android:background="@color/color2"/>
+
+            <androidx.appcompat.widget.Toolbar
+                android:id="@+id/toolbar"
+                android:layout_width="match_parent"
+                android:layout_height="?attr/actionBarSize"
+                app:layout_collapseMode="pin" />
+        </com.google.android.material.appbar.CollapsingToolbarLayout>
+    </com.google.android.material.appbar.AppBarLayout>
+
+    <!-- Content -->
+    <androidx.core.widget.NestedScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:layout_behavior="@string/appbar_scrolling_view_behavior"
+        android:padding="16dp">
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/nested_scroll_long_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+            <androidx.core.widget.NestedScrollView
+                xmlns:android="http://schemas.android.com/apk/res/android"
+                android:layout_width="match_parent"
+                android:layout_height="400dp"
+                android:padding="16dp">
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="vertical">
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/nested_scroll_short_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+                    <androidx.core.widget.NestedScrollView
+                        android:layout_width="match_parent"
+                        android:layout_height="200dp"
+                        android:padding="16dp">
+                        <TextView
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:text="@string/nested_scroll_long_text"
+                            android:textAppearance="?android:attr/textAppearance"/>
+                    </androidx.core.widget.NestedScrollView>
+                    <TextView
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:text="@string/nested_scroll_short_text"
+                        android:textAppearance="?android:attr/textAppearance"/>
+                </LinearLayout>
+            </androidx.core.widget.NestedScrollView>
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/nested_scroll_long_text"
+                android:textAppearance="?android:attr/textAppearance"/>
+        </LinearLayout>
+    </androidx.core.widget.NestedScrollView>
+</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml b/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
index 214c637..7e3cc1f 100644
--- a/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
+++ b/samples/Support4Demos/src/main/res/menu/swipe_refresh_menu.xml
@@ -14,9 +14,11 @@
      limitations under the License.
 -->
 
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:compat="http://schemas.android.com/apk/res-auto" >
     <item android:id="@+id/force_refresh"
-        android:showAsAction="ifRoom"
+        compat:showAsAction="ifRoom"
         android:icon="@drawable/refresh"
         android:title="Refresh" />
 </menu>
\ No newline at end of file
diff --git a/samples/Support4Demos/src/main/res/values/strings.xml b/samples/Support4Demos/src/main/res/values/strings.xml
index 143b556..3ebe008 100644
--- a/samples/Support4Demos/src/main/res/values/strings.xml
+++ b/samples/Support4Demos/src/main/res/values/strings.xml
@@ -233,6 +233,8 @@
 
     <string name="nested_scroll">Widget/Nested Scrolling</string>
     <string name="nested_scroll_3_levels">Widget/Nested Scrolling (3 levels)</string>
+    <string name="nested_scroll_3_levels_collapsing">Widget/Nested Scrolling (3 levels) with Collapsing Toolbar</string>
+    <string name="nested_scroll_3_levels_collapsing_appbar_title">Collapsing Appbar</string>
 
     <string name="nested_scroll_long_text">This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it. This is some long text. It just keeps going. Look at it. Scroll it. Scroll the nested version of it.</string>
     <string name="nested_scroll_short_text">This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close. This is shorter text. In fact, it was designed to be half as long as the long text, it is very close.</string>
diff --git a/settings.gradle b/settings.gradle
index 3d2ec6a..31e3e3c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -360,6 +360,7 @@
 includeProject(":annotation:annotation-experimental-lint")
 includeProject(":annotation:annotation-experimental-lint-integration-tests", "annotation/annotation-experimental-lint/integration-tests")
 includeProject(":annotation:annotation-sampled")
+includeProject(":appactions:interaction:interaction-proto", [BuildType.MAIN])
 includeProject(":appcompat:appcompat", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-benchmark", [BuildType.MAIN])
 includeProject(":appcompat:appcompat-lint", [BuildType.MAIN])
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 5459765..3f64139 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
@@ -28,6 +28,7 @@
 import android.view.KeyEvent;
 import android.widget.TextView;
 
+import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
@@ -440,6 +441,8 @@
         validateMainActivityXml(xml);
     }
 
+
+    @FlakyTest(bugId = 259299647)
     @Test
     public void testWaitForWindowUpdate() {
         launchTestActivity(WaitTestActivity.class);
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 64af3dd..c88a125 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
@@ -260,7 +260,7 @@
         UiObject2 nestedObject = mDevice.findObject(By.res(TEST_APP, "nested_elements"));
         List<UiObject2> children = nestedObject.getChildren();
         assertEquals(2, children.size());
-        Set<String> childrenClassNames = new HashSet<String>();
+        Set<String> childrenClassNames = new HashSet<>();
         childrenClassNames.add(children.get(0).getClassName());
         childrenClassNames.add(children.get(1).getClassName());
         assertTrue(childrenClassNames.contains("android.widget.TextView"));
@@ -687,7 +687,7 @@
         pinchArea.setGestureMargin(1_000);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
 
@@ -697,7 +697,7 @@
         pinchArea.setGestureMargin(1);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
@@ -715,7 +715,7 @@
         pinchArea.setGestureMargins(1, 1, 1_000, 1_000);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        float scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertEquals(String.format("Expected scale value to be equal to 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), 1f, scaleValueAfterPinch, 0f);
 
@@ -725,7 +725,7 @@
         pinchArea.setGestureMargins(1, 1, 1, 1);
         pinchArea.pinchClose(1f);
         scaleText.wait(Until.textNotEquals("1.0f"), TIMEOUT_MS);
-        scaleValueAfterPinch = Float.valueOf(scaleText.getText());
+        scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
         assertTrue(String.format("Expected scale value to be less than 1f after pinchClose(), "
                 + "but got [%f]", scaleValueAfterPinch), scaleValueAfterPinch < 1f);
     }
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 ee12f2d..c38e665 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
@@ -635,6 +635,8 @@
         UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/scale_factor").text("1.0f"));
 
+        assertTrue(pinchArea.pinchOut(0, 10));
+        assertFalse(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         assertTrue(pinchArea.pinchOut(100, 10));
         assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
@@ -655,6 +657,8 @@
         UiObject expectedScaleText = mDevice.findObject(new UiSelector().resourceId(TEST_APP + ":id"
                 + "/scale_factor").text("1.0f"));
 
+        assertTrue(pinchArea.pinchIn(0, 10));
+        assertFalse(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         assertTrue(pinchArea.pinchIn(100, 10));
         assertTrue(expectedScaleText.waitUntilGone(TIMEOUT_MS));
         float scaleValueAfterPinch = Float.parseFloat(scaleText.getText());
@@ -674,6 +678,10 @@
         assertUiObjectNotFound(() -> noNode.pinchIn(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchOut(100, 10));
         assertThrows(IllegalStateException.class, () -> smallArea.pinchIn(100, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchOut(-1, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchOut(101, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchIn(-1, 10));
+        assertThrows(IllegalArgumentException.class, () -> smallArea.pinchIn(101, 10));
     }
 
     @Test
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
index 0d0c707..cd4c2a2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/AccessibilityNodeInfoDumper.java
@@ -87,11 +87,11 @@
                     dumpNodeRec(child, serializer, i, width, height);
                     child.recycle();
                 } else {
-                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child.toString()));
+                    Log.i(LOGTAG, String.format("Skipping invisible child: %s", child));
                 }
             } else {
                 Log.i(LOGTAG, String.format("Null child %d/%d, parent: %s",
-                        i, count, node.toString()));
+                        i, count, node));
             }
         }
         serializer.endTag("", "node");
@@ -166,11 +166,7 @@
     }
 
     private static String safeCharSeqToString(CharSequence cs) {
-        if (cs == null)
-            return "";
-        else {
-            return stripInvalidXMLChars(cs);
-        }
+        return cs == null ? "" : stripInvalidXMLChars(cs);
     }
 
     private static String stripInvalidXMLChars(CharSequence cs) {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
index 99d6228..2f7e288 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/ByMatcher.java
@@ -34,9 +34,9 @@
 
     private static final String TAG = ByMatcher.class.getSimpleName();
 
-    private UiDevice mDevice;
-    private BySelector mSelector;
-    private boolean mShortCircuit;
+    private final UiDevice mDevice;
+    private final BySelector mSelector;
+    private final boolean mShortCircuit;
 
     /**
      * Constructs a new {@link ByMatcher} instance. Used by
@@ -89,7 +89,7 @@
     static List<AccessibilityNodeInfo> findMatches(UiDevice device, BySelector selector,
             AccessibilityNodeInfo... roots) {
 
-        List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+        List<AccessibilityNodeInfo> ret = new ArrayList<>();
         ByMatcher matcher = new ByMatcher(device, selector, false);
         for (AccessibilityNodeInfo root : roots) {
             ret.addAll(matcher.findMatches(root));
@@ -108,13 +108,13 @@
      */
     private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo root) {
         List<AccessibilityNodeInfo> ret =
-                findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());
+                findMatches(root, 0, 0, new SinglyLinkedList<>());
 
         // If no matches were found
         if (ret.isEmpty()) {
             // Run watchers and retry
             mDevice.runWatchers();
-            ret = findMatches(root, 0, 0, new SinglyLinkedList<PartialMatch>());
+            ret = findMatches(root, 0, 0, new SinglyLinkedList<>());
         }
 
         return ret;
@@ -134,7 +134,7 @@
      */
     private List<AccessibilityNodeInfo> findMatches(AccessibilityNodeInfo node,
             int index, int depth, SinglyLinkedList<PartialMatch> partialMatches) {
-        List<AccessibilityNodeInfo> ret = new ArrayList<AccessibilityNodeInfo>();
+        List<AccessibilityNodeInfo> ret = new ArrayList<>();
 
         // Don't bother searching the subtree if it is not visible
         if (!node.isVisibleToUser()) {
@@ -159,7 +159,7 @@
             AccessibilityNodeInfo child = node.getChild(i);
             if (child == null) {
                 if (!hasNullChild) {
-                    Log.w(TAG, String.format("Node returned null child: %s", node.toString()));
+                    Log.w(TAG, String.format("Node returned null child: %s", node));
                 }
                 hasNullChild = true;
                 Log.w(TAG, String.format("Skipping null child (%s of %s)", i, numChildren));
@@ -211,7 +211,7 @@
     static private class PartialMatch {
         private final int matchDepth;
         private final BySelector matchSelector;
-        private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>();
+        private final List<PartialMatch> partialMatches = new ArrayList<>();
 
         /**
          * Private constructor. Should be instanciated by calling the
@@ -315,7 +315,7 @@
          */
         public boolean finalizeMatch() {
             // Find out which of our child selectors were fully matched
-            Set<BySelector> matches = new HashSet<BySelector>();
+            Set<BySelector> matches = new HashSet<>();
             for (PartialMatch p : partialMatches) {
                 if (p.finalizeMatch()) {
                     matches.add(p.matchSelector);
@@ -346,7 +346,7 @@
 
         /** Returns a new list obtained by prepending {@code data} to {@code rest}. */
         public static <T> SinglyLinkedList<T> prepend(T data, SinglyLinkedList<T> rest) {
-            return new SinglyLinkedList<T>(new Node<T>(data, rest.mHead));
+            return new SinglyLinkedList<>(new Node<>(data, rest.mHead));
         }
 
         @Override
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
index 684466e..8ee007d 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/BySelector.java
@@ -51,7 +51,7 @@
     Integer mMaxDepth;
 
     // Child selectors
-    List<BySelector> mChildSelectors = new LinkedList<BySelector>();
+    List<BySelector> mChildSelectors = new LinkedList<>();
 
 
     /** Clients should not instanciate this class directly. Use the {@link By} factory class instead. */
@@ -79,6 +79,9 @@
         mScrollable    = original.mScrollable;
         mSelected      = original.mSelected;
 
+        mMinDepth = original.mMinDepth;
+        mMaxDepth = original.mMaxDepth;
+
         for (BySelector childSelector : original.mChildSelectors) {
             mChildSelectors.add(new BySelector(childSelector));
         }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
index 87a787c..29aa8c0 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/GestureController.java
@@ -57,7 +57,7 @@
         }
     }
 
-    private UiDevice mDevice;
+    private final UiDevice mDevice;
 
     /** Comparator for sorting PointerGestures by start times. */
     private static final Comparator<PointerGesture> START_TIME_COMPARATOR =
@@ -108,19 +108,19 @@
     public void performGesture(PointerGesture ... gestures) {
         // Initialize pointers
         int count = 0;
-        Map<PointerGesture, Pointer> pointers = new HashMap<PointerGesture, Pointer>();
+        Map<PointerGesture, Pointer> pointers = new HashMap<>();
         for (PointerGesture g : gestures) {
             pointers.put(g, new Pointer(count++, g.start()));
         }
 
         // Initialize MotionEvent arrays
-        List<PointerProperties> properties = new ArrayList<PointerProperties>();
-        List<PointerCoords>     coordinates = new ArrayList<PointerCoords>();
+        List<PointerProperties> properties = new ArrayList<>();
+        List<PointerCoords>     coordinates = new ArrayList<>();
 
         // Track active and pending gestures
-        PriorityQueue<PointerGesture> active = new PriorityQueue<PointerGesture>(gestures.length,
+        PriorityQueue<PointerGesture> active = new PriorityQueue<>(gestures.length,
                 END_TIME_COMPARATOR);
-        PriorityQueue<PointerGesture> pending = new PriorityQueue<PointerGesture>(gestures.length,
+        PriorityQueue<PointerGesture> pending = new PriorityQueue<>(gestures.length,
                 START_TIME_COMPARATOR);
         pending.addAll(Arrays.asList(gestures));
 
@@ -196,11 +196,7 @@
                 active.add(gesture);
             }
 
-            try {
-                Thread.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
-            } catch (InterruptedException e) {
-                Log.e(TAG, "Interrupted while sleeping between events in performGesture");
-            }
+            SystemClock.sleep(MOTION_EVENT_INJECTION_DELAY_MILLIS);
         }
     }
 
@@ -208,8 +204,8 @@
     private static MotionEvent getMotionEvent(long downTime, long eventTime, int action,
             List<PointerProperties> properties, List<PointerCoords> coordinates, int displayId) {
 
-        PointerProperties[] props = properties.toArray(new PointerProperties[properties.size()]);
-        PointerCoords[] coords = coordinates.toArray(new PointerCoords[coordinates.size()]);
+        PointerProperties[] props = properties.toArray(new PointerProperties[0]);
+        PointerCoords[] coords = coordinates.toArray(new PointerCoords[0]);
         final MotionEvent ev = MotionEvent.obtain(
                 downTime, eventTime, action, props.length, props, coords,
                 0, 0, 1, 1, 0, 0, InputDevice.SOURCE_TOUCHSCREEN, 0);
@@ -258,7 +254,7 @@
 
     /** Runnable wrapper around a {@link GestureController#performGesture} call. */
     private class GestureRunnable implements Runnable {
-        private PointerGesture[] mGestures;
+        private final PointerGesture[] mGestures;
 
         public GestureRunnable(PointerGesture[] gestures) {
             mGestures = gestures;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
index 3390113..b69d9d2 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InstrumentationAutomationSupport.java
@@ -30,7 +30,7 @@
  */
 class InstrumentationAutomationSupport implements IAutomationSupport {
 
-    private Instrumentation mInstrumentation;
+    private final Instrumentation mInstrumentation;
 
     InstrumentationAutomationSupport(Instrumentation instrumentation) {
         mInstrumentation = instrumentation;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
index 2377af4..2881d27 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/InteractionController.java
@@ -135,11 +135,7 @@
                 mMask &= ~t.getEventType();
 
                 // Since we're waiting for all events to be matched at least once
-                if (mMask != 0)
-                    return false;
-
-                // all matched
-                return true;
+                return mMask == 0;
             }
 
             // no match yet
@@ -186,19 +182,16 @@
      */
     public boolean sendKeyAndWaitForEvent(final int keyCode, final int metaState,
             final int eventType, long timeout) {
-        Runnable command = new Runnable() {
-            @Override
-            public void run() {
-                final long eventTime = SystemClock.uptimeMillis();
-                KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+        Runnable command = () -> {
+            final long eventTime = SystemClock.uptimeMillis();
+            KeyEvent downEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_DOWN,
+                    keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
+                    InputDevice.SOURCE_KEYBOARD);
+            if (injectEventSync(downEvent)) {
+                KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
                         keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
                         InputDevice.SOURCE_KEYBOARD);
-                if (injectEventSync(downEvent)) {
-                    KeyEvent upEvent = new KeyEvent(eventTime, eventTime, KeyEvent.ACTION_UP,
-                            keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
-                            InputDevice.SOURCE_KEYBOARD);
-                    injectEventSync(upEvent);
-                }
+                injectEventSync(upEvent);
             }
         };
 
@@ -270,13 +263,10 @@
      * @return Runnable
      */
     private Runnable clickRunnable(final int x, final int y) {
-        return new Runnable() {
-            @Override
-            public void run() {
-                if(touchDown(x, y)) {
-                    SystemClock.sleep(REGULAR_CLICK_LENGTH);
-                    touchUp(x, y);
-                }
+        return () -> {
+            if (touchDown(x, y)) {
+                SystemClock.sleep(REGULAR_CLICK_LENGTH);
+                touchUp(x, y);
             }
         };
     }
@@ -290,13 +280,10 @@
      * @return Runnable
      */
     private Runnable longTapRunnable(final int x, final int y) {
-        return new Runnable() {
-            @Override
-            public void run() {
-                if(touchDown(x, y)) {
-                    SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
-                    touchUp(x, y);
-                }
+        return () -> {
+            if (touchDown(x, y)) {
+                SystemClock.sleep(ViewConfiguration.getLongPressTimeout());
+                touchUp(x, y);
             }
         };
     }
@@ -384,16 +371,11 @@
         Log.d(LOG_TAG, "scrollSwipe (" +  downX + ", " + downY + ", " + upX + ", "
                 + upY + ", " + steps +")");
 
-        Runnable command = new Runnable() {
-            @Override
-            public void run() {
-                swipe(downX, downY, upX, upY, steps);
-            }
-        };
+        Runnable command = () -> swipe(downX, downY, upX, upY, steps);
 
         // Collect all accessibility events generated during the swipe command and get the
         // last event
-        ArrayList<AccessibilityEvent> events = new ArrayList<AccessibilityEvent>();
+        ArrayList<AccessibilityEvent> events = new ArrayList<>();
         runAndWaitForEvents(command,
                 new EventCollectingPredicate(AccessibilityEvent.TYPE_VIEW_SCROLLED, events),
                 Configurator.getInstance().getScrollAcknowledgmentTimeout());
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
index 40883a1..9cc5399 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/PointerGesture.java
@@ -26,7 +26,7 @@
  */
 class PointerGesture {
     // The list of actions that make up this gesture.
-    private final Deque<PointerAction> mActions = new ArrayDeque<PointerAction>();
+    private final Deque<PointerAction> mActions = new ArrayDeque<>();
     private final long mDelay;
     private final int mDisplayId;
     private long mDuration;
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
index 452757f..d876a72 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/QueryController.java
@@ -62,30 +62,31 @@
 
     String mLastTraversedText = "";
 
-    private OnAccessibilityEventListener mEventListener = new OnAccessibilityEventListener() {
-        @Override
-        public void onAccessibilityEvent(AccessibilityEvent event) {
-            synchronized (mLock) {
-                switch(event.getEventType()) {
-                    case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
-                        // don't trust event.getText(), check for nulls
-                        if (event.getText() != null && event.getText().size() > 0) {
-                            if(event.getText().get(0) != null)
-                                mLastActivityName = event.getText().get(0).toString();
+    private final OnAccessibilityEventListener mEventListener = event -> {
+        synchronized (mLock) {
+            switch(event.getEventType()) {
+                case AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED:
+                    // don't trust event.getText(), check for nulls
+                    if (event.getText() != null && event.getText().size() > 0) {
+                        if (event.getText().get(0) != null) {
+                            mLastActivityName = event.getText().get(0).toString();
                         }
-                       break;
-                    case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
-                        // don't trust event.getText(), check for nulls
-                        if (event.getText() != null && event.getText().size() > 0)
-                            if(event.getText().get(0) != null)
-                                mLastTraversedText = event.getText().get(0).toString();
-                        if (DEBUG)
-                            Log.d(LOG_TAG, "Last text selection reported: " +
-                                    mLastTraversedText);
-                        break;
-                }
-                mLock.notifyAll();
+                    }
+                    break;
+                case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY:
+                    // don't trust event.getText(), check for nulls
+                    if (event.getText() != null && event.getText().size() > 0) {
+                        if (event.getText().get(0) != null) {
+                            mLastTraversedText = event.getText().get(0).toString();
+                        }
+                    }
+                    if (DEBUG) {
+                        Log.d(LOG_TAG, "Last text selection reported: "
+                                + mLastTraversedText);
+                    }
+                    break;
             }
+            mLock.notifyAll();
         }
     };
 
@@ -339,7 +340,7 @@
                 Log.w(LOG_TAG, String.format(
                         "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
                 if (!hasNullChild) {
-                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+                    Log.w(LOG_TAG, String.format("parent = %s", fromNode));
                 }
                 hasNullChild = true;
                 continue;
@@ -347,7 +348,7 @@
             if (!childNode.isVisibleToUser()) {
                 if (VERBOSE)
                     Log.v(LOG_TAG,
-                            String.format("Skipping invisible child: %s", childNode.toString()));
+                            String.format("Skipping invisible child: %s", childNode));
                 continue;
             }
             AccessibilityNodeInfo retNode = findNodeRegularRecursive(subSelector, childNode, i);
@@ -469,7 +470,7 @@
                 Log.w(LOG_TAG, String.format(
                         "AccessibilityNodeInfo returned a null child (%d of %d)", i, childCount));
                 if (!hasNullChild) {
-                    Log.w(LOG_TAG, String.format("parent = %s", fromNode.toString()));
+                    Log.w(LOG_TAG, String.format("parent = %s", fromNode));
                 }
                 hasNullChild = true;
                 continue;
@@ -477,7 +478,7 @@
             if (!childNode.isVisibleToUser()) {
                 if (DEBUG)
                     Log.d(LOG_TAG,
-                        String.format("Skipping invisible child: %s", childNode.toString()));
+                        String.format("Skipping invisible child: %s", childNode));
                 continue;
             }
             AccessibilityNodeInfo retNode = findNodePatternRecursive(
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java
deleted file mode 100644
index e4dc963..0000000
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Tracer.java
+++ /dev/null
@@ -1,294 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT 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.test.uiautomator;
-
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.PrintWriter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-
-/**
- * Class that creates traces of the calls to the UiAutomator API and outputs the
- * traces either to logcat or a logfile. Each public method in the UiAutomator
- * that needs to be traced should include a call to Tracer.trace in the
- * beginning. Tracing is turned off by default and needs to be enabled
- * explicitly.
- * @hide
- */
-public class Tracer {
-    private static final String UNKNOWN_METHOD_STRING = "(unknown method)";
-    private static final String UIAUTOMATOR_PACKAGE = Tracer.class.getPackage().getName();
-    private static final int CALLER_LOCATION = 6;
-    private static final int METHOD_TO_TRACE_LOCATION = 5;
-    private static final int MIN_STACK_TRACE_LENGTH = 7;
-
-    /**
-     * Enum that determines where the trace output goes. It can go to either
-     * logcat, log file or both.
-     */
-    public enum Mode {
-        NONE,
-        FILE,
-        LOGCAT,
-        ALL
-    }
-
-    private interface TracerSink {
-        public void log(String message);
-
-        public void close();
-    }
-
-    private static class FileSink implements TracerSink {
-        private final PrintWriter mOut;
-        private final SimpleDateFormat mDateFormat;
-
-        public FileSink(File file) throws FileNotFoundException {
-            mOut = new PrintWriter(file);
-            mDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US);
-        }
-
-        @Override
-        public void log(String message) {
-            mOut.printf("%s %s\n", mDateFormat.format(new Date()), message);
-        }
-
-        @Override
-        public void close() {
-            mOut.close();
-        }
-    }
-
-    static class LogcatSink implements TracerSink {
-
-        private static final String LOGCAT_TAG = "UiAutomatorTrace";
-
-        @Override
-        public void log(String message) {
-            Log.i(LOGCAT_TAG, message);
-        }
-
-        @Override
-        public void close() {
-            // nothing is needed
-        }
-    }
-
-    private Mode mCurrentMode = Mode.NONE;
-    private final List<TracerSink> mSinks = new ArrayList<>();
-    private File mOutputFile;
-
-    private static Tracer mInstance = null;
-
-    /**
-     * Returns a reference to an instance of the tracer. Useful to set the
-     * parameters before the trace is collected.
-     *
-     * @return
-     */
-    @NonNull
-    public static Tracer getInstance() {
-        if (mInstance == null) {
-            mInstance = new Tracer();
-        }
-        return mInstance;
-    }
-
-    /**
-     * Sets where the trace output will go. Can be either be logcat or a file or
-     * both. Setting this to NONE will turn off tracing.
-     *
-     * @param mode
-     */
-    public void setOutputMode(@NonNull Mode mode) {
-        closeSinks();
-        mCurrentMode = mode;
-        try {
-            switch (mode) {
-                case FILE:
-                    if (mOutputFile == null) {
-                        throw new IllegalArgumentException("Please provide a filename before " +
-                                "attempting write trace to a file");
-                    }
-                    mSinks.add(new FileSink(mOutputFile));
-                    break;
-                case LOGCAT:
-                    mSinks.add(new LogcatSink());
-                    break;
-                case ALL:
-                    mSinks.add(new LogcatSink());
-                    if (mOutputFile == null) {
-                        throw new IllegalArgumentException("Please provide a filename before " +
-                                "attempting write trace to a file");
-                    }
-                    mSinks.add(new FileSink(mOutputFile));
-                    break;
-                default:
-                    break;
-            }
-        } catch (FileNotFoundException e) {
-            Log.w("Tracer", "Could not open log file: " + e.getMessage());
-        }
-    }
-
-    private void closeSinks() {
-        for (TracerSink sink : mSinks) {
-            sink.close();
-        }
-        mSinks.clear();
-    }
-
-    /**
-     * Sets the name of the log file where tracing output will be written if the
-     * tracer is set to write to a file.
-     *
-     * @param filename name of the log file.
-     */
-    public void setOutputFilename(@NonNull String filename) {
-        mOutputFile = new File(filename);
-    }
-
-    private void doTrace(Object[] arguments) {
-        if (mCurrentMode == Mode.NONE) {
-            return;
-        }
-
-        String caller = getCaller();
-        if (caller == null) {
-            return;
-        }
-
-        log(String.format("%s (%s)", caller, join(", ", arguments)));
-    }
-
-    private void log(String message) {
-        for (TracerSink sink : mSinks) {
-            sink.log(message);
-        }
-    }
-
-    /**
-     * Queries whether the tracing is enabled.
-     * @return true if tracing is enabled, false otherwise.
-     */
-    public boolean isTracingEnabled() {
-        return mCurrentMode != Mode.NONE;
-    }
-
-    /**
-     * Public methods in the UiAutomator should call this function to generate a
-     * trace. The trace will include the method that's is being called, it's
-     * arguments and where in the user's code the method is called from. If a
-     * public method is called internally from UIAutomator then this will not
-     * output a trace entry. Only calls from outside the UiAutomator package will
-     * produce output.
-     *
-     * Special note about array arguments. You can safely pass arrays of reference types
-     * to this function. Like String[] or Integer[]. The trace function will print their
-     * contents by calling toString() on each of the elements. This will not work for
-     * array of primitive types like int[] or float[]. Before passing them to this function
-     * convert them to arrays of reference types manually. Example: convert int[] to Integer[].
-     *
-     * @param arguments arguments of the method being traced.
-     */
-    public static void trace(@NonNull Object... arguments) {
-        Tracer.getInstance().doTrace(arguments);
-    }
-
-    private static String join(String separator, Object[] strings) {
-        if (strings.length == 0) {
-            return "";
-        }
-
-        StringBuilder builder = new StringBuilder(objectToString(strings[0]));
-        for (int i = 1; i < strings.length; i++) {
-            builder.append(separator);
-            builder.append(objectToString(strings[i]));
-        }
-        return builder.toString();
-    }
-
-    /**
-     * Special toString method to handle arrays. If the argument is a normal object then this will
-     * return normal output of obj.toString(). If the argument is an array this will return a
-     * string representation of the elements of the array.
-     *
-     * This method will not work for arrays of primitive types. Arrays of primitive types are
-     * expected to be converted manually by the caller. If the array is not converter then
-     * this function will only output "[...]" instead of the contents of the array.
-     *
-     * @param obj object to convert to a string
-     * @return String representation of the object.
-     */
-    private static String objectToString(Object obj) {
-        if (obj.getClass().isArray()) {
-            if (obj instanceof Object[]) {
-                return Arrays.deepToString((Object[]) obj);
-            } else {
-                return "[...]";
-            }
-        } else {
-            return obj.toString();
-        }
-    }
-
-    /**
-     * This method outputs which UiAutomator method was called and where in the
-     * user code it was called from. If it can't decide which method is called
-     * it will output "(unknown method)". If the method was called from inside
-     * the UiAutomator then it returns null.
-     *
-     * @return name of the method called and where it was called from. Null if
-     *         method was called from inside UiAutomator.
-     */
-    private static String getCaller() {
-        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
-        if (stackTrace.length < MIN_STACK_TRACE_LENGTH) {
-            return UNKNOWN_METHOD_STRING;
-        }
-
-        StackTraceElement caller = stackTrace[METHOD_TO_TRACE_LOCATION];
-        StackTraceElement previousCaller = stackTrace[CALLER_LOCATION];
-
-        if (previousCaller.getClassName().startsWith(UIAUTOMATOR_PACKAGE)) {
-            return null;
-        }
-
-        int indexOfDot = caller.getClassName().lastIndexOf('.');
-        if (indexOfDot < 0) {
-            indexOfDot = 0;
-        }
-
-        if (indexOfDot + 1 >= caller.getClassName().length()) {
-            return UNKNOWN_METHOD_STRING;
-        }
-
-        String shortClassName = caller.getClassName().substring(indexOfDot + 1);
-        return String.format("%s.%s from %s() at %s:%d", shortClassName, caller.getMethodName(),
-                previousCaller.getMethodName(), previousCaller.getFileName(),
-                previousCaller.getLineNumber());
-    }
-}
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
index 2ec04ba..274e7d9 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorInstrumentationTestRunner.java
@@ -29,26 +29,6 @@
  */
 public class UiAutomatorInstrumentationTestRunner extends InstrumentationTestRunner {
 
-    @Override
-    public void onStart() {
-        // process runner arguments before test starts
-        String traceType = getArguments().getString("traceOutputMode");
-        if(traceType != null) {
-            Tracer.Mode mode = Tracer.Mode.valueOf(Tracer.Mode.class, traceType);
-            if (mode == Tracer.Mode.FILE || mode == Tracer.Mode.ALL) {
-                String filename = getArguments().getString("traceLogFilename");
-                if (filename == null) {
-                    throw new RuntimeException("Name of log file not specified. " +
-                            "Please specify it using traceLogFilename parameter");
-                }
-                Tracer.getInstance().setOutputFilename(filename);
-            }
-            Tracer.getInstance().setOutputMode(mode);
-        }
-        super.onStart();
-    }
-
-
     /**
      * Perform initialization specific to UiAutomator test. It sets up the test case so that
      * it can access the UiDevice and gives it access to the command line arguments.
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
index 914f9b3..c3e7bf1 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiAutomatorTestCase.java
@@ -81,7 +81,7 @@
         if (monkeyVal != null) {
             // only if the monkey key is specified, we alter the state of monkey
             // else we should leave things as they are.
-            getUiDevice().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal));
+            getUiDevice().getUiAutomation().setRunAsMonkey(Boolean.parseBoolean(monkeyVal));
         }
     }
 
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
index e1c2e70..d2e6cbf 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiDevice.java
@@ -147,7 +147,7 @@
     @Override
     @NonNull
     public List<UiObject2> findObjects(@NonNull BySelector selector) {
-        List<UiObject2> ret = new ArrayList<UiObject2>();
+        List<UiObject2> ret = new ArrayList<>();
         for (AccessibilityNodeInfo node : ByMatcher.findMatches(this, selector, getWindowRoots())) {
             ret.add(new UiObject2(this, selector, node));
         }
@@ -165,7 +165,7 @@
      * was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -180,8 +180,8 @@
     public <U> U performActionAndWait(@NonNull Runnable action,
             @NonNull EventCondition<U> condition, long timeout) {
         AccessibilityEvent event = null;
-        Log.d(TAG, String.format("Performing action %s and waiting %dms for condition %s.",
-                action, timeout, condition));
+        Log.d(TAG, String.format("Performing action %s and waiting %dms for %s.", action, timeout,
+                condition));
         try {
             event = getUiAutomation().executeAndWaitForEvent(
                 action, new EventForwardingFilter(condition), timeout);
@@ -200,7 +200,7 @@
     /** Proxy class which acts as an {@link AccessibilityEventFilter} and forwards calls to an
      * {@link EventCondition} instance. */
     private static class EventForwardingFilter implements AccessibilityEventFilter {
-        private EventCondition<?> mCondition;
+        private final EventCondition<?> mCondition;
 
         public EventForwardingFilter(EventCondition<?> condition) {
             mCondition = condition;
@@ -898,20 +898,13 @@
                 return false;
             }
         }
-        Runnable emptyRunnable = new Runnable() {
-            @Override
-            public void run() {
+        Runnable emptyRunnable = () -> {};
+        AccessibilityEventFilter checkWindowUpdate = t -> {
+            if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
+                return packageName == null || (t.getPackageName() != null
+                        && packageName.contentEquals(t.getPackageName()));
             }
-        };
-        AccessibilityEventFilter checkWindowUpdate = new AccessibilityEventFilter() {
-            @Override
-            public boolean accept(AccessibilityEvent t) {
-                if (t.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
-                    return packageName == null || (t.getPackageName() != null
-                            && packageName.contentEquals(t.getPackageName()));
-                }
-                return false;
-            }
+            return false;
         };
         Log.d(TAG, String.format("Waiting %dms for window update of package %s.", timeout,
                 packageName));
@@ -1070,7 +1063,7 @@
                 roots.add(root);
             }
         }
-        return roots.toArray(new AccessibilityNodeInfo[roots.size()]);
+        return roots.toArray(new AccessibilityNodeInfo[0]);
     }
 
     Instrumentation getInstrumentation() {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
index 974fb8f..780f82d 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject.java
@@ -895,10 +895,7 @@
      */
     public boolean waitForExists(long timeout) {
         Log.d(TAG, String.format("Waiting %dms for %s.", timeout, mUiSelector));
-        if(findAccessibilityNodeInfo(timeout) != null) {
-            return true;
-        }
-        return false;
+        return findAccessibilityNodeInfo(timeout) != null;
     }
 
     /**
@@ -964,8 +961,9 @@
      * @throws UiObjectNotFoundException
      */
     public boolean pinchOut(int percent, int steps) throws UiObjectNotFoundException {
-        // make value between 1 and 100
-        percent = (percent < 0) ? 1 : (percent > 100) ? 100 : percent;
+        if (percent < 0 || percent > 100) {
+            throw new IllegalArgumentException("Percent must be between 0 and 100");
+        }
         float percentage = percent / 100f;
 
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
@@ -1001,8 +999,9 @@
      * @throws UiObjectNotFoundException
      */
     public boolean pinchIn(int percent, int steps) throws UiObjectNotFoundException {
-        // make value between 1 and 100
-        percent = (percent < 0) ? 0 : (percent > 100) ? 100 : percent;
+        if (percent < 0 || percent > 100) {
+            throw new IllegalArgumentException("Percent must be between 0 and 100");
+        }
         float percentage = percent / 100f;
 
         AccessibilityNodeInfo node = findAccessibilityNodeInfo(mConfig.getWaitForSelectorTimeout());
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cb83ac0ab..cc5a089 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -153,7 +153,7 @@
      * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull UiObject2Condition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -166,7 +166,7 @@
      * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
-        Log.d(TAG, String.format("Waiting %dms for condition %s.", timeout, condition));
+        Log.d(TAG, String.format("Waiting %dms for %s.", timeout, condition));
         return mWaitMixin.wait(condition, timeout);
     }
 
@@ -494,8 +494,8 @@
      */
     public <U> U clickAndWait(@NonNull EventCondition<U> condition, long timeout) {
         Point center = getVisibleCenter();
-        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for condition %s.",
-                center.x, center.y, timeout, condition));
+        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for %s.", center.x,
+                center.y, timeout, condition));
         return mGestureController.performGestureAndWait(condition, timeout,
                 Gestures.click(center, getDisplayId()));
     }
@@ -511,8 +511,8 @@
     public <U> U clickAndWait(@NonNull Point point, @NonNull EventCondition<U> condition,
             long timeout) {
         clipToGestureBounds(point);
-        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for condition %s.",
-                point.x, point.y, timeout, condition));
+        Log.d(TAG, String.format("Clicking on (%d, %d) and waiting %dms for %s.", point.x,
+                point.y, timeout, condition));
         return mGestureController.performGestureAndWait(
                 condition, timeout, Gestures.click(point, getDisplayId()));
     }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
index 8dcfbf5..1db9068 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiScrollable.java
@@ -86,10 +86,7 @@
      * @return true if found else false
      */
     protected boolean exists(@NonNull UiSelector selector) {
-        if(getQueryController().findAccessibilityNodeInfo(selector) != null) {
-            return true;
-        }
-        return false;
+        return getQueryController().findAccessibilityNodeInfo(selector) != null;
     }
 
     /**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
index 53c6471..5cb978a 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiSelector.java
@@ -63,7 +63,7 @@
     static final int SELECTOR_CHECKABLE = 30;
     static final int SELECTOR_RESOURCE_ID_REGEX = 31;
 
-    private SparseArray<Object> mSelectorAttributes = new SparseArray<Object>();
+    private SparseArray<Object> mSelectorAttributes = new SparseArray<>();
 
     public UiSelector() {
     }
@@ -109,9 +109,7 @@
      */
     @NonNull
     public UiSelector text(@NonNull String text) {
-        if (text == null) {
-            throw new IllegalArgumentException("text cannot be null");
-        }
+        checkNotNull(text, "text cannot be null");
         return buildSelector(SELECTOR_TEXT, text);
     }
 
@@ -127,9 +125,7 @@
      */
     @NonNull
     public UiSelector textMatches(@NonNull String regex) {
-        if (regex == null) {
-            throw new IllegalArgumentException("regex cannot be null");
-        }
+        checkNotNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
@@ -144,9 +140,7 @@
      */
     @NonNull
     public UiSelector textStartsWith(@NonNull String text) {
-        if (text == null) {
-            throw new IllegalArgumentException("text cannot be null");
-        }
+        checkNotNull(text, "text cannot be null");
         return buildSelector(SELECTOR_START_TEXT, text);
     }
 
@@ -161,9 +155,7 @@
      */
     @NonNull
     public UiSelector textContains(@NonNull String text) {
-        if (text == null) {
-            throw new IllegalArgumentException("text cannot be null");
-        }
+        checkNotNull(text, "text cannot be null");
         return buildSelector(SELECTOR_CONTAINS_TEXT, text);
     }
 
@@ -176,9 +168,7 @@
      */
     @NonNull
     public UiSelector className(@NonNull String className) {
-        if (className == null) {
-            throw new IllegalArgumentException("className cannot be null");
-        }
+        checkNotNull(className, "className cannot be null");
         return buildSelector(SELECTOR_CLASS, className);
     }
 
@@ -191,9 +181,7 @@
      */
     @NonNull
     public UiSelector classNameMatches(@NonNull String regex) {
-        if (regex == null) {
-            throw new IllegalArgumentException("regex cannot be null");
-        }
+        checkNotNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_CLASS_REGEX, Pattern.compile(regex));
     }
 
@@ -206,9 +194,7 @@
      */
     @NonNull
     public <T> UiSelector className(@NonNull Class<T> type) {
-        if (type == null) {
-            throw new IllegalArgumentException("type cannot be null");
-        }
+        checkNotNull(type, "type cannot be null");
         return buildSelector(SELECTOR_CLASS, type.getName());
     }
 
@@ -230,9 +216,7 @@
      */
     @NonNull
     public UiSelector description(@NonNull String desc) {
-        if (desc == null) {
-            throw new IllegalArgumentException("desc cannot be null");
-        }
+        checkNotNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_DESCRIPTION, desc);
     }
 
@@ -252,9 +236,7 @@
      */
     @NonNull
     public UiSelector descriptionMatches(@NonNull String regex) {
-        if (regex == null) {
-            throw new IllegalArgumentException("regex cannot be null");
-        }
+        checkNotNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
@@ -276,9 +258,7 @@
      */
     @NonNull
     public UiSelector descriptionStartsWith(@NonNull String desc) {
-        if (desc == null) {
-            throw new IllegalArgumentException("desc cannot be null");
-        }
+        checkNotNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_START_DESCRIPTION, desc);
     }
 
@@ -300,9 +280,7 @@
      */
     @NonNull
     public UiSelector descriptionContains(@NonNull String desc) {
-        if (desc == null) {
-            throw new IllegalArgumentException("desc cannot be null");
-        }
+        checkNotNull(desc, "desc cannot be null");
         return buildSelector(SELECTOR_CONTAINS_DESCRIPTION, desc);
     }
 
@@ -314,9 +292,7 @@
      */
     @NonNull
     public UiSelector resourceId(@NonNull String id) {
-        if (id == null) {
-            throw new IllegalArgumentException("id cannot be null");
-        }
+        checkNotNull(id, "id cannot be null");
         return buildSelector(SELECTOR_RESOURCE_ID, id);
     }
 
@@ -329,9 +305,7 @@
      */
     @NonNull
     public UiSelector resourceIdMatches(@NonNull String regex) {
-        if (regex == null) {
-            throw new IllegalArgumentException("regex cannot be null");
-        }
+        checkNotNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_RESOURCE_ID_REGEX, Pattern.compile(regex));
     }
 
@@ -563,9 +537,7 @@
      */
     @NonNull
     public UiSelector childSelector(@NonNull UiSelector selector) {
-        if (selector == null) {
-            throw new IllegalArgumentException("selector cannot be null");
-        }
+        checkNotNull(selector, "selector cannot be null");
         return buildSelector(SELECTOR_CHILD, selector);
     }
 
@@ -589,9 +561,7 @@
      */
     @NonNull
     public UiSelector fromParent(@NonNull UiSelector selector) {
-        if (selector == null) {
-            throw new IllegalArgumentException("selector cannot be null");
-        }
+        checkNotNull(selector, "selector cannot be null");
         return buildSelector(SELECTOR_PARENT, selector);
     }
 
@@ -604,9 +574,7 @@
      */
     @NonNull
     public UiSelector packageName(@NonNull String name) {
-        if (name == null) {
-            throw new IllegalArgumentException("name cannot be null");
-        }
+        checkNotNull(name, "name cannot be null");
         return buildSelector(SELECTOR_PACKAGE_NAME, name);
     }
 
@@ -619,9 +587,7 @@
      */
     @NonNull
     public UiSelector packageNameMatches(@NonNull String regex) {
-        if (regex == null) {
-            throw new IllegalArgumentException("regex cannot be null");
-        }
+        checkNotNull(regex, "regex cannot be null");
         return buildSelector(SELECTOR_PACKAGE_NAME_REGEX, Pattern.compile(regex));
     }
 
@@ -855,12 +821,12 @@
         // matched attributes - now check for matching instance number
         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_INSTANCE) >= 0) {
             currentSelectorInstance =
-                    (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
+                    (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_INSTANCE);
         }
 
         // instance is required. Add count if not already counting
         if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_COUNT) >= 0) {
-            currentSelectorCounter = (Integer)mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
+            currentSelectorCounter = (Integer) mSelectorAttributes.get(UiSelector.SELECTOR_COUNT);
         }
 
         // Verify
@@ -880,39 +846,24 @@
      * @return true if is leaf.
      */
     boolean isLeaf() {
-        if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0 &&
-                mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
-            return true;
-        }
-        return false;
+        return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0
+                && mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0;
     }
 
     boolean hasChildSelector() {
-        if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) < 0) {
-            return false;
-        }
-        return true;
+        return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CHILD) >= 0;
     }
 
     boolean hasPatternSelector() {
-        if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) < 0) {
-            return false;
-        }
-        return true;
+        return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PATTERN) >= 0;
     }
 
     boolean hasContainerSelector() {
-        if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) < 0) {
-            return false;
-        }
-        return true;
+        return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_CONTAINER) >= 0;
     }
 
     boolean hasParentSelector() {
-        if (mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) < 0) {
-            return false;
-        }
-        return true;
+        return mSelectorAttributes.indexOfKey(UiSelector.SELECTOR_PARENT) >= 0;
     }
 
     /**
@@ -946,7 +897,7 @@
 
     String dumpToString(boolean all) {
         StringBuilder builder = new StringBuilder();
-        builder.append(UiSelector.class.getSimpleName() + "[");
+        builder.append(UiSelector.class.getSimpleName()).append("[");
         final int criterionCount = mSelectorAttributes.size();
         for (int i = 0; i < criterionCount; i++) {
             if (i > 0) {
@@ -1064,11 +1015,18 @@
                     builder.append("RESOURCE_ID_REGEX=").append(mSelectorAttributes.valueAt(i));
                     break;
                 default:
-                    builder.append("UNDEFINED=" + criterion + " ").append(
+                    builder.append("UNDEFINED=").append(criterion).append(" ").append(
                             mSelectorAttributes.valueAt(i));
             }
         }
         builder.append("]");
         return builder.toString();
     }
+
+    private static <T> T checkNotNull(T value, @NonNull String message) {
+        if (value == null) {
+            throw new NullPointerException(message);
+        }
+        return value;
+    }
 }
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index c680d4f..5a6d9b5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -44,6 +44,12 @@
             Boolean apply(Searchable container) {
                 return !container.hasObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[gone=%s]", selector);
+            }
         };
     }
 
@@ -58,6 +64,12 @@
             Boolean apply(Searchable container) {
                 return container.hasObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[hasObject=%s]", selector);
+            }
         };
     }
 
@@ -72,6 +84,12 @@
             UiObject2 apply(Searchable container) {
                 return container.findObject(selector);
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[findObject=%s]", selector);
+            }
         };
     }
 
@@ -87,6 +105,12 @@
                 List<UiObject2> ret = container.findObjects(selector);
                 return ret.isEmpty() ? null : ret;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("SearchCondition[findObjects=%s]", selector);
+            }
         };
     }
 
@@ -105,6 +129,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isCheckable() == isCheckable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[checkable=%b]", isCheckable);
+            }
         };
     }
 
@@ -120,6 +150,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isChecked() == isChecked;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[checked=%b]", isChecked);
+            }
         };
     }
 
@@ -135,6 +171,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isClickable() == isClickable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[clickable=%b]", isClickable);
+            }
         };
     }
 
@@ -150,6 +192,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isEnabled() == isEnabled;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[enabled=%b]", isEnabled);
+            }
         };
     }
 
@@ -165,6 +213,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isFocusable() == isFocusable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[focusable=%b]", isFocusable);
+            }
         };
     }
 
@@ -180,6 +234,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isFocused() == isFocused;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[focused=%b]", isFocused);
+            }
         };
     }
 
@@ -195,6 +255,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isLongClickable() == isLongClickable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[longClickable=%b]", isLongClickable);
+            }
         };
     }
 
@@ -210,6 +276,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isScrollable() == isScrollable;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[scrollable=%b]", isScrollable);
+            }
         };
     }
 
@@ -225,6 +297,12 @@
             Boolean apply(UiObject2 object) {
                 return object.isSelected() == isSelected;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[selected=%b]", isSelected);
+            }
         };
     }
 
@@ -240,6 +318,12 @@
                 String desc = object.getContentDescription();
                 return regex.matcher(desc != null ? desc : "").matches();
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[descMatches='%s']", regex);
+            }
         };
     }
 
@@ -299,6 +383,12 @@
                 String text = object.getText();
                 return regex.matcher(text != null ? text : "").matches();
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[textMatches='%s']", regex);
+            }
         };
     }
 
@@ -321,6 +411,12 @@
             Boolean apply(UiObject2 object) {
                 return !text.equals(object.getText());
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("UiObject2Condition[textNotEquals='%s']", text);
+            }
         };
     }
 
@@ -380,6 +476,12 @@
             Boolean getResult() {
                 return mMask == 0;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("EventCondition[newWindow]");
+            }
         };
     }
 
@@ -449,6 +551,12 @@
                 // the end and return true.
                 return mResult == null || mResult;
             }
+
+            @NonNull
+            @Override
+            public String toString() {
+                return String.format("EventCondition[scrollFinished=%s]", direction.name());
+            }
         };
     }
 }
diff --git a/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt b/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
index d87011e..5f975123 100644
--- a/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
+++ b/text/text/src/main/java/androidx/compose/ui/text/android/InternalPlatformTextApi.kt
@@ -25,4 +25,5 @@
     AnnotationTarget.FUNCTION,
     AnnotationTarget.PROPERTY
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class InternalPlatformTextApi
\ No newline at end of file
diff --git a/tv/tv-foundation/api/current.txt b/tv/tv-foundation/api/current.txt
index 5bc200c..a4ee192 100644
--- a/tv/tv-foundation/api/current.txt
+++ b/tv/tv-foundation/api/current.txt
@@ -149,6 +149,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -261,6 +263,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
diff --git a/tv/tv-foundation/api/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index ebce6ac..ba5e62b 100644
--- a/tv/tv-foundation/api/public_plus_experimental_current.txt
+++ b/tv/tv-foundation/api/public_plus_experimental_current.txt
@@ -1,7 +1,7 @@
 // Signature format: 4.0
 package androidx.tv.foundation {
 
-  @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") public @interface ExperimentalTvFoundationApi {
+  @kotlin.RequiresOptIn(message="This tv-foundation API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvFoundationApi {
   }
 
   public final class PivotOffsets {
@@ -154,6 +154,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -267,6 +269,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
diff --git a/tv/tv-foundation/api/restricted_current.txt b/tv/tv-foundation/api/restricted_current.txt
index 5bc200c..a4ee192 100644
--- a/tv/tv-foundation/api/restricted_current.txt
+++ b/tv/tv-foundation/api/restricted_current.txt
@@ -149,6 +149,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
@@ -261,6 +263,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int firstVisibleItemIndex;
     property public final int firstVisibleItemScrollOffset;
     property public final androidx.compose.foundation.interaction.InteractionSource interactionSource;
diff --git a/tv/tv-foundation/build.gradle b/tv/tv-foundation/build.gradle
index 2584bf1..29362d9 100644
--- a/tv/tv-foundation/build.gradle
+++ b/tv/tv-foundation/build.gradle
@@ -30,9 +30,9 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    def composeVersion = '1.2.1'
+    def composeVersion = '1.3.0-rc01'
 
-    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.annotation:annotation:1.5.0")
     api("androidx.compose.animation:animation:$composeVersion")
     api("androidx.compose.runtime:runtime:$composeVersion")
     api("androidx.compose.ui:ui:$composeVersion")
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
index c0efb11..aed7e89 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridAnimateItemPlacementTest.kt
@@ -27,6 +27,7 @@
 import androidx.compose.foundation.layout.requiredHeightIn
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.layout.requiredWidthIn
+import androidx.compose.foundation.lazy.grid.items
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -1142,6 +1143,39 @@
         }
     }
 
+    @Test
+    fun itemWithSpecsIsMovingOut() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyGrid(1, maxSize = itemSizeDp * 2) {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            val listSize = itemSize * 2
+            val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, IntOffset>>().apply {
+                add(0 to AxisIntOffset(0, 0))
+                if (item1Offset < listSize) {
+                    add(1 to AxisIntOffset(0, item1Offset))
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
     private fun AxisIntOffset(crossAxis: Int, mainAxis: Int) =
         if (isVertical) IntOffset(crossAxis, mainAxis) else IntOffset(mainAxis, crossAxis)
 
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
index 5e29562..336c09e 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyGridTest.kt
@@ -1058,6 +1058,39 @@
             assertThat(state.numMeasurePasses).isEqualTo(1)
         }
     }
+
+    @Test
+    fun fillingFullSize_nextItemIsNotComposed() {
+        val state = TvLazyGridState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyGrid(
+                1,
+                Modifier
+                    .testTag(LazyGridTag)
+                    .mainAxisSize(itemSize),
+                state
+            ) {
+                items(3) { index ->
+                    Box(Modifier.size(itemSize).testTag("$index"))
+                }
+            }
+        }
+
+        repeat(3) { index ->
+            rule.onNodeWithTag("$index")
+                .assertIsDisplayed()
+            rule.onNodeWithTag("${index + 1}")
+                .assertDoesNotExist()
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
 }
 
 internal fun IntegerSubject.isEqualTo(expected: Int, tolerance: Int) {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
index f7fd6c7..b14edf9 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/grid/LazyScrollTest.kt
@@ -243,6 +243,33 @@
         assertSpringAnimation(toIndex = 0, toOffset = 40, fromIndex = 8)
     }
 
+    @Test
+    fun canScrollForward() = runBlocking {
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isFalse()
+    }
+
+    @Test
+    fun canScrollBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 6)
+        assertThat(state.canScrollForward).isFalse()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
+    @Test
+    fun canScrollForwardAndBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(10)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(10)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
     private fun assertSpringAnimation(
         toIndex: Int,
         toOffset: Int = 0,
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
index d451964..194c128 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/BaseLazyListTestWithOrientation.kt
@@ -75,6 +75,13 @@
             this.fillMaxHeight()
         }
 
+    fun TvLazyListItemScope.fillParentMaxMainAxis() =
+        if (vertical) {
+            Modifier.fillParentMaxHeight()
+        } else {
+            Modifier.fillParentMaxWidth()
+        }
+
     fun TvLazyListItemScope.fillParentMaxCrossAxis() =
         if (vertical) {
             Modifier.fillParentMaxWidth()
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
index 45825f43..d9f8e64 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
@@ -56,6 +56,7 @@
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.tv.foundation.PivotOffsets
@@ -349,6 +350,7 @@
             .assertPositionInRootIsEqualTo(30.dp, 50.dp)
     }
 
+    @FlakyTest(bugId = 259297305)
     @Test
     fun removalWithMutableStateListOf() {
         val items = mutableStateListOf("1", "2", "3")
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
index 07b5201..116fbea 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListAnimateItemPlacementTest.kt
@@ -111,7 +111,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -137,7 +137,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -160,7 +160,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -195,7 +195,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -233,7 +233,7 @@
                 maxSize = itemSizeDp * 5
             ) {
                 items(listOf(0, 1, 2, 3), key = { it }) {
-                    item(it, size = if (it == 1) size else itemSizeDp)
+                    Item(it, size = if (it == 1) size else itemSizeDp)
                 }
             }
         }
@@ -275,7 +275,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
+                    Item(it, animSpec = if (it == 1 || it == 3) AnimSpec else null)
                 }
             }
         }
@@ -303,7 +303,7 @@
             LazyList {
                 items(list, key = { it }) {
                     val duration = if (it == 1 || it == 3) Duration * 2 else Duration
-                    item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
+                    Item(it, animSpec = tween(duration.toInt(), easing = LinearEasing))
                 }
             }
         }
@@ -331,8 +331,8 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
-                    item(it + 1)
+                    Item(it)
+                    Item(it + 1)
                 }
             }
         }
@@ -365,8 +365,8 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
-                    item(it + 1, animSpec = null)
+                    Item(it)
+                    Item(it + 1, animSpec = null)
                 }
             }
         }
@@ -396,7 +396,7 @@
                 maxSize = itemSizeDp * 5
             ) {
                 items(listOf(1, 2, 3), key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -428,7 +428,7 @@
         rule.setContent {
             LazyList(maxSize = itemSizeDp * 3) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -473,7 +473,7 @@
         rule.setContent {
             LazyList(maxSize = itemSizeDp * 3f, startIndex = 3) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -518,7 +518,7 @@
         rule.setContent {
             LazyList(arrangement = Arrangement.spacedBy(spacingDp)) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -547,7 +547,7 @@
                 arrangement = Arrangement.spacedBy(spacingDp)
             ) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -599,7 +599,7 @@
                 arrangement = Arrangement.spacedBy(spacingDp)
             ) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -648,7 +648,7 @@
                 items(list, key = { it }) {
                     val size =
                         if (it == 3) itemSize2Dp else if (it == 1) itemSize3Dp else itemSizeDp
-                    item(it, size = size)
+                    Item(it, size = size)
                 }
             }
         }
@@ -708,7 +708,7 @@
                 items(list, key = { it }) {
                     val size =
                         if (it == 0) itemSize2Dp else if (it == 4) itemSize3Dp else itemSizeDp
-                    item(it, size = size)
+                    Item(it, size = size)
                 }
             }
         }
@@ -769,7 +769,7 @@
                 items(listOf(1, 2, 3), key = { it }) {
                     val crossAxisSize =
                         if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
-                    item(it, crossAxisSize = crossAxisSize)
+                    Item(it, crossAxisSize = crossAxisSize)
                 }
             }
         }
@@ -821,7 +821,7 @@
                     listOf(1, 2, 3).forEach {
                         val crossAxisSize =
                             if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
-                        item(it, crossAxisSize = crossAxisSize)
+                        Item(it, crossAxisSize = crossAxisSize)
                     }
                 }
             }
@@ -863,7 +863,7 @@
                     items(listOf(1, 2, 3), key = { it }) {
                         val crossAxisSize =
                             if (it == 1) itemSizeDp else if (it == 2) itemSize2Dp else itemSize3Dp
-                        item(it, crossAxisSize = crossAxisSize)
+                        Item(it, crossAxisSize = crossAxisSize)
                     }
                 }
             }
@@ -911,7 +911,7 @@
         rule.setContent {
             LazyList(startPadding = startPaddingDp, endPadding = endPaddingDp) {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -949,7 +949,7 @@
         rule.setContent {
             LazyList {
                 items(list, key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
             LaunchedEffect(Unit) {
@@ -982,7 +982,7 @@
         rule.setContent {
             LazyList(maxSize = itemSizeDp * 3) {
                 items(listOf(0, 1, 2, 3, 4, 5, 6, 7), key = { it }) {
-                    item(it)
+                    Item(it)
                 }
             }
         }
@@ -1004,6 +1004,39 @@
         }
     }
 
+    @Test
+    fun itemWithSpecsIsMovingOut() {
+        var list by mutableStateOf(listOf(0, 1, 2, 3))
+        rule.setContent {
+            LazyList(maxSize = itemSizeDp * 2) {
+                items(list, key = { it }) {
+                    Item(it, animSpec = if (it == 1) AnimSpec else null)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            list = listOf(0, 2, 3, 1)
+        }
+
+        onAnimationFrame { fraction ->
+            val listSize = itemSize * 2
+            val item1Offset = itemSize + (itemSize * 2f * fraction).roundToInt()
+            val expected = mutableListOf<Pair<Any, Int>>().apply {
+                add(0 to 0)
+                if (item1Offset < listSize) {
+                    add(1 to item1Offset)
+                } else {
+                    rule.onNodeWithTag("1").assertIsNotDisplayed()
+                }
+            }
+            assertPositions(
+                expected = expected.toTypedArray(),
+                fraction = fraction
+            )
+        }
+    }
+
     private fun assertPositions(
         vararg expected: Pair<Any, Int>,
         crossAxis: List<Pair<Any, Int>>? = null,
@@ -1161,7 +1194,7 @@
     }
 
     @Composable
-    private fun TvLazyListItemScope.item(
+    private fun TvLazyListItemScope.Item(
         tag: Int,
         size: Dp = itemSizeDp,
         crossAxisSize: Dp = size,
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
index 43150c1..2a8e3e2 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListBeyondBoundsTest.kt
@@ -101,9 +101,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'contains' with 'containsExactly'.
-            assertThat(placedItems).contains(0)
-            assertThat(visibleItems).contains(0)
+            assertThat(placedItems).containsExactly(0)
+            assertThat(visibleItems).containsExactly(0)
         }
     }
 
@@ -122,9 +121,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(0, 1)
-            assertThat(visibleItems).containsAtLeast(0, 1)
+            assertThat(placedItems).containsExactly(0, 1)
+            assertThat(visibleItems).containsExactly(0, 1)
         }
     }
 
@@ -143,9 +141,8 @@
 
         // Assert.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(0, 1, 2)
-            assertThat(visibleItems).containsAtLeast(0, 1, 2)
+            assertThat(placedItems).containsExactly(0, 1, 2)
+            assertThat(visibleItems).containsExactly(0, 1, 2)
         }
     }
 
@@ -185,13 +182,11 @@
             beyondBoundsLayout!!.layout(beyondBoundsLayoutDirection) {
                 // Assert that the beyond bounds items are present.
                 if (expectedExtraItemsBeforeVisibleBounds()) {
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(4, 5, 6, 7)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(4, 5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 } else {
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(5, 6, 7, 8)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(5, 6, 7, 8)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 }
                 placedItems.clear()
                 // Just return true so that we stop as soon as we run this once.
@@ -202,9 +197,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -250,13 +244,11 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(3, 4, 5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     } else {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7, 8, 9)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     placedItems.clear()
                     // Return true to stop the search.
@@ -267,9 +259,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -316,13 +307,11 @@
                 } else {
                     // Assert that the beyond bounds items are present.
                     if (expectedExtraItemsBeforeVisibleBounds()) {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(0, 1, 2, 3, 4, 5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(0, 1, 2, 3, 4, 5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     } else {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7, 8, 9, 10)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7, 8, 9, 10)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     placedItems.clear()
                     // Return true to end the search.
@@ -333,8 +322,7 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
         }
     }
 
@@ -364,11 +352,15 @@
                 Box(
                     Modifier
                         .size(10.toDp())
-                        .onPlaced { placedItems += index + 5 }
+                        .onPlaced { placedItems += index + 6 }
                 )
             }
         }
-        rule.runOnIdle { placedItems.clear() }
+        rule.runOnIdle {
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
+            placedItems.clear()
+        }
 
         // Act.
         rule.runOnUiThread {
@@ -377,18 +369,16 @@
                 when (beyondBoundsLayoutDirection) {
                     Left, Right, Above, Below -> {
                         assertThat(placedItems).containsExactlyElementsIn(visibleItems)
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                        assertThat(placedItems).containsAtLeast(5, 6, 7)
-                        assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                        assertThat(placedItems).containsExactly(5, 6, 7)
+                        assertThat(visibleItems).containsExactly(5, 6, 7)
                     }
                     Before, After -> {
-                        // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
                         if (expectedExtraItemsBeforeVisibleBounds()) {
-                            assertThat(placedItems).containsAtLeast(4, 5, 6, 7)
-                            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                            assertThat(placedItems).containsExactly(4, 5, 6, 7)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
                         } else {
-                            assertThat(placedItems).containsAtLeast(5, 6, 7, 8)
-                            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                            assertThat(placedItems).containsExactly(5, 6, 7, 8)
+                            assertThat(visibleItems).containsExactly(5, 6, 7)
                         }
                     }
                 }
@@ -408,9 +398,8 @@
                     assertThat(beyondBoundsLayoutCount).isEqualTo(1)
 
                     // Assert that the beyond bounds items are removed.
-                    // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-                    assertThat(placedItems).containsAtLeast(5, 6, 7)
-                    assertThat(visibleItems).containsAtLeast(5, 6, 7)
+                    assertThat(placedItems).containsExactly(5, 6, 7)
+                    assertThat(visibleItems).containsExactly(5, 6, 7)
                 }
                 else -> error("Unsupported BeyondBoundsLayoutDirection")
             }
@@ -466,9 +455,8 @@
 
         // Assert that the beyond bounds items are removed.
         rule.runOnIdle {
-            // TODO(b/228100623): Replace 'containsAtLeast' with 'containsExactly'.
-            assertThat(placedItems).containsAtLeast(5, 6, 7)
-            assertThat(visibleItems).containsAtLeast(5, 6, 7)
+            assertThat(placedItems).containsExactly(5, 6, 7)
+            assertThat(visibleItems).containsExactly(5, 6, 7)
         }
     }
 
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
index 6e446cb..25fdd03 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.foundation.border
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
@@ -1758,6 +1759,67 @@
             .assertCrossAxisSizeIsEqualTo(0.dp)
     }
 
+    @Test
+    fun fillingFullSize_nextItemIsNotComposed() {
+        val state = TvLazyListState()
+        state.prefetchingEnabled = false
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier
+                    .testTag(LazyListTag)
+                    .mainAxisSize(itemSize),
+                state = state
+            ) {
+                items(3) { index ->
+                    Box(fillParentMaxMainAxis().crossAxisSize(1.dp).testTag("$index"))
+                }
+            }
+        }
+
+        repeat(3) { index ->
+            rule.onNodeWithTag("$index")
+                .assertIsDisplayed()
+            rule.onNodeWithTag("${index + 1}")
+                .assertDoesNotExist()
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun fillingFullSize_crossAxisSizeOfVisibleItemIsUsed() {
+        val state = TvLazyListState()
+        val itemSizePx = 5f
+        val itemSize = with(rule.density) { itemSizePx.toDp() }
+        rule.setContentWithTestViewConfiguration {
+            LazyColumnOrRow(
+                Modifier
+                    .testTag(LazyListTag)
+                    .mainAxisSize(itemSize),
+                state = state
+            ) {
+                items(5) { index ->
+                    Box(fillParentMaxMainAxis().crossAxisSize(index.dp))
+                }
+            }
+        }
+
+        repeat(5) { index ->
+            rule.onNodeWithTag(LazyListTag)
+                .assertCrossAxisSizeIsEqualTo(index.dp)
+            rule.runOnIdle {
+                runBlocking {
+                    state.scrollBy(itemSizePx)
+                }
+            }
+        }
+    }
+
     // ********************* END OF TESTS *********************
     // Helper functions, etc. live below here
 
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
index f5b44f4..e533a1e 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyScrollTest.kt
@@ -239,6 +239,33 @@
         assertSpringAnimation(toIndex = 0, toOffset = 20, fromIndex = 8)
     }
 
+    @Test
+    fun canScrollForward() = runBlocking {
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isFalse()
+    }
+
+    @Test
+    fun canScrollBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(itemsCount)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(itemsCount - 3)
+        assertThat(state.canScrollForward).isFalse()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
+    @Test
+    fun canScrollForwardAndBackward() = runBlocking {
+        withContext(Dispatchers.Main + AutoTestFrameClock()) {
+            state.scrollToItem(1)
+        }
+        assertThat(state.firstVisibleItemIndex).isEqualTo(1)
+        assertThat(state.canScrollForward).isTrue()
+        assertThat(state.canScrollBackward).isTrue()
+    }
+
     private fun assertSpringAnimation(
         toIndex: Int,
         toOffset: Int = 0,
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
index f33925a..b1402f3 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/ExperimentalTvFoundationApi.kt
@@ -19,4 +19,5 @@
 @RequiresOptIn(
     "This tv-foundation API is experimental and likely to change or be removed in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTvFoundationApi
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
index 223640a..6db01c4 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridItemPlacementAnimator.kt
@@ -81,7 +81,7 @@
         positionedItems: MutableList<TvLazyGridPositionedItem>,
         measuredItemProvider: LazyMeasuredItemProvider,
     ) {
-        if (!positionedItems.fastAny { it.hasAnimations }) {
+        if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
             // no animations specified - no work needed
             reset()
             return
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
index d50c7ec..03003be 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/LazyGridMeasure.kt
@@ -138,7 +138,11 @@
         // then composing visible lines forward until we fill the whole viewport.
         // we want to have at least one line in visibleItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
-        while (currentMainAxisOffset <= maxMainAxis || visibleLines.isEmpty()) {
+        while (index.value < itemsCount &&
+            (currentMainAxisOffset < maxMainAxis ||
+                currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+                visibleLines.isEmpty())
+        ) {
             val measuredLine = measuredLineProvider.getAndMeasure(index)
             if (measuredLine.isEmpty()) {
                 --index
@@ -250,7 +254,7 @@
         return TvLazyGridMeasureResult(
             firstVisibleLine = firstLine,
             firstVisibleLineScrollOffset = currentFirstLineScrollOffset,
-            canScrollForward = currentMainAxisOffset > maxOffset,
+            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach { it.place(this) }
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
index 42f4e83..15f05c7 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/grid/TvLazyGridState.kt
@@ -258,8 +258,9 @@
     override val isScrollInProgress: Boolean
         get() = scrollableState.isScrollInProgress
 
-    private var canScrollBackward: Boolean = false
-    internal var canScrollForward: Boolean = false
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
         private set
 
     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
index 317b454..a3ad3d9 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListItemPlacementAnimator.kt
@@ -71,7 +71,7 @@
         positionedItems: MutableList<LazyListPositionedItem>,
         itemProvider: LazyMeasuredItemProvider,
     ) {
-        if (!positionedItems.fastAny { it.hasAnimations }) {
+        if (!positionedItems.fastAny { it.hasAnimations } && keyToItemInfoMap.isEmpty()) {
             // no animations specified - no work needed
             reset()
             return
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
index 13dfdce..856d77b 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListMeasure.kt
@@ -150,8 +150,10 @@
         // then composing visible items forward until we fill the whole viewport.
         // we want to have at least one item in visibleItems even if in fact all the items are
         // offscreen, this can happen if the content padding is larger than the available size.
-        while ((currentMainAxisOffset <= maxMainAxis || visibleItems.isEmpty()) &&
-            index.value < itemsCount
+        while (index.value < itemsCount &&
+            (currentMainAxisOffset < maxMainAxis ||
+                currentMainAxisOffset <= 0 || // filling beforeContentPadding area
+                visibleItems.isEmpty())
         ) {
             val measuredItem = itemProvider.getAndMeasure(index)
             currentMainAxisOffset += measuredItem.sizeWithSpacings
@@ -301,7 +303,7 @@
         return LazyListMeasureResult(
             firstVisibleItem = firstItem,
             firstVisibleItemScrollOffset = currentFirstItemScrollOffset,
-            canScrollForward = currentMainAxisOffset > maxOffset,
+            canScrollForward = index.value < itemsCount || currentMainAxisOffset > maxOffset,
             consumedScroll = consumedScroll,
             measureResult = layout(layoutWidth, layoutHeight) {
                 positionedItems.fastForEach {
diff --git a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
index 6330923..2f0cc8c 100644
--- a/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
+++ b/tv/tv-foundation/src/main/java/androidx/tv/foundation/lazy/list/LazyListState.kt
@@ -261,8 +261,9 @@
     override val isScrollInProgress: Boolean
         get() = scrollableState.isScrollInProgress
 
-    private var canScrollBackward: Boolean = false
-    internal var canScrollForward: Boolean = false
+    override var canScrollForward: Boolean by mutableStateOf(false)
+        private set
+    override var canScrollBackward: Boolean by mutableStateOf(false)
         private set
 
     // TODO: Coroutine scrolling APIs will allow this to be private again once we have more
diff --git a/tv/tv-material/api/current.txt b/tv/tv-material/api/current.txt
index 24ca3bd..61d6bc4 100644
--- a/tv/tv-material/api/current.txt
+++ b/tv/tv-material/api/current.txt
@@ -1,4 +1,40 @@
 // Signature format: 4.0
+package androidx.tv.material {
+
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+  }
+
+}
+
 package androidx.tv.material.carousel {
 
   public final class CarouselItemKt {
diff --git a/tv/tv-material/api/public_plus_experimental_current.txt b/tv/tv-material/api/public_plus_experimental_current.txt
index 8712654..dc3de1c 100644
--- a/tv/tv-material/api/public_plus_experimental_current.txt
+++ b/tv/tv-material/api/public_plus_experimental_current.txt
@@ -1,7 +1,39 @@
 // Signature format: 4.0
 package androidx.tv.material {
 
-  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") public @interface ExperimentalTvMaterialApi {
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  @kotlin.RequiresOptIn(message="This tv-material API is experimental and likely to change or be removed in the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalTvMaterialApi {
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
   }
 
 }
diff --git a/tv/tv-material/api/restricted_current.txt b/tv/tv-material/api/restricted_current.txt
index 24ca3bd..61d6bc4 100644
--- a/tv/tv-material/api/restricted_current.txt
+++ b/tv/tv-material/api/restricted_current.txt
@@ -1,4 +1,40 @@
 // Signature format: 4.0
+package androidx.tv.material {
+
+  public final class ContentColorKt {
+    method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> getLocalContentColor();
+    property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.graphics.Color> LocalContentColor;
+  }
+
+  public final class TabColors {
+  }
+
+  public final class TabDefaults {
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors pillIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    method @androidx.compose.runtime.Composable public androidx.tv.material.TabColors underlinedIndicatorTabColors(optional long activeContentColor, optional long selectedContentColor, optional long focusedContentColor, optional long disabledActiveContentColor, optional long disabledSelectedContentColor);
+    field public static final androidx.tv.material.TabDefaults INSTANCE;
+  }
+
+  public final class TabKt {
+    method @androidx.compose.runtime.Composable public static void Tab(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onSelect, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.tv.material.TabColors colors, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
+  }
+
+  public final class TabRowDefaults {
+    method @androidx.compose.runtime.Composable public void PillIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public void TabSeparator();
+    method @androidx.compose.runtime.Composable public void UnderlinedIndicator(androidx.compose.ui.unit.DpRect currentTabPosition, optional androidx.compose.ui.Modifier modifier, optional long activeColor, optional long inactiveColor);
+    method @androidx.compose.runtime.Composable public long contentColor();
+    method public long getContainerColor();
+    property public final long ContainerColor;
+    field public static final androidx.tv.material.TabRowDefaults INSTANCE;
+  }
+
+  public final class TabRowKt {
+    method @androidx.compose.runtime.Composable public static void TabRow(int selectedTabIndex, optional androidx.compose.ui.Modifier modifier, optional long containerColor, optional long contentColor, optional kotlin.jvm.functions.Function0<kotlin.Unit> separator, optional kotlin.jvm.functions.Function1<? super java.util.List<androidx.compose.ui.unit.DpRect>,kotlin.Unit> indicator, kotlin.jvm.functions.Function0<kotlin.Unit> tabs);
+  }
+
+}
+
 package androidx.tv.material.carousel {
 
   public final class CarouselItemKt {
diff --git a/tv/tv-material/build.gradle b/tv/tv-material/build.gradle
index 19f04632..50fc402 100644
--- a/tv/tv-material/build.gradle
+++ b/tv/tv-material/build.gradle
@@ -27,9 +27,9 @@
 dependencies {
     api(libs.kotlinStdlib)
 
-    def composeVersion = '1.2.1'
+    def composeVersion = '1.3.0-rc01'
 
-    api("androidx.annotation:annotation:1.4.0")
+    api("androidx.annotation:annotation:1.5.0")
     api("androidx.compose.animation:animation:$composeVersion")
     api("androidx.compose.runtime:runtime:$composeVersion")
 
diff --git a/tv/tv-material/samples/build.gradle b/tv/tv-material/samples/build.gradle
index f78e2c8..2dfd344 100644
--- a/tv/tv-material/samples/build.gradle
+++ b/tv/tv-material/samples/build.gradle
@@ -26,11 +26,10 @@
 dependencies {
     implementation(project(":tv:tv-material"))
     implementation(libs.kotlinStdlib)
-    implementation("androidx.leanback:leanback:1.0.0")
     implementation(project(":activity:activity"))
     implementation(project(":compose:material3:material3"))
     implementation(project(":navigation:navigation-runtime"))
-    implementation("androidx.activity:activity-compose:1.5.1")
+    implementation("androidx.activity:activity-compose:1.6.1")
     implementation(project(":tv:tv-foundation"))
     implementation("androidx.appcompat:appcompat:1.6.0-alpha05")
 }
@@ -56,4 +55,4 @@
         }
     }
     namespace "androidx.tv.tvmaterial.samples"
-}
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt
new file mode 100644
index 0000000..773c124
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/App.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun App() {
+    var selectedTab by remember { mutableStateOf(navigationMap[Navigation.FeaturedCarousel]) }
+
+    Column(
+        modifier = Modifier.padding(20.dp),
+        verticalArrangement = Arrangement.spacedBy(20.dp),
+    ) {
+        TopNavigation(updateSelectedTab = { selectedTab = it })
+        when (reverseNavigationMap[selectedTab]) {
+            Navigation.FeaturedCarousel -> FeaturedCarousel()
+            Navigation.ImmersiveList -> SampleImmersiveList()
+            Navigation.LazyRowsAndColumns -> LazyRowsAndColumns()
+            else -> { }
+        }
+    }
+}
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
index f1b3668..a0f499c 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/FeaturedCarousel.kt
@@ -16,26 +16,15 @@
 
 package androidx.tv.tvmaterial.samples
 
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
-import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
@@ -44,107 +33,70 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.Color.Companion.Cyan
-import androidx.compose.ui.graphics.Color.Companion.Gray
-import androidx.compose.ui.graphics.Color.Companion.Yellow
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.tv.material.ExperimentalTvMaterialApi
 import androidx.tv.material.carousel.Carousel
 import androidx.tv.material.carousel.CarouselItem
-import androidx.tv.material.carousel.CarouselState
 
 @OptIn(ExperimentalTvMaterialApi::class)
 @Composable
 fun FeaturedCarousel() {
-    val carouselState = remember { CarouselState(0) }
+    val backgrounds = listOf(
+        Color.Red.copy(alpha = 0.3f),
+        Color.Yellow.copy(alpha = 0.3f),
+        Color.Green.copy(alpha = 0.3f)
+    )
+
     Carousel(
-        modifier = Modifier.height(130.dp).fillMaxWidth().border(1.dp, Color.Black),
-        carouselState = carouselState,
-        slideCount = mediaItems.size
-    ) { SampleFrame(it) }
-}
-
-@OptIn(ExperimentalTvMaterialApi::class)
-@Composable
-fun SampleFrame(idx: Int) {
-    val item = mediaItems[idx]
-
-    CarouselItem(
-        background = {
-            Box(
-                Modifier.background(item.backgroundColor).fillMaxSize()
-            )
-        }) {
-        Box {
-            Column(modifier = Modifier.align(Alignment.BottomStart)) {
-                Text(
-                    text = item.title,
-                    style = MaterialTheme.typography.headlineSmall,
-                    color = Color.Black,
-                    fontWeight = FontWeight.Bold
+        slideCount = backgrounds.size,
+        modifier = Modifier
+            .height(300.dp)
+            .fillMaxWidth(),
+    ) { itemIndex ->
+        CarouselItem(
+            overlayEnterTransitionStartDelayMillis = 0,
+            background = {
+                Box(
+                    modifier = Modifier
+                        .background(backgrounds[itemIndex])
+                        .border(2.dp, Color.White.copy(alpha = 0.5f))
+                        .fillMaxSize()
                 )
-                Text(
-                    text = item.description,
-                    style = MaterialTheme.typography.bodyMedium,
-                    color = Color.Black,
-                    fontWeight = FontWeight.Normal,
-                    modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
-                )
-
-                Row(modifier = Modifier.horizontalScroll(rememberScrollState())) {
-                    SampleButton(text = "PLAY")
-                    SampleButton(text = "INFO")
-                }
             }
+        ) {
+            Card()
         }
     }
 }
 
 @Composable
-fun SampleButton(text: String) {
-    var cardScale
-        by remember { mutableStateOf(0.5f) }
-    val borderGlowColorTransition =
-        rememberInfiniteTransition()
-    var initialValue
-        by remember { mutableStateOf(Color.Transparent) }
-    val glowingColor
-        by borderGlowColorTransition.animateColor(
-            initialValue = initialValue,
-            targetValue = Color.Transparent,
-            animationSpec = infiniteRepeatable(
-                animation = tween(1000, easing = LinearEasing),
-                repeatMode = RepeatMode.Reverse
-            )
-        )
-
-    Button(
-        onClick = {},
+private fun Card() {
+    Box(
         modifier = Modifier
-            .scale(cardScale)
-            .border(
-                2.dp, glowingColor,
-                RoundedCornerShape(12.dp)
-            )
-            .onFocusChanged { focusState ->
-                if (focusState.isFocused) {
-                    cardScale = 1.0f
-                    initialValue = Color.White
-                } else {
-                    cardScale = 0.5f
-                    initialValue = Color.Transparent
-                }
-            }) {
-        Text(text = text)
+            .fillMaxSize()
+            .padding(40.dp),
+        contentAlignment = Alignment.CenterStart
+    ) {
+        var isFocused by remember { mutableStateOf(false) }
+
+        Box(
+            modifier = Modifier
+                .border(
+                    width = 2.dp,
+                    color = if (isFocused) Color.Red else Color.Transparent,
+                    shape = RoundedCornerShape(50)
+                )
+        ) {
+            Button(
+                onClick = { },
+                modifier = Modifier
+                    .onFocusChanged { isFocused = it.isFocused }
+                    .padding(vertical = 2.dp, horizontal = 5.dp)
+            ) {
+                Text(text = "Play")
+            }
+        }
     }
 }
-
-val mediaItems = listOf(
-    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Gray),
-    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Yellow),
-    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Cyan)
-)
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt
new file mode 100644
index 0000000..cb4847f
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/ImmersiveList.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.material.ExperimentalTvMaterialApi
+import androidx.tv.material.immersivelist.ImmersiveList
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+fun SampleImmersiveList() {
+    val immersiveListHeight = 300.dp
+    val cardSpacing = 10.dp
+    val cardWidth = 200.dp
+    val cardHeight = 150.dp
+    val backgrounds = listOf(
+        Color.Red,
+        Color.Blue,
+        Color.Magenta,
+    )
+
+    Box(
+        modifier = Modifier
+            .height(immersiveListHeight + cardHeight / 2)
+            .fillMaxWidth()
+    ) {
+        ImmersiveList(
+            modifier = Modifier
+                .height(immersiveListHeight)
+                .fillMaxWidth(),
+            listAlignment = Alignment.BottomEnd,
+            background = { index, _ ->
+                Box(
+                    modifier = Modifier
+                        .background(backgrounds[index].copy(alpha = 0.3f))
+                        .fillMaxSize()
+                )
+            }
+        ) {
+            Row(
+                horizontalArrangement = Arrangement.spacedBy(cardSpacing),
+                modifier = Modifier.offset(y = cardHeight / 2)
+            ) {
+                backgrounds.forEachIndexed { index, backgroundColor ->
+                    var isFocused by remember { mutableStateOf(false) }
+
+                    Box(
+                        modifier = Modifier
+                            .background(backgroundColor)
+                            .width(cardWidth)
+                            .height(cardHeight)
+                            .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.3f))
+                            .onFocusChanged { isFocused = it.isFocused }
+                            .focusableItem(index)
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
new file mode 100644
index 0000000..bbf0111
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/LazyRowsAndColumns.kt
@@ -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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import androidx.tv.foundation.lazy.list.TvLazyColumn
+import androidx.tv.foundation.lazy.list.TvLazyRow
+
+const val rowsCount = 20
+const val columnsCount = 100
+
+@Composable
+fun LazyRowsAndColumns() {
+    TvLazyColumn(verticalArrangement = Arrangement.spacedBy(20.dp)) {
+        repeat((0 until rowsCount).count()) {
+            item { LazyRow() }
+        }
+    }
+}
+
+@Composable
+private fun LazyRow() {
+    val colors = listOf(Color.Red, Color.Magenta, Color.Green, Color.Yellow, Color.Blue, Color.Cyan)
+    val backgroundColors = (0 until columnsCount).map { colors.random() }
+
+    TvLazyRow(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+        backgroundColors.forEach { backgroundColor ->
+            item {
+                var isFocused by remember { mutableStateOf(false) }
+
+                Box(
+                    modifier = Modifier
+                        .background(backgroundColor.copy(alpha = 0.3f))
+                        .width(200.dp)
+                        .height(150.dp)
+                        .border(5.dp, Color.White.copy(alpha = if (isFocused) 1f else 0.2f))
+                        .onFocusChanged { isFocused = it.isFocused }
+                        .focusable()
+                )
+            }
+        }
+    }
+}
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
index b3f2c76..e8ebd3a 100644
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/MainActivity.kt
@@ -19,111 +19,12 @@
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.activity.compose.setContent
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
 
 class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContent {
-            // A surface container using the 'background' color from the theme
-            Surface(color = MaterialTheme.colorScheme.background) {
-                LazyColumn {
-                    item { FeaturedCarousel() }
-                    item { SampleImmersiveList() }
-
-                    items(7) { SampleLazyRow() }
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun SampleLazyRow() {
-        LazyRow(
-            state = rememberLazyListState(),
-            contentPadding = PaddingValues(2.dp),
-            horizontalArrangement = Arrangement.spacedBy(4.dp),
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(100.dp)) {
-            items((1..10).map { it.toString() }) { SampleCard(it) }
-        }
-    }
-
-    @Composable
-    private fun SampleCard(it: String) {
-        var cardScale by remember { mutableStateOf(0.5f) }
-        val borderGlowColorTransition = rememberInfiniteTransition()
-        var initialValue by remember { mutableStateOf(Color.Transparent) }
-        val glowingColor by borderGlowColorTransition.animateColor(
-            initialValue = initialValue,
-            targetValue = Color.Transparent,
-            animationSpec = infiniteRepeatable(
-                animation = tween(1000, easing = LinearEasing),
-                repeatMode = RepeatMode.Reverse
-            )
-        )
-
-        Card(
-            modifier = Modifier
-                .width(100.dp)
-                .height(100.dp)
-                .scale(cardScale)
-                .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
-                .onFocusChanged { focusState ->
-                    if (focusState.isFocused) {
-                        cardScale = 1.0f
-                        initialValue = Color.White
-                    } else {
-                        cardScale = 0.5f
-                        initialValue = Color.Transparent
-                    }
-                }
-                .focusable()
-        ) {
-            Text(
-                text = it,
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .height(100.dp)
-                    .padding(12.dp),
-                color = Color.Red,
-                fontWeight = FontWeight.Bold
-
-            )
+            App()
         }
     }
 }
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
deleted file mode 100644
index cab1518..0000000
--- a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/SampleImmersiveList.kt
+++ /dev/null
@@ -1,138 +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.tv.tvmaterial.samples
-
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.focusable
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Card
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.dp
-import androidx.tv.foundation.lazy.list.TvLazyRow
-import androidx.tv.material.ExperimentalTvMaterialApi
-import androidx.tv.material.immersivelist.ImmersiveList
-
-@OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
-@Composable
-fun SampleImmersiveList() {
-    ImmersiveList(
-        modifier = Modifier
-            .height(130.dp)
-            .fillMaxWidth()
-            .border(1.dp, Color.Black),
-        background = { index, _ ->
-            AnimatedContent(targetState = index) { SampleBackground(it) } },
-    ) {
-        TvLazyRow {
-            items(immersiveClusterMediaItems.size) {
-                SampleCard(Modifier.focusableItem(it), (it + 1).toString())
-            }
-        }
-    }
-}
-
-@Composable
-fun SampleBackground(idx: Int) {
-    val item = immersiveClusterMediaItems[idx]
-
-    Box(
-        Modifier
-            .background(item.backgroundColor)
-            .fillMaxWidth()
-            .height(90.dp)) {
-        Text(
-            text = item.title,
-            style = MaterialTheme.typography.headlineSmall,
-            color = Color.Black,
-            fontWeight = FontWeight.Bold
-        )
-    }
-}
-
-@Composable
-private fun SampleCard(modifier: Modifier, cardText: String) {
-    var cardScale by remember { mutableStateOf(0.5f) }
-    val borderGlowColorTransition = rememberInfiniteTransition()
-    var initialValue by remember { mutableStateOf(Color.Transparent) }
-    val glowingColor by borderGlowColorTransition.animateColor(
-        initialValue = initialValue,
-        targetValue = Color.Transparent,
-        animationSpec = infiniteRepeatable(
-            animation = tween(1000, easing = LinearEasing),
-            repeatMode = RepeatMode.Reverse
-        )
-    )
-
-    Card(
-        modifier = modifier
-            .width(100.dp)
-            .height(100.dp)
-            .scale(cardScale)
-            .border(2.dp, glowingColor, RoundedCornerShape(12.dp))
-            .onFocusChanged { focusState ->
-                if (focusState.isFocused) {
-                    cardScale = 1.0f
-                    initialValue = Color.White
-                } else {
-                    cardScale = 0.5f
-                    initialValue = Color.Transparent
-                }
-            }
-            .focusable()
-    ) {
-        Text(
-            text = cardText,
-            modifier = Modifier
-                .fillMaxWidth()
-                .height(100.dp)
-                .padding(12.dp),
-            color = Color.Red,
-            fontWeight = FontWeight.Bold
-
-        )
-    }
-}
-
-val immersiveClusterMediaItems = listOf(
-    Media(id = "1", title = "Title 1", description = "Description 1", backgroundColor = Color.Gray),
-    Media(id = "2", title = "Title 2", description = "Description 2", backgroundColor = Color.Blue),
-    Media(id = "3", title = "Title 3", description = "Description 3", backgroundColor = Color.Cyan)
-)
\ No newline at end of file
diff --git a/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
new file mode 100644
index 0000000..8cee8db
--- /dev/null
+++ b/tv/tv-material/samples/src/main/java/androidx/tv/tvmaterial/samples/TopNavigation.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.tv.tvmaterial.samples
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.tv.material.LocalContentColor
+import androidx.tv.material.Tab
+import androidx.tv.material.TabDefaults
+import androidx.tv.material.TabRow
+import androidx.tv.material.TabRowDefaults
+
+enum class Navigation {
+  FeaturedCarousel,
+  ImmersiveList,
+  LazyRowsAndColumns,
+}
+
+val navigationMap =
+  hashMapOf(
+    Navigation.FeaturedCarousel to "Featured Carousel",
+    Navigation.ImmersiveList to "Immersive List",
+    Navigation.LazyRowsAndColumns to "Lazy Rows and Columns",
+  )
+val reverseNavigationMap = navigationMap.entries.associate { it.value to it.key }
+
+@Composable
+internal fun TopNavigation(
+  updateSelectedTab: (String) -> Unit = {},
+) {
+  var selectedTabIndex by remember { mutableStateOf(0) }
+  val tabs = navigationMap.entries.map { it.value }
+
+  // Pill indicator
+  PillIndicatorTabRow(
+    tabs = tabs,
+    selectedTabIndex = selectedTabIndex,
+    updateSelectedTab = { selectedTabIndex = it }
+  )
+
+  LaunchedEffect(selectedTabIndex) { updateSelectedTab(tabs[selectedTabIndex]) }
+}
+
+/**
+ * Pill indicator tab row for reference
+ */
+@Composable
+fun PillIndicatorTabRow(
+  tabs: List<String>,
+  selectedTabIndex: Int,
+  updateSelectedTab: (Int) -> Unit
+) {
+  TabRow(
+    selectedTabIndex = selectedTabIndex,
+    separator = { Spacer(modifier = Modifier.width(12.dp)) },
+  ) {
+    tabs.forEachIndexed { index, tab ->
+      Tab(
+        selected = index == selectedTabIndex,
+        onSelect = { updateSelectedTab(index) },
+      ) {
+        Text(
+          text = tab,
+          fontSize = 12.sp,
+          color = LocalContentColor.current,
+          modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp)
+        )
+      }
+    }
+  }
+}
+
+/**
+ * Underlined indicator tab row for reference
+ */
+@Composable
+fun UnderlinedIndicatorTabRow(
+  tabs: List<String>,
+  selectedTabIndex: Int,
+  updateSelectedTab: (Int) -> Unit
+) {
+  TabRow(
+    selectedTabIndex = selectedTabIndex,
+    separator = { Spacer(modifier = Modifier.width(12.dp)) },
+    indicator = { tabPositions ->
+      TabRowDefaults.UnderlinedIndicator(
+        currentTabPosition = tabPositions[selectedTabIndex]
+      )
+    }
+  ) {
+    tabs.forEachIndexed { index, tab ->
+      Tab(
+        selected = index == selectedTabIndex,
+        onSelect = { updateSelectedTab(index) },
+        colors = TabDefaults.underlinedIndicatorTabColors(),
+      ) {
+        Text(
+          text = tab,
+          fontSize = 12.sp,
+          color = LocalContentColor.current,
+          modifier = Modifier.padding(bottom = 4.dp)
+        )
+      }
+    }
+  }
+}
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
new file mode 100644
index 0000000..8459060
--- /dev/null
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/TabRowTest.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.key.NativeKeyEvent
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.dp
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Rule
+import org.junit.Test
+
+class TabRowTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun tabRow_firstTabIsSelected() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+
+        setContent(tabs)
+
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadRightMovesFocusToSecondTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+    }
+
+    @Test
+    fun tabRow_dPadLeftMovesFocusToPreviousTab() {
+        val tabs = constructTabs()
+        val firstTab = tabs[0]
+        val secondTab = tabs[1]
+        val thirdTab = tabs[2]
+
+        setContent(tabs)
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+
+        rule.waitForIdle()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to next tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        rule.waitForIdle()
+
+        // Third tab should be focused
+        rule.onNodeWithTag(thirdTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // Second tab should be focused
+        rule.onNodeWithTag(secondTab).assertIsFocused()
+
+        // Move to previous tab
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        rule.waitForIdle()
+
+        // First tab should be focused
+        rule.onNodeWithTag(firstTab).assertIsFocused()
+    }
+
+    private fun setContent(tabs: List<String>) {
+        val fr = FocusRequester()
+
+        rule.setContent {
+            Column(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Black)
+            ) {
+                var selectedTabIndex by remember { mutableStateOf(0) }
+
+                TabRow(
+                    selectedTabIndex = selectedTabIndex,
+                    separator = { Spacer(modifier = Modifier.width(12.dp)) }
+                ) {
+                    tabs.forEachIndexed { index, tab ->
+                        Tab(
+                            selected = index == selectedTabIndex,
+                            onSelect = { selectedTabIndex = index },
+                            modifier = Modifier
+                                .width(100.dp)
+                                .height(50.dp)
+                                .testTag(tab)
+                                .border(2.dp, Color.White, RoundedCornerShape(50))
+                        ) {}
+                    }
+                }
+
+                // Added so that this can get focus and pass it to the tab row
+                Box(
+                    modifier = Modifier
+                        .size(50.dp)
+                        .focusRequester(fr)
+                        .background(Color.White)
+                        .focusable()
+                )
+
+                // Send focus to button
+                LaunchedEffect(Unit) {
+                    fr.requestFocus()
+                }
+            }
+        }
+
+        rule.waitForIdle()
+
+        // Move the focus TabRow
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP)
+
+        rule.waitForIdle()
+    }
+
+    private fun performKeyPress(keyCode: Int, count: Int = 1) {
+        for (i in 1..count) {
+            InstrumentationRegistry
+                .getInstrumentation()
+                .sendKeyDownUpSync(keyCode)
+        }
+    }
+
+    private fun constructTabs(
+        count: Int = 3,
+        buildTab: (index: Int) -> String = { "Season $it" }
+    ): List<String> = (0 until count).map(buildTab)
+}
\ No newline at end of file
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
index 16787a1..c4933a3 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/carousel/CarouselTest.kt
@@ -16,16 +16,9 @@
 
 package androidx.tv.material.carousel
 
-import androidx.compose.animation.animateColor
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.RepeatMode
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.focusable
-import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -33,9 +26,8 @@
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -46,7 +38,6 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.focus.onFocusChanged
@@ -58,34 +49,37 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
 import androidx.compose.ui.test.assertIsNotFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onNodeWithText
 import androidx.compose.ui.test.onParent
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
-import androidx.test.filters.FlakyTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tv.material.ExperimentalTvMaterialApi
+import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 
+private const val delayBetweenSlides = 2500L
+private const val animationTime = 900L
+private const val overlayRenderWaitTime = 1500L
+
 @OptIn(ExperimentalTvMaterialApi::class)
 class CarouselTest {
-
-    private val delayBetweenSlides = 2500L
-    private val animationTime = 900L
-    private val overlayRenderWaitTime = 1500L
-
     @get:Rule
     val rule = createComposeRule()
 
     @Test
     fun carousel_autoScrolls() {
-
         rule.setContent {
-            Content()
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -101,9 +95,10 @@
 
     @Test
     fun carousel_onFocus_stopsScroll() {
-
         rule.setContent {
-            Content()
+            SampleCarousel {
+                BasicText(text = "Text ${it + 1}")
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -122,15 +117,12 @@
 
     @Test
     fun carousel_onUserTriggeredPause_stopsScroll() {
-        var carouselState: CarouselState?
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) { carouselState?.pauseAutoScroll(it) }
-                })
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) { carouselState.pauseAutoScroll(it) }
+            }
         }
 
         rule.onNodeWithText("Text 1").assertIsDisplayed()
@@ -145,18 +137,15 @@
 
     @Test
     fun carousel_onUserTriggeredPauseAndResume_resumeScroll() {
-        var carouselState: CarouselState?
         var pauseHandle: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        pauseHandle = carouselState?.pauseAutoScroll(it)
-                    }
-                })
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    pauseHandle = carouselState.pauseAutoScroll(it)
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -183,24 +172,21 @@
 
     @Test
     fun carousel_onMultipleUserTriggeredPauseAndResume_resumesScroll() {
-        var carouselState: CarouselState?
         var pauseHandle1: ScrollPauseHandle? = null
         var pauseHandle2: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        if (pauseHandle1 == null) {
-                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
-                        }
-                        if (pauseHandle2 == null) {
-                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
-                        }
+            val carouselState = remember { CarouselState() }
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
                     }
-                })
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -233,24 +219,21 @@
 
     @Test
     fun carousel_onRepeatedResumesOnSamePauseHandle_ignoresSubsequentResumeCalls() {
-        var carouselState: CarouselState?
         var pauseHandle1: ScrollPauseHandle? = null
-        var pauseHandle2: ScrollPauseHandle? = null
         rule.setContent {
-            carouselState = remember { CarouselState() }
-            Content(
-                carouselState = carouselState!!,
-                content = {
-                    BasicText(text = "Text ${it + 1}")
-                    LaunchedEffect(carouselState) {
-                        if (pauseHandle1 == null) {
-                            pauseHandle1 = carouselState?.pauseAutoScroll(it)
-                        }
-                        if (pauseHandle2 == null) {
-                            pauseHandle2 = carouselState?.pauseAutoScroll(it)
-                        }
+            val carouselState = remember { CarouselState() }
+            var pauseHandle2: ScrollPauseHandle? = null
+            SampleCarousel(carouselState = carouselState) {
+                BasicText(text = "Text ${it + 1}")
+                LaunchedEffect(carouselState) {
+                    if (pauseHandle1 == null) {
+                        pauseHandle1 = carouselState.pauseAutoScroll(it)
                     }
-                })
+                    if (pauseHandle2 == null) {
+                        pauseHandle2 = carouselState.pauseAutoScroll(it)
+                    }
+                }
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -278,7 +261,12 @@
     @Test
     fun carousel_outOfFocus_resumesScroll() {
         rule.setContent {
-            Content()
+            Column {
+                SampleCarousel {
+                    BasicText(text = "Text ${it + 1}")
+                }
+                BasicText(text = "Card", modifier = Modifier.focusable())
+            }
         }
 
         rule.onNodeWithText("Text 1")
@@ -297,7 +285,9 @@
     @Test
     fun carousel_pagerIndicatorDisplayed() {
         rule.setContent {
-            Content()
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
         }
 
         rule.onNodeWithTag("indicator").assertIsDisplayed()
@@ -306,7 +296,14 @@
     @Test
     fun carousel_withAnimatedContent_successfulTransition() {
         rule.setContent {
-            AnimatedContent()
+            SampleCarousel {
+                SampleCarouselSlide(index = it) {
+                    Column {
+                        BasicText(text = "Text ${it + 1}")
+                        BasicText(text = "PLAY")
+                    }
+                }
+            }
         }
 
         rule.onNodeWithText("Text 1").assertDoesNotExist()
@@ -318,11 +315,12 @@
         rule.onNodeWithText("PLAY").assertIsDisplayed()
     }
 
-    @FlakyTest(bugId = 246336782)
     @Test
     fun carousel_withAnimatedContent_successfulFocusIn() {
         rule.setContent {
-            AnimatedContent()
+            SampleCarousel {
+                SampleCarouselSlide(index = it)
+            }
         }
 
         rule.mainClock.autoAdvance = false
@@ -334,228 +332,98 @@
         rule.mainClock.advanceTimeBy(animationTime, false)
         rule.mainClock.advanceTimeByFrame()
 
-        rule.onNodeWithText("PLAY").assertIsDisplayed()
-        rule.onNodeWithText("PLAY").assertIsFocused()
+        rule.onNodeWithText("Play 0").assertIsDisplayed()
+        rule.onNodeWithText("Play 0").assertIsFocused()
     }
 
     @Test
-    fun carousel_manualScrolling_ltr() {
-        rule.setContent {
-            Content {
-                TestButton("Button ${it + 1}")
-            }
-        }
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-
-    @Test
-    fun carousel_manualScrolling_rtl() {
-        rule.setContent {
-            CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Content {
-                    TestButton("Button ${it + 1}")
-                }
-            }
-        }
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-
-        // advance time
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // go right once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 2 is in view
-        rule.onNodeWithText("Button 2").assertIsDisplayed()
-
-        // go left once
-        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
-
-        // Wait for slide to load
-        rule.mainClock.advanceTimeBy(delayBetweenSlides)
-        rule.mainClock.advanceTimeBy(animationTime)
-
-        // Assert that slide 1 is in view
-        rule.onNodeWithText("Button 1").assertIsDisplayed()
-    }
-
-    private fun performKeyPress(keyCode: Int, count: Int = 1) {
-        for (i in 1..count) {
-            InstrumentationRegistry
-                .getInstrumentation()
-                .sendKeyDownUpSync(keyCode)
-        }
-    }
-
-    @Composable
-    fun Content(
-        carouselState: CarouselState = remember { CarouselState() },
-        slideCount: Int = 3,
-        content: @Composable (index: Int) -> Unit = { BasicText(text = "Text ${it + 1}") }
-    ) {
-        LazyColumn {
-            item {
-                Carousel(
-                    modifier = Modifier.fillMaxSize(),
-                    carouselState = carouselState,
-                    slideCount = slideCount,
-                    timeToDisplaySlideMillis = delayBetweenSlides,
-                    carouselIndicator = {
-                        CarouselDefaults.Indicator(modifier = Modifier
-                            .align(Alignment.BottomEnd)
-                            .padding(16.dp)
-                            .testTag("indicator"),
-                            carouselState = carouselState,
-                            slideCount = slideCount)
-                    },
-                    content = content
-                )
-            }
-            item {
-                Box(modifier = Modifier.focusable()
-                ) {
-                    BasicText(
-                        text = "Card",
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .padding(12.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun AnimatedContent(carouselState: CarouselState = remember { CarouselState() }) {
-        LazyColumn {
-            item {
-                Carousel(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .testTag("pager"),
-                    slideCount = 3,
-                    timeToDisplaySlideMillis = delayBetweenSlides,
-                    carouselState = carouselState
-                ) { Frame(text = "Text ${it + 1}") }
-            }
-            item {
-                Box(modifier = Modifier.focusable()
-                ) {
-                    BasicText(
-                        text = "Card",
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .height(100.dp)
-                            .padding(12.dp)
-                            .focusable()
-                    )
-                }
-            }
-        }
-    }
-
-    @Composable
-    fun Frame(text: String) {
+    fun carousel_scrollToRegainFocus_checkBringIntoView() {
         val focusRequester = FocusRequester()
-        CarouselItem(
-            overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
-            background = {}) {
-            Column(modifier = Modifier
-                .onFocusChanged {
-                    if (it.isFocused) {
-                        focusRequester.requestFocus()
-                    }
+        rule.setContent {
+            LazyColumn {
+                items(3) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = Modifier
+                            .focusRequester(if (it == 0) focusRequester else FocusRequester.Default)
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .border(2.dp, if (isFocused) Color.Red else Color.Black)
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
                 }
-                .focusable()) {
-                BasicText(text = text)
-                Row(modifier = Modifier
-                    .horizontalScroll(rememberScrollState())
-                    .onFocusChanged {
-                        if (it.isFocused) {
-                            focusRequester.requestFocus()
+                item {
+                    Carousel(
+                        modifier = Modifier
+                            .height(500.dp)
+                            .fillMaxWidth()
+                            .testTag("featured-carousel")
+                            .border(2.dp, Color.Black),
+                        carouselState = remember { CarouselState() },
+                        slideCount = 3,
+                        timeToDisplaySlideMillis = delayBetweenSlides
+                    ) {
+                        SampleCarouselSlide(
+                            index = it,
+                            overlayRenderWaitTime = overlayRenderWaitTime,
+                        ) {
+                            Box {
+                                Column(modifier = Modifier.align(Alignment.BottomStart)) {
+                                    BasicText(text = "carousel-frame")
+                                    Row {
+                                        SampleButton(text = "PLAY")
+                                    }
+                                }
+                            }
                         }
                     }
-                    .focusable()) {
-                    TestButton(text = "PLAY", focusRequester)
+                }
+                items(2) {
+                    var isFocused by remember { mutableStateOf(false) }
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(250.dp)
+                            .border(
+                                2.dp,
+                                if (isFocused) Color.Red else Color.Black
+                            )
+                            .onFocusChanged { fs ->
+                                isFocused = fs.isFocused
+                            }
+                            .focusable()
+                    )
                 }
             }
         }
-    }
+        rule.runOnIdle { focusRequester.requestFocus() }
 
-    @Composable
-    fun TestButton(text: String, focusRequester: FocusRequester? = null) {
-        var cardScale
-            by remember { mutableStateOf(0.5f) }
-        val borderGlowColorTransition =
-            rememberInfiniteTransition()
-        var initialValue
-            by remember { mutableStateOf(Color.Transparent) }
-        val glowingColor
-            by borderGlowColorTransition.animateColor(
-                initialValue = initialValue,
-                targetValue = Color.Transparent,
-                animationSpec = infiniteRepeatable(
-                    animation = tween(1000,
-                        easing = LinearEasing),
-                    repeatMode = RepeatMode.Reverse
-                )
-            )
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
 
-        val baseModifier =
-            if (focusRequester == null) Modifier else Modifier.focusRequester(focusRequester)
+        // Scroll down to the Carousel and check if it's brought into view on gaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
 
-        Box(
-            modifier = baseModifier
-                .scale(cardScale)
-                .border(
-                    2.dp, glowingColor,
-                    RoundedCornerShape(12.dp)
-                )
-                .onFocusChanged { focusState ->
-                    if (focusState.isFocused) {
-                        cardScale = 1.0f
-                        initialValue = Color.White
-                    } else {
-                        cardScale = 0.5f
-                        initialValue = Color.Transparent
-                    }
-                }
-                .clickable(onClick = {})) {
-            BasicText(text = text)
-        }
+        // Scroll down to last element, making sure the carousel is partially visible
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+
+        // Scroll back to the carousel to check if it's brought into view on regaining focus
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("featured-carousel").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible(rule, "featured-carousel")).isTrue()
     }
 
     @Test
@@ -567,4 +435,188 @@
 
         rule.onNodeWithTag(testTag).assertExists()
     }
+
+    @Test
+    fun carousel_manualScrolling_ltr() {
+        rule.setContent {
+            SampleCarousel { index ->
+                SampleButton("Button ${index + 1}")
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides)
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+
+    @Test
+    fun carousel_manualScrolling_rtl() {
+        rule.setContent {
+            CompositionLocalProvider(
+                LocalLayoutDirection provides LayoutDirection.Rtl
+            ) {
+                SampleCarousel {
+                    SampleButton("Button ${it + 1}")
+                }
+            }
+        }
+
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag("pager")
+            .performSemanticsAction(SemanticsActions.RequestFocus)
+
+        // current slide overlay render delay
+        rule.mainClock.advanceTimeBy(animationTime + overlayRenderWaitTime, false)
+        rule.mainClock.advanceTimeBy(animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+
+        // advance time
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // go right once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_LEFT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(animationTime)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 2 is in view
+        rule.onNodeWithText("Button 2").assertIsDisplayed()
+
+        // go left once
+        performKeyPress(NativeKeyEvent.KEYCODE_DPAD_RIGHT)
+
+        // Wait for slide to load
+        rule.mainClock.advanceTimeBy(delayBetweenSlides + animationTime, false)
+        rule.mainClock.advanceTimeByFrame()
+
+        // Assert that slide 1 is in view
+        rule.onNodeWithText("Button 1").assertIsDisplayed()
+    }
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun SampleCarousel(
+    carouselState: CarouselState = remember { CarouselState() },
+    slideCount: Int = 3,
+    timeToDisplaySlideMillis: Long = delayBetweenSlides,
+    content: @Composable (index: Int) -> Unit
+) {
+    Carousel(
+        modifier = Modifier
+            .padding(5.dp)
+            .fillMaxWidth()
+            .height(50.dp)
+            .testTag("pager"),
+        carouselState = carouselState,
+        slideCount = slideCount,
+        timeToDisplaySlideMillis = timeToDisplaySlideMillis,
+        carouselIndicator = {
+            CarouselDefaults.Indicator(
+                modifier = Modifier
+                    .align(Alignment.BottomEnd)
+                    .padding(16.dp)
+                    .testTag("indicator"),
+                carouselState = carouselState,
+                slideCount = slideCount
+            )
+        },
+        content = content,
+    )
+}
+
+@OptIn(ExperimentalTvMaterialApi::class)
+@Composable
+private fun SampleCarouselSlide(
+    index: Int,
+    overlayRenderWaitTime: Long = CarouselItemDefaults.OverlayEnterTransitionStartDelayMillis,
+    content: (@Composable () -> Unit) = { SampleButton("Play $index") },
+) {
+    CarouselItem(
+        overlayEnterTransitionStartDelayMillis = overlayRenderWaitTime,
+        background = {
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .background(Color.Red)
+                    .border(2.dp, Color.Blue)
+            )
+        },
+        overlay = content
+    )
+}
+
+@Composable
+private fun SampleButton(text: String = "Play") {
+    var isFocused by remember { mutableStateOf(false) }
+    BasicText(
+        text = text,
+        modifier = Modifier
+            .size(100.dp, 20.dp)
+            .background(Color.Yellow)
+            .onFocusChanged { isFocused = it.isFocused }
+            .border(2.dp, if (isFocused) Color.Green else Color.Transparent)
+            .focusable(),
+    )
+}
+
+private fun checkNodeCompletelyVisible(
+    rule: ComposeContentTestRule,
+    tag: String,
+): Boolean {
+    rule.waitForIdle()
+
+    val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+    val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+    return itemRect.left >= rootRect.left &&
+        itemRect.right <= rootRect.right &&
+        itemRect.top >= rootRect.top &&
+        itemRect.bottom <= rootRect.bottom
+}
+
+private fun performKeyPress(keyCode: Int, count: Int = 1) {
+    for (i in 1..count) {
+        InstrumentationRegistry
+            .getInstrumentation()
+            .sendKeyDownUpSync(keyCode)
+    }
 }
diff --git a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
index 5b22bdd..1aeefeb 100644
--- a/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
+++ b/tv/tv-material/src/androidTest/java/androidx/tv/material/immersivelist/ImmersiveListTest.kt
@@ -17,22 +17,34 @@
 package androidx.tv.material.immersivelist
 
 import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.foundation.border
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.input.key.NativeKeyEvent
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsFocused
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.dp
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.tv.foundation.lazy.list.TvLazyRow
 import androidx.tv.material.ExperimentalTvMaterialApi
+import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 
@@ -100,6 +112,113 @@
         rule.onNodeWithTag("background-2").assertDoesNotExist()
     }
 
+    @Test
+    fun immersiveList_scrollToRegainFocus_checkBringIntoView() {
+        val focusRequesterList = mutableListOf<FocusRequester>()
+        for (item in 0..2) { focusRequesterList.add(FocusRequester()) }
+        setupContent(focusRequesterList)
+
+        // Initially first focusable element would be focused
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-0").assertIsFocused()
+
+        // Scroll down to the Immersive List's first card
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 3)
+        rule.waitForIdle()
+        rule.onNodeWithTag("list-card-0").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+
+        // Scroll down to last element, making sure the immersive list is partially visible
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_DOWN, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("test-card-4").assertIsFocused()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+
+        // Scroll back to the immersive list to check if it's brought into view on regaining focus
+        keyPress(NativeKeyEvent.KEYCODE_DPAD_UP, 2)
+        rule.waitForIdle()
+        rule.onNodeWithTag("immersive-list").assertIsDisplayed()
+        assertThat(checkNodeCompletelyVisible("immersive-list")).isTrue()
+    }
+
+    private fun checkNodeCompletelyVisible(tag: String): Boolean {
+        rule.waitForIdle()
+
+        val rootRect = rule.onRoot().getUnclippedBoundsInRoot()
+        val itemRect = rule.onNodeWithTag(tag).getUnclippedBoundsInRoot()
+
+        return itemRect.left >= rootRect.left &&
+            itemRect.right <= rootRect.right &&
+            itemRect.top >= rootRect.top &&
+            itemRect.bottom <= rootRect.bottom
+    }
+
+    private fun setupContent(focusRequesterList: List<FocusRequester>) {
+        val focusRequester = FocusRequester()
+        rule.setContent {
+            LazyColumn() {
+                items(3) {
+                    val modifier =
+                        if (it == 0) Modifier.focusRequester(focusRequester)
+                        else Modifier
+                    BasicText(
+                        text = "test-card-$it",
+                        modifier = modifier
+                            .testTag("test-card-$it")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+                item { TestImmersiveList(focusRequesterList) }
+                items(2) {
+                    BasicText(
+                        text = "test-card-${it + 3}",
+                        modifier = Modifier
+                            .testTag("test-card-${it + 3}")
+                            .size(200.dp)
+                            .focusable()
+                    )
+                }
+            }
+        }
+        rule.runOnIdle { focusRequester.requestFocus() }
+    }
+
+    @OptIn(ExperimentalTvMaterialApi::class, ExperimentalAnimationApi::class)
+    @Composable
+    private fun TestImmersiveList(focusRequesterList: List<FocusRequester>) {
+        val frList = remember { focusRequesterList }
+        ImmersiveList(
+            background = { index, _ ->
+                AnimatedContent(targetState = index) {
+                    Box(
+                        Modifier
+                            .testTag("background-$it")
+                            .fillMaxWidth()
+                            .height(400.dp)
+                            .border(2.dp, Color.Black, RectangleShape)
+                    ) {
+                        BasicText("background-$it")
+                    }
+                }
+            },
+            modifier = Modifier.testTag("immersive-list")
+        ) {
+            TvLazyRow {
+                items(frList.count()) { index ->
+                    var modifier = Modifier
+                        .testTag("list-card-$index")
+                        .size(50.dp)
+                    for (item in frList) {
+                        modifier = modifier.focusRequester(frList[index])
+                    }
+                    Box(modifier.focusableItem(index)) { BasicText("list-card-$index") }
+                }
+            }
+        }
+    }
+
     private fun keyPress(keyCode: Int, numberOfPresses: Int = 1) {
         for (index in 0 until numberOfPresses)
             InstrumentationRegistry.getInstrumentation().sendKeyDownUpSync(keyCode)
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt b/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.kt
new file mode 100644
index 0000000..f1b11f5
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ContentColor.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.tv.material
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.graphics.Color
+
+/**
+ * CompositionLocal containing the preferred content color for a given position in the hierarchy.
+ * This typically represents the `on` color for a color in `ColorScheme`. For example, if the
+ * background color is `surface`, this color is typically set to
+ * `onSurface`.
+ *
+ * This color should be used for any typography / iconography, to ensure that the color of these
+ * adjusts when the background color changes. For example, on a dark background, text should be
+ * light, and on a light background, text should be dark.
+ *
+ * Defaults to [Color.Black] if no color has been explicitly set.
+ */
+val LocalContentColor = compositionLocalOf { Color.Black }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
index b0e19e2..ebac64e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/ExperimentalTvMaterialApi.kt
@@ -19,4 +19,5 @@
 @RequiresOptIn(
     "This tv-material API is experimental and likely to change or be removed in the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalTvMaterialApi
\ No newline at end of file
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt b/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
new file mode 100644
index 0000000..a1cefdb
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/Tab.kt
@@ -0,0 +1,239 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.foundation.focusable
+import androidx.compose.foundation.interaction.Interaction
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.selected
+import androidx.compose.ui.semantics.semantics
+
+/**
+ * Material Design tab.
+ *
+ * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different
+ * screens, data sets, and other interactions.
+ *
+ * This should typically be used inside of a [TabRow], see the corresponding documentation for
+ * example usage.
+ *
+ * @param selected whether this tab is selected or not
+ * @param onSelect called when this tab is selected (when in focus). Doesn't trigger if the tab is
+ * already selected
+ * @param modifier the [Modifier] to be applied to this tab
+ * @param enabled controls the enabled state of this tab. When `false`, this component will not
+ * respond to user input, and it will appear visually disabled and disabled to accessibility
+ * services.
+ * @param colors these will be used by the tab when in different states (focused,
+ * selected, etc.)
+ * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
+ * for this tab. You can create and pass in your own `remember`ed instance to observe [Interaction]s
+ * and customize the appearance / behavior of this tab in different states.
+ * @param content content of the [Tab]
+ */
+@Composable
+fun Tab(
+  selected: Boolean,
+  onSelect: () -> Unit,
+  modifier: Modifier = Modifier,
+  enabled: Boolean = true,
+  colors: TabColors = TabDefaults.pillIndicatorTabColors(),
+  interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+  content: @Composable RowScope.() -> Unit
+) {
+  val contentColor by
+    animateColorAsState(
+      getTabContentColor(
+        colors = colors,
+        anyTabFocused = LocalTabRowHasFocus.current,
+        selected = selected,
+        enabled = enabled,
+      )
+    )
+  CompositionLocalProvider(LocalContentColor provides contentColor) {
+    Row(
+      modifier =
+        modifier
+          .semantics {
+            this.selected = selected
+            this.role = Role.Tab
+          }
+          .onFocusChanged {
+            if (it.isFocused && !selected) {
+              onSelect()
+            }
+          }
+          .focusable(enabled, interactionSource),
+      horizontalArrangement = Arrangement.Center,
+      verticalAlignment = Alignment.CenterVertically,
+      content = content
+    )
+  }
+}
+
+/**
+ * Represents the colors used in a tab in different states.
+ *
+ * - See [TabDefaults.pillIndicatorTabColors] for the default colors used in a [Tab] when using a
+ * Pill indicator.
+ * - See [TabDefaults.underlinedIndicatorTabColors] for the default colors used in a [Tab] when
+ * using an Underlined indicator
+ */
+class TabColors
+internal constructor(
+  private val activeContentColor: Color,
+  private val selectedContentColor: Color,
+  private val focusedContentColor: Color,
+  private val disabledActiveContentColor: Color,
+  private val disabledSelectedContentColor: Color,
+) {
+  /**
+   * Represents the content color for this tab, depending on whether it is inactive and [enabled]
+   *
+   * [Tab] is inactive when the [TabRow] is not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun inactiveContentColor(enabled: Boolean): Color {
+    return if (enabled) activeContentColor.copy(alpha = 0.4f)
+    else disabledActiveContentColor.copy(alpha = 0.4f)
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is active and [enabled]
+   *
+   * [Tab] is active when some other [Tab] is focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun activeContentColor(enabled: Boolean): Color {
+    return if (enabled) activeContentColor else disabledActiveContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is selected and [enabled]
+   *
+   * [Tab] is selected when the current [Tab] is selected and not focused
+   *
+   * @param enabled whether the button is enabled
+   */
+  internal fun selectedContentColor(enabled: Boolean): Color {
+    return if (enabled) selectedContentColor else disabledSelectedContentColor
+  }
+
+  /**
+   * Represents the content color for this tab, depending on whether it is focused
+   *
+   * * [Tab] is focused when the current [Tab] is selected and focused
+   */
+  internal fun focusedContentColor(): Color {
+    return focusedContentColor
+  }
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (other == null || other !is TabColors) return false
+
+    if (activeContentColor != other.activeContentColor(true)) return false
+    if (selectedContentColor != other.selectedContentColor(true)) return false
+    if (focusedContentColor != other.focusedContentColor()) return false
+
+    if (disabledActiveContentColor != other.activeContentColor(false)) return false
+    if (disabledSelectedContentColor != other.selectedContentColor(false)) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = activeContentColor.hashCode()
+    result = 31 * result + selectedContentColor.hashCode()
+    result = 31 * result + focusedContentColor.hashCode()
+    result = 31 * result + disabledActiveContentColor.hashCode()
+    result = 31 * result + disabledSelectedContentColor.hashCode()
+    return result
+  }
+}
+
+object TabDefaults {
+  /**
+   * [Tab]'s content colors to in conjunction with underlined indicator
+   */
+  // TODO: get selected & focused values from theme
+  @Composable
+  fun underlinedIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    selectedContentColor: Color = Color(0xFFC9C2E8),
+    focusedContentColor: Color = Color(0xFFC9BFFF),
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+
+  /**
+   * [Tab]'s content colors to in conjunction with pill indicator
+   */
+  // TODO: get selected & focused values from theme
+  @Composable
+  fun pillIndicatorTabColors(
+    activeContentColor: Color = LocalContentColor.current,
+    selectedContentColor: Color = Color(0xFFE5DEFF),
+    focusedContentColor: Color = Color(0xFF313033),
+    disabledActiveContentColor: Color = activeContentColor,
+    disabledSelectedContentColor: Color = selectedContentColor,
+  ): TabColors =
+    TabColors(
+      activeContentColor = activeContentColor,
+      selectedContentColor = selectedContentColor,
+      focusedContentColor = focusedContentColor,
+      disabledActiveContentColor = disabledActiveContentColor,
+      disabledSelectedContentColor = disabledSelectedContentColor,
+    )
+}
+
+/** Returns the [Tab]'s content color based on focused/selected state */
+private fun getTabContentColor(
+  colors: TabColors,
+  anyTabFocused: Boolean,
+  selected: Boolean,
+  enabled: Boolean,
+): Color =
+  when {
+    anyTabFocused && selected -> colors.focusedContentColor()
+    selected -> colors.selectedContentColor(enabled)
+    anyTabFocused -> colors.activeContentColor(enabled)
+    else -> colors.inactiveContentColor(enabled)
+  }
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt b/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
new file mode 100644
index 0000000..9fb0765
--- /dev/null
+++ b/tv/tv-material/src/main/java/androidx/tv/material/TabRow.kt
@@ -0,0 +1,275 @@
+/*
+ * 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.tv.material
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.selection.selectableGroup
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.focus.onFocusChanged
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.DpRect
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
+import androidx.compose.ui.zIndex
+
+/**
+ * TV-Material Design Horizontal TabRow
+ *
+ * Display all tabs in a set simultaneously and if the tabs exceed the container size, it has
+ * scrolling to navigate to next tab. They are best for switching between related content quickly,
+ * such as between transportation methods in a map. To navigate between tabs, use d-pad left or
+ * d-pad right when focused.
+ *
+ * A TvTabRow contains a row of []s, and displays an indicator underneath the currently selected
+ * tab. A TvTabRow places its tabs offset from the starting edge, and allows scrolling to tabs that
+ * are placed off screen.
+ *
+ * @param selectedTabIndex the index of the currently selected tab
+ * @param modifier the [Modifier] to be applied to this tab row
+ * @param containerColor the color used for the background of this tab row
+ * @param contentColor the primary color used in the tabs
+ * @param separator use this composable to add a separator between the tabs
+ * @param indicator used to indicate which tab is currently selected and/or focused
+ * @param tabs a composable which will render all the tabs
+ */
+@Composable
+fun TabRow(
+  selectedTabIndex: Int,
+  modifier: Modifier = Modifier,
+  containerColor: Color = TabRowDefaults.ContainerColor,
+  contentColor: Color = TabRowDefaults.contentColor(),
+  separator: @Composable () -> Unit = { TabRowDefaults.TabSeparator() },
+  indicator: @Composable (tabPositions: List<DpRect>) -> Unit =
+    @Composable { tabPositions ->
+      TabRowDefaults.PillIndicator(currentTabPosition = tabPositions[selectedTabIndex])
+    },
+  tabs: @Composable () -> Unit
+) {
+  val scrollState = rememberScrollState()
+  var isAnyTabFocused by remember { mutableStateOf(false) }
+
+  CompositionLocalProvider(
+    LocalTabRowHasFocus provides isAnyTabFocused,
+    LocalContentColor provides contentColor
+  ) {
+    SubcomposeLayout(
+      modifier =
+        modifier
+          .background(containerColor)
+          .clipToBounds()
+          .horizontalScroll(scrollState)
+          .onFocusChanged { isAnyTabFocused = it.hasFocus }
+          .selectableGroup()
+    ) { constraints ->
+      // Tab measurables
+      val tabMeasurables = subcompose(TabRowSlots.Tabs, tabs)
+
+      // Tab placeables
+      val tabPlaceables =
+        tabMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val tabsCount = tabMeasurables.size
+      val separatorsCount = tabsCount - 1
+
+      // Separators
+      val separators = @Composable { repeat(separatorsCount) { separator() } }
+      val separatorMeasurables = subcompose(TabRowSlots.Separator, separators)
+      val separatorPlaceables =
+        separatorMeasurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
+      val separatorWidth = separatorPlaceables.first().width
+
+      val layoutWidth = tabPlaceables.sumOf { it.width } + separatorsCount * separatorWidth
+      val layoutHeight =
+        (tabMeasurables.maxOfOrNull { it.maxIntrinsicHeight(Constraints.Infinity) } ?: 0)
+          .coerceAtLeast(0)
+
+      // Position the children
+      layout(layoutWidth, layoutHeight) {
+
+        // Place the tabs
+        val tabPositions = mutableListOf<DpRect>()
+        var left = 0
+        tabPlaceables.forEachIndexed { index, tabPlaceable ->
+          // place the tab
+          tabPlaceable.placeRelative(left, 0)
+
+          tabPositions.add(
+            this@SubcomposeLayout.buildTabPosition(placeable = tabPlaceable, initialLeft = left)
+          )
+          left += tabPlaceable.width
+
+          // place the separator
+          if (tabPlaceables.lastIndex != index) {
+            separatorPlaceables[index].placeRelative(left, 0)
+          }
+
+          left += separatorWidth
+        }
+
+        // Place the indicator
+        subcompose(TabRowSlots.Indicator) { indicator(tabPositions) }
+          .forEach { it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) }
+      }
+    }
+  }
+}
+
+object TabRowDefaults {
+  /** Color of the background of a tab */
+  val ContainerColor = Color.Transparent
+
+  /** Space between tabs in the tab row */
+  @Composable
+  fun TabSeparator() {
+    Spacer(modifier = Modifier.width(20.dp))
+  }
+
+  /** Default accent color for the TabRow */
+  // TODO: Use value from a theme
+  @Composable fun contentColor(): Color = Color(0xFFC9C5D0)
+
+  /**
+   * Adds a pill indicator behind the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun PillIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFE5E1E6),
+    inactiveColor: Color = Color(0xFF484362).copy(alpha = 0.4f)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val width by animateDpAsState(targetValue = currentTabPosition.width)
+    val height = currentTabPosition.height
+    val leftOffset by animateDpAsState(targetValue = currentTabPosition.left)
+    val topOffset = currentTabPosition.top
+
+    val pillColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset, y = topOffset)
+        .width(width)
+        .height(height)
+        .background(color = pillColor, shape = RoundedCornerShape(50))
+        .zIndex(-1f)
+    )
+  }
+
+  /**
+   * Adds an underlined indicator below the tab
+   *
+   * @param currentTabPosition position of the current selected tab
+   * @param modifier modifier to be applied to the indicator
+   * @param activeColor color of indicator when [TabRow] is active
+   * @param inactiveColor color of indicator when [TabRow] is inactive
+   */
+  @Composable
+  fun UnderlinedIndicator(
+    currentTabPosition: DpRect,
+    modifier: Modifier = Modifier,
+    activeColor: Color = Color(0xFFC9BFFF),
+    inactiveColor: Color = Color(0xFFC9C2E8)
+  ) {
+    val anyTabFocused = LocalTabRowHasFocus.current
+    val unfocusedUnderlineWidth = 10.dp
+    val indicatorHeight = 2.dp
+    val width by
+      animateDpAsState(
+        targetValue = if (anyTabFocused) currentTabPosition.width else unfocusedUnderlineWidth
+      )
+    val leftOffset by
+      animateDpAsState(
+        targetValue =
+          if (anyTabFocused) {
+            currentTabPosition.left
+          } else {
+            val tabCenter = currentTabPosition.left + currentTabPosition.width / 2
+            tabCenter - unfocusedUnderlineWidth / 2
+          }
+      )
+
+    val underlineColor by
+      animateColorAsState(targetValue = if (anyTabFocused) activeColor else inactiveColor)
+
+    Box(
+      modifier
+        .fillMaxWidth()
+        .wrapContentSize(Alignment.BottomStart)
+        .offset(x = leftOffset)
+        .width(width)
+        .height(indicatorHeight)
+        .background(color = underlineColor)
+    )
+  }
+}
+
+/** A provider to store whether any [Tab] is focused inside the [TabRow] */
+internal val LocalTabRowHasFocus = compositionLocalOf { false }
+
+/** Slots for [TabRow]'s content */
+private enum class TabRowSlots {
+  Tabs,
+  Indicator,
+  Separator
+}
+
+/** Builds TabPosition based on placeable */
+private fun Density.buildTabPosition(
+  placeable: Placeable,
+  initialLeft: Int = 0,
+  initialTop: Int = 0,
+): DpRect =
+  DpRect(
+    left = initialLeft.toDp(),
+    right = (initialLeft + placeable.width).toDp(),
+    top = initialTop.toDp(),
+    bottom = (initialTop + placeable.height).toDp(),
+  )
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
index cc77d09..2b37161 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/Carousel.kt
@@ -345,11 +345,11 @@
         if (slideCount <= 0) {
             Box(modifier = modifier)
         } else {
-            val defaultSize = 8.dp
-            val inactiveColor = Color.LightGray
-            val activeColor = Color.White
-            val shape = CircleShape
-            val indicatorModifier = Modifier.size(defaultSize)
+            val defaultSize = remember { 8.dp }
+            val inactiveColor = remember { Color.LightGray }
+            val activeColor = remember { Color.White }
+            val shape = remember { CircleShape }
+            val indicatorModifier = remember { Modifier.size(defaultSize) }
 
             Box(modifier = modifier) {
                 Row(
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
index 0a58e20..9fa9d6e 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/carousel/CarouselItem.kt
@@ -22,13 +22,17 @@
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
@@ -41,6 +45,7 @@
 import androidx.tv.material.ExperimentalTvMaterialApi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
 
 /**
  * This composable is intended for use in Carousel.
@@ -57,7 +62,7 @@
  * @param overlay composable defining the content overlaid on the background.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun CarouselItem(
@@ -72,22 +77,35 @@
     val overlayVisible = remember { MutableTransitionState(initialState = false) }
     var focusState: FocusState? by remember { mutableStateOf(null) }
     val focusManager = LocalFocusManager.current
+    val bringIntoViewRequester = remember { BringIntoViewRequester() }
+    val coroutineScope = rememberCoroutineScope()
 
     LaunchedEffect(overlayVisible) {
         snapshotFlow { overlayVisible.isIdle && overlayVisible.currentState }.first { it }
         // slide has loaded completely.
         if (focusState?.isFocused == true) {
-	    focusManager.moveFocus(FocusDirection.Enter)
+            // Using bringIntoViewRequester here instead of in Carousel.kt as when the focusable
+            // item is within an animation, bringIntoView scrolls excessively and loses focus.
+            // b/241591211
+            // By using bringIntoView inside the snapshotFlow, we ensure that the focusable has
+            // completed animating into position.
+            bringIntoViewRequester.bringIntoView()
+            focusManager.moveFocus(FocusDirection.Enter)
         }
     }
 
     Box(modifier = modifier
-            .onFocusChanged {
-                focusState = it
-                if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+        .bringIntoViewRequester(bringIntoViewRequester)
+        .onFocusChanged {
+            focusState = it
+            if (it.isFocused && overlayVisible.isIdle && overlayVisible.currentState) {
+                coroutineScope.launch {
+                    bringIntoViewRequester.bringIntoView()
                     focusManager.moveFocus(FocusDirection.Enter)
                 }
-             }.focusable()) {
+            }
+        }
+        .focusable()) {
         background()
 
         LaunchedEffect(overlayVisible) {
diff --git a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
index 091c16e..592bbf5 100644
--- a/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
+++ b/tv/tv-material/src/main/java/androidx/tv/material/immersivelist/ImmersiveList.kt
@@ -26,14 +26,18 @@
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.with
+import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.relocation.BringIntoViewRequester
+import androidx.compose.foundation.relocation.bringIntoViewRequester
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -42,6 +46,7 @@
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.tv.material.ExperimentalTvMaterialApi
+import kotlinx.coroutines.launch
 
 /**
  * Immersive List consists of a list with multiple items and a background that displays content
@@ -57,7 +62,7 @@
  * @param list composable defining the list of items that has to be rendered.
  */
 @Suppress("IllegalExperimentalApiUsage")
-@OptIn(ExperimentalComposeUiApi::class)
+@OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)
 @ExperimentalTvMaterialApi
 @Composable
 fun ImmersiveList(
@@ -69,8 +74,19 @@
 ) {
     var currentItemIndex by remember { mutableStateOf(0) }
     var listHasFocus by remember { mutableStateOf(false) }
+    val bringIntoViewRequester = remember { BringIntoViewRequester() }
+    val coroutineScope = rememberCoroutineScope()
 
-    Box(modifier) {
+    Box(modifier
+            .bringIntoViewRequester(bringIntoViewRequester)
+            .onFocusChanged {
+                if (it.isFocused) {
+                    coroutineScope.launch {
+                        bringIntoViewRequester.bringIntoView()
+                    }
+                }
+            }
+    ) {
         ImmersiveListBackgroundScope(this).background(currentItemIndex, listHasFocus)
 
         val focusManager = LocalFocusManager.current
diff --git a/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt b/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
index 70aa8ed..a328f50 100644
--- a/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
+++ b/viewpager2/integration-tests/targetsdk-tests/src/androidTest/kotlin/androidx/viewpager2/integration/targetsdktests/OnApplyWindowInsetsListenerTest.kt
@@ -58,15 +58,12 @@
 
     companion object {
         private const val numPages = 3
-        private var mSystemWindowInsetsConsumedField: Field? = null
-
-        init {
+        private val mSystemWindowInsetsConsumedField: Field? by lazy {
             // Only need reflection on API < 29 to create an unconsumed WindowInsets.
             // On API 29+, a new builder is used that will do that for us.
             if (Build.VERSION.SDK_INT < 29) {
-                mSystemWindowInsetsConsumedField = field("mSystemWindowInsetsConsumed")
-                mSystemWindowInsetsConsumedField!!.isAccessible = true
-            }
+                field("mSystemWindowInsetsConsumed").also { it.isAccessible = true }
+            } else null
         }
 
         @Suppress("SameParameterValue")
diff --git a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
index ebe6b1b..d781ba1 100644
--- a/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
+++ b/wear/compose/compose-foundation/src/androidMain/kotlin/androidx/wear/compose/foundation/CurvedTextDelegate.android.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontSynthesis
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.resolveAsTypeface
 import androidx.compose.ui.text.style.TextOverflow
 import kotlin.math.roundToInt
 
@@ -90,12 +91,12 @@
         val fontFamilyResolver = LocalFontFamilyResolver.current
         typeFace = remember(fontFamily, fontWeight, fontStyle, fontSynthesis, fontFamilyResolver) {
             derivedStateOf {
-                fontFamilyResolver.resolve(
+                fontFamilyResolver.resolveAsTypeface(
                     fontFamily,
                     fontWeight ?: FontWeight.Normal,
                     fontStyle ?: FontStyle.Normal,
                     fontSynthesis ?: FontSynthesis.All
-                ).value as Typeface
+                ).value
             }
         }
         updateTypeFace()
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 7c9473c..c87b5b0 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -288,6 +288,8 @@
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void setNumberOfOptions(int);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public boolean isScrollInProgress;
     property public final int numberOfOptions;
     property public final boolean repeatItems;
@@ -472,6 +474,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 1180309..7b1c7bc8 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -189,7 +189,7 @@
   public final class DefaultTimeSourceKt {
   }
 
-  @kotlin.RequiresOptIn(message="This Wear Material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalWearMaterialApi {
+  @kotlin.RequiresOptIn(message="This Wear Material API is experimental and is likely to change or to be removed in" + " the future.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) public @interface ExperimentalWearMaterialApi {
   }
 
   @androidx.compose.runtime.Immutable @androidx.wear.compose.material.ExperimentalWearMaterialApi public final class FixedThreshold implements androidx.wear.compose.material.ThresholdConfig {
@@ -304,6 +304,8 @@
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void setNumberOfOptions(int);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public boolean isScrollInProgress;
     property public final int numberOfOptions;
     property public final boolean repeatItems;
@@ -520,6 +522,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 7c9473c..c87b5b0 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -288,6 +288,8 @@
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public void setNumberOfOptions(int);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public boolean isScrollInProgress;
     property public final int numberOfOptions;
     property public final boolean repeatItems;
@@ -472,6 +474,8 @@
     method public boolean isScrollInProgress();
     method public suspend Object? scroll(androidx.compose.foundation.MutatePriority scrollPriority, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.gestures.ScrollScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public suspend Object? scrollToItem(int index, optional int scrollOffset, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public boolean canScrollBackward;
+    property public boolean canScrollForward;
     property public final int centerItemIndex;
     property public final int centerItemScrollOffset;
     property public boolean isScrollInProgress;
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
index 4ea531b..e681cd5 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PositionIndicatorTest.kt
@@ -106,6 +106,15 @@
 
     @Test
     fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun scalingLazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: ScalingLazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -121,7 +130,7 @@
                 autoCentering = null
             ) {
                 items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
+                    Box(Modifier.requiredSize(itemSize))
                 }
             }
             PositionIndicator(
@@ -444,6 +453,15 @@
 
     @Test
     fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun lazyColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: LazyListState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -458,7 +476,7 @@
                     .requiredSize(itemSizeDp * 3.5f + itemSpacingDp * 2.5f)
             ) {
                 items(3) {
-                    Box(Modifier.requiredSize(itemSizeDp))
+                    Box(Modifier.requiredSize(itemSize))
                 }
             }
             PositionIndicator(
@@ -759,6 +777,15 @@
 
     @Test
     fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize() {
+        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSizeDp)
+    }
+
+    @Test
+    fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSizeForZeroSizeItems() {
+        scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(0.dp)
+    }
+
+    private fun scrollableColumnNotLargeEnoughToScrollGivesCorrectPositionAndSize(itemSize: Dp) {
         lateinit var state: ScrollState
         lateinit var positionIndicatorState: PositionIndicatorState
         var viewPortHeight = 0
@@ -772,9 +799,9 @@
                     .verticalScroll(state = state),
                 verticalArrangement = Arrangement.spacedBy(itemSpacingDp)
             ) {
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
-                Box(Modifier.requiredSize(itemSizeDp))
+                Box(Modifier.requiredSize(itemSize))
+                Box(Modifier.requiredSize(itemSize))
+                Box(Modifier.requiredSize(itemSize))
             }
             PositionIndicator(
                 state = positionIndicatorState,
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
index b43f0d2..c81b71a 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
@@ -35,6 +35,7 @@
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.FlakyTest
 import androidx.test.filters.MediumTest
 import org.junit.Rule
 import org.junit.Test
@@ -46,6 +47,7 @@
     @get:Rule
     val rule = createComposeRule()
 
+    @FlakyTest(bugId = 259134313)
     @Test
     fun hidesTimeTextWithScalingLazyColumn() {
         lateinit var scrollState: ScalingLazyListState
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
index 730437c..028dd52 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ExperimentalWearMaterialApi.kt
@@ -20,4 +20,5 @@
     "This Wear Material API is experimental and is likely to change or to be removed in" +
         " the future."
 )
+@Retention(AnnotationRetention.BINARY)
 annotation class ExperimentalWearMaterialApi
\ No newline at end of file
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 f2d2d66..3434c10 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
@@ -456,6 +456,12 @@
     public override val isScrollInProgress: Boolean
         get() = scalingLazyListState.isScrollInProgress
 
+    override val canScrollForward: Boolean
+        get() = scalingLazyListState.canScrollForward
+
+    override val canScrollBackward: Boolean
+        get() = scalingLazyListState.canScrollBackward
+
     private fun verifyNumberOfOptions(numberOfOptions: Int) {
         require(numberOfOptions > 0) { "The picker should have at least one item." }
         require(numberOfOptions < LARGE_NUMBER_OF_ITEMS / 3) {
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
index 7d15403..ecd6bde 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/PositionIndicator.kt
@@ -462,63 +462,63 @@
         ) {
             Box(
                 modifier = Modifier
-                    .fillMaxSize()
-                    .drawWithContent {
-                        // We need to invert reverseDirection when the screen is round and we are on
-                        // the left.
+                .fillMaxSize()
+                .drawWithContent {
+                    // We need to invert reverseDirection when the screen is round and we are on
+                    // the left.
                         val actualReverseDirection =
                             if (isScreenRound && !indicatorOnTheRight) {
-                                !reverseDirection
-                            } else {
-                                reverseDirection
-                            }
+                        !reverseDirection
+                    } else {
+                        reverseDirection
+                    }
 
-                        val indicatorPosition = if (actualReverseDirection) {
-                            1 - animatedDisplayState.value.position
-                        } else {
-                            animatedDisplayState.value.position
-                        }
+                    val indicatorPosition = if (actualReverseDirection) {
+                        1 - animatedDisplayState.value.position
+                    } else {
+                        animatedDisplayState.value.position
+                    }
 
-                        val indicatorWidthPx = indicatorWidth.toPx()
+                    val indicatorWidthPx = indicatorWidth.toPx()
 
-                        // We want position = 0 be the indicator aligned at the top of its area and
-                        // position = 1 be aligned at the bottom of the area.
+                    // We want position = 0 be the indicator aligned at the top of its area and
+                    // position = 1 be aligned at the bottom of the area.
                         val indicatorStart =
                             indicatorPosition * (1 - animatedDisplayState.value.size)
 
                         val diameter = max(containerSize.width, containerSize.height)
 
-                        val paddingHorizontalPx = paddingHorizontal.toPx()
-                        if (isScreenRound) {
-                            val usableHalf = diameter / 2f - paddingHorizontalPx
-                            val sweepDegrees =
-                                (2 * asin((indicatorHeight.toPx() / 2) / usableHalf)).toDegrees()
+                    val paddingHorizontalPx = paddingHorizontal.toPx()
+                    if (isScreenRound) {
+                        val usableHalf = diameter / 2f - paddingHorizontalPx
+                        val sweepDegrees =
+                            (2 * asin((indicatorHeight.toPx() / 2) / usableHalf)).toDegrees()
 
-                            drawCurvedIndicator(
-                                color,
-                                background,
-                                paddingHorizontalPx,
-                                indicatorOnTheRight,
-                                sweepDegrees,
-                                indicatorWidthPx,
-                                indicatorStart,
-                                animatedDisplayState.value.size,
-                                highlightAlpha.value
-                            )
-                        } else {
-                            drawStraightIndicator(
-                                color,
-                                background,
-                                paddingHorizontalPx,
-                                indicatorOnTheRight,
-                                indicatorWidthPx,
-                                indicatorHeightPx = indicatorHeight.toPx(),
-                                indicatorStart,
-                                animatedDisplayState.value.size,
-                                highlightAlpha.value
-                            )
-                        }
+                        drawCurvedIndicator(
+                            color,
+                            background,
+                            paddingHorizontalPx,
+                            indicatorOnTheRight,
+                            sweepDegrees,
+                            indicatorWidthPx,
+                            indicatorStart,
+                            animatedDisplayState.value.size,
+                            highlightAlpha.value
+                        )
+                    } else {
+                        drawStraightIndicator(
+                            color,
+                            background,
+                            paddingHorizontalPx,
+                            indicatorOnTheRight,
+                            indicatorWidthPx,
+                            indicatorHeightPx = indicatorHeight.toPx(),
+                            indicatorStart,
+                            animatedDisplayState.value.size,
+                            highlightAlpha.value
+                        )
                     }
+                }
             )
         }
     }
@@ -730,8 +730,10 @@
         // of list item offset.
         val lastItemEndOffset = lastItem.startOffset(state.anchorType.value!!) + lastItem.size
         val viewportEndOffset = state.viewportHeightPx.value!! / 2f
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         val lastItemVisibleFraction =
-            (1f - ((lastItemEndOffset - viewportEndOffset) / lastItem.size)).coerceAtMost(1f)
+            (1f - ((lastItemEndOffset - viewportEndOffset) /
+                lastItem.size.coerceAtLeast(1))).coerceAtMost(1f)
 
         return lastItem.index.toFloat() + lastItemVisibleFraction
     }
@@ -751,8 +753,10 @@
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
         val firstItemStartOffset = firstItem.startOffset(state.anchorType.value!!)
         val viewportStartOffset = - (state.viewportHeightPx.value!! / 2f)
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         val firstItemInvisibleFraction =
-            ((viewportStartOffset - firstItemStartOffset) / firstItem.size).coerceAtLeast(0f)
+            ((viewportStartOffset - firstItemStartOffset) /
+                firstItem.size.coerceAtLeast(1)).coerceAtLeast(0f)
 
         return firstItem.index.toFloat() + firstItemInvisibleFraction
     }
@@ -823,18 +827,22 @@
     private fun decimalLastItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val lastItem = state.layoutInfo.visibleItemsInfo.last()
+        // Coerce item sizes to at least 1 to avoid divide by zero for zero height items
         val lastItemVisibleSize =
-            (state.layoutInfo.viewportEndOffset - lastItem.offset).coerceAtMost(lastItem.size)
+            (state.layoutInfo.viewportEndOffset - lastItem.offset)
+                .coerceAtMost(lastItem.size).coerceAtLeast(1)
         return lastItem.index.toFloat() +
-            lastItemVisibleSize.toFloat() / lastItem.size.toFloat()
+            lastItemVisibleSize.toFloat() / lastItem.size.coerceAtLeast(1).toFloat()
     }
 
     private fun decimalFirstItemIndex(): Float {
         if (state.layoutInfo.visibleItemsInfo.isEmpty()) return 0f
         val firstItem = state.layoutInfo.visibleItemsInfo.first()
         val firstItemOffset = firstItem.offset - state.layoutInfo.viewportStartOffset
+        // Coerce item size to at least 1 to avoid divide by zero for zero height items
         return firstItem.index.toFloat() -
-            firstItemOffset.coerceAtMost(0).toFloat() / firstItem.size.toFloat()
+            firstItemOffset.coerceAtMost(0).toFloat() /
+            firstItem.size.coerceAtLeast(1).toFloat()
     }
 }
 
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
index 123c967..a8ef4a8 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScalingLazyListState.kt
@@ -403,6 +403,11 @@
         lazyListState.scroll(scrollPriority = scrollPriority, block = block)
     }
 
+    override val canScrollForward: Boolean
+        get() = lazyListState.canScrollForward
+
+    override val canScrollBackward: Boolean
+        get() = lazyListState.canScrollBackward
     /**
      * Instantly brings the item at [index] to the center of the viewport and positions it based on
      * the [anchorType] and applies the [scrollOffset] pixels.
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 7b38d00..84af177e 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
@@ -17,7 +17,6 @@
 package androidx.wear.compose.material
 
 import androidx.compose.animation.core.AnimationSpec
-import androidx.compose.animation.core.CubicBezierEasing
 import androidx.compose.animation.core.LinearOutSlowInEasing
 import androidx.compose.animation.core.TweenSpec
 import androidx.compose.foundation.background
@@ -154,12 +153,7 @@
 
                 val translationX = if (squeezeMode) squeezeOffset else slideOffset
 
-                val backgroundAlpha =
-                    lerp(
-                        MAX_BACKGROUND_SCRIM_ALPHA,
-                        MIN_BACKGROUND_SCRIM_ALPHA,
-                        BACKGROUND_SCRIM_EASING.transform(progress)
-                    )
+                val backgroundAlpha = MAX_BACKGROUND_SCRIM_ALPHA * (1 - progress)
                 val contentScrimAlpha = min(MAX_CONTENT_SCRIM_ALPHA, progress / 2f)
 
                 Modifiers(
@@ -590,8 +584,6 @@
 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 const val MAX_BACKGROUND_SCRIM_ALPHA = 0.5f
 private val SWIPE_TO_DISMISS_BOX_ANIMATION_SPEC =
     TweenSpec<Float>(200, 0, LinearOutSlowInEasing)
diff --git a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
index 0162522..593789a 100644
--- a/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
+++ b/wear/watchface/watchface-client/src/androidTest/AndroidManifest.xml
@@ -19,6 +19,9 @@
         <service android:name="androidx.wear.watchface.client.test.WatchFaceControlTestService"/>
         <service android:name="androidx.wear.watchface.client.test.TestLifeCycleWatchFaceService"/>
         <service android:name="androidx.wear.watchface.client.test.TestNopCanvasWatchFaceService"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceA"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceB"/>
+        <service android:name="androidx.wear.watchface.client.test.ObservableServiceC"/>
         <service
             android:name="androidx.wear.watchface.client.test.OutdatedWatchFaceControlTestService">
             <meta-data android:name="androidx.wear.watchface.xml_version" android:value="99999" />
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt
new file mode 100644
index 0000000..864dba7
--- /dev/null
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/ObservableServices.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.client.test
+
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+open class ObservableService(private val latch: CountDownLatch) : Service() {
+    private val binder: IBinder = LocalBinder()
+
+    inner class LocalBinder : Binder() {
+        val service: ObservableService
+            get() = this@ObservableService
+    }
+
+    override fun onBind(intent: Intent?) = binder
+
+    override fun onCreate() {
+        super.onCreate()
+        latch.countDown()
+        this.stopSelf()
+    }
+}
+
+class ObservableServiceA : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceA::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
+
+class ObservableServiceB : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceB::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
+
+class ObservableServiceC : ObservableService(latch) {
+    companion object {
+        private var latch = CountDownLatch(1)
+
+        /**
+         * Awaits for up to [maxDurationMillis] milliseconds the service to be bound.
+         * @return True if the service is bound before the time runs out, or false otherwise.
+         */
+        fun awaitForServiceToBeBound(maxDurationMillis: Long): Boolean =
+            latch.await(maxDurationMillis, TimeUnit.MILLISECONDS)
+
+        fun reset() {
+            latch = CountDownLatch(1)
+        }
+
+        fun createPendingIntent(context: Context) = PendingIntent.getService(
+            context,
+            101,
+            Intent(context, ObservableServiceC::class.java),
+            PendingIntent.FLAG_IMMUTABLE
+        )
+    }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
index 0d06e96..53b3eb5 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/TestWatchFaceServices.kt
@@ -131,6 +131,16 @@
         )
         return watchFace
     }
+
+    companion object {
+        var systemTimeMillis = 1000000000L
+    }
+
+    override fun getSystemTimeProvider() = object : SystemTimeProvider {
+        override fun getSystemTimeMillis() = systemTimeMillis
+
+        override fun getSystemTimeZoneId() = ZoneId.of("UTC")
+    }
 }
 
 internal class TestExampleOpenGLBackgroundInitWatchFaceService(
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 a10788a..dc02bd9 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
@@ -31,6 +31,7 @@
 import android.os.Looper
 import android.view.Surface
 import android.view.SurfaceHolder
+import androidx.annotation.CallSuper
 import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -43,6 +44,7 @@
 import androidx.wear.watchface.ContentDescriptionLabel
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
+import androidx.wear.watchface.TapType
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceColors
 import androidx.wear.watchface.WatchFaceExperimental
@@ -66,6 +68,8 @@
 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.complications.data.toApiComplicationData
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService.Companion.BLUE_STYLE
@@ -166,7 +170,8 @@
     }
 
     @After
-    fun tearDown() {
+    @CallSuper
+    open fun tearDown() {
         // Interactive instances are not currently shut down when all instances go away. E.g. WCS
         // crashing does not cause the watch face to stop. So we need to shut down explicitly.
         if (this::engine.isInitialized) {
@@ -236,7 +241,52 @@
         }
         return value!!
     }
+
+    /**
+     * Updates the complications for [interactiveInstance] and waits until they have been applied.
+     */
+    protected fun updateComplicationsBlocking(
+        interactiveInstance: InteractiveWatchFaceClient,
+        slotIdToComplicationData: Map<Int, ComplicationData>
+    ) {
+        val slotIdToWatchForUpdates = slotIdToComplicationData.keys.first()
+        var slot: ComplicationSlot
+
+        runBlocking {
+            slot = engine.deferredWatchFaceImpl.await()
+                .complicationSlotsManager.complicationSlots[slotIdToWatchForUpdates]!!
+        }
+
+        val updateCountDownLatch = CountDownLatch(1)
+        handlerCoroutineScope.launch {
+            slot.complicationData.collect { updateCountDownLatch.countDown() }
+        }
+
+        interactiveInstance.updateComplicationData(slotIdToComplicationData)
+        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
+    }
+
+    protected fun tapOnComplication(
+        interactiveInstance: InteractiveWatchFaceClient,
+        slotId: Int
+    ) {
+        val leftClickX = interactiveInstance.complicationSlotsState[slotId]!!.bounds.centerX()
+        val leftClickY = interactiveInstance.complicationSlotsState[slotId]!!.bounds.centerY()
+
+        interactiveInstance.sendTouchEvent(leftClickX, leftClickY, TapType.DOWN)
+        interactiveInstance.sendTouchEvent(leftClickX, leftClickY, TapType.UP)
+    }
 }
+
+fun rangedValueComplicationBuilder() =
+    RangedValueComplicationData.Builder(
+        value = 50.0f,
+        min = 10.0f,
+        max = 100.0f,
+        ComplicationText.EMPTY
+    )
+        .setText(PlainComplicationText.Builder("Battery").build())
+
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 @RequiresApi(Build.VERSION_CODES.O_MR1)
@@ -245,6 +295,14 @@
     private val exampleCanvasAnalogWatchFaceComponentName =
         componentOf<ExampleCanvasAnalogWatchFaceService>()
 
+    @After
+    override fun tearDown() {
+        super.tearDown()
+        ObservableServiceA.reset()
+        ObservableServiceB.reset()
+        ObservableServiceC.reset()
+    }
+
     @Test
     fun complicationProviderDefaults() {
         val wallpaperService = TestComplicationProviderDefaultsWatchFaceService(
@@ -413,36 +471,11 @@
     fun updateComplicationData() {
         val interactiveInstance = getOrCreateTestSubject()
 
-        // Under the hood updateComplicationData is a oneway aidl method so we need to perform some
-        // additional synchronization to ensure it's side effects have been applied before
-        // inspecting complicationSlotsState otherwise we risk test flakes.
-        val updateCountDownLatch = CountDownLatch(1)
-        var leftComplicationSlot: ComplicationSlot
-
-        runBlocking {
-            leftComplicationSlot = engine.deferredWatchFaceImpl.await()
-                .complicationSlotsManager.complicationSlots[
-                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
-            ]!!
-        }
-
-        handlerCoroutineScope.launch {
-            leftComplicationSlot.complicationData.collect {
-                updateCountDownLatch.countDown()
-            }
-        }
-
-        interactiveInstance.updateComplicationData(
+        updateComplicationsBlocking(
+            interactiveInstance,
             mapOf(
                 EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
-                    RangedValueComplicationData.Builder(
-                        50.0f,
-                        10.0f,
-                        100.0f,
-                        ComplicationText.EMPTY
-                    )
-                        .setText(PlainComplicationText.Builder("Battery").build())
-                        .build(),
+                    rangedValueComplicationBuilder().build(),
                 EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID to
                     LongTextComplicationData.Builder(
                         PlainComplicationText.Builder("Test").build(),
@@ -450,7 +483,6 @@
                     ).build()
             )
         )
-        assertTrue(updateCountDownLatch.await(UPDATE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS))
 
         assertThat(interactiveInstance.complicationSlotsState.size).isEqualTo(2)
 
@@ -1092,6 +1124,107 @@
 
         assertThat(lastDisconnectReason).isEqualTo(DisconnectReasons.ENGINE_DETACHED)
     }
+
+    @Test
+    fun tapComplication() {
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(
+            context,
+            surfaceHolder
+        )
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
+        updateComplicationsBlocking(
+            interactiveInstance,
+            mapOf(
+                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+                    rangedValueComplicationBuilder()
+                        .setTapAction(ObservableServiceA.createPendingIntent(context))
+                        .build()
+            )
+        )
+
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(
+            ObservableServiceA.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS)
+        )
+    }
+
+    @Test
+    fun tapTimelineComplication() {
+        val wallpaperService = TestExampleCanvasAnalogWatchFaceService(
+            context,
+            surfaceHolder
+        )
+        val interactiveInstance = getOrCreateTestSubject(wallpaperService)
+        val watchFaceImpl = runBlocking { engine.deferredWatchFaceImpl.await() }
+
+        // Create a timeline complication with three phases, each with their own tap actions leading
+        // to ObservableServiceA, ObservableServiceB & ObservableServiceC getting bound.
+        val timelineComplication = rangedValueComplicationBuilder()
+            .setTapAction(ObservableServiceA.createPendingIntent(context))
+            .build()
+            .asWireComplicationData()
+
+        timelineComplication.setTimelineEntryCollection(
+            listOf(
+                ShortTextComplicationData.Builder(
+                    PlainComplicationText.Builder("B").build(),
+                    ComplicationText.EMPTY
+                )
+                    .setTapAction(ObservableServiceB.createPendingIntent(context))
+                    .build()
+                    .asWireComplicationData().apply {
+                        timelineStartEpochSecond =
+                            10 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                        timelineEndEpochSecond =
+                            20 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                    },
+                ShortTextComplicationData.Builder(
+                    PlainComplicationText.Builder("C").build(),
+                    ComplicationText.EMPTY
+                )
+                    .setTapAction(ObservableServiceC.createPendingIntent(context))
+                    .build()
+                    .asWireComplicationData().apply {
+                        timelineStartEpochSecond =
+                            20 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                        timelineEndEpochSecond =
+                            90 + TestExampleCanvasAnalogWatchFaceService.systemTimeMillis / 1000
+                    }
+            )
+        )
+
+        updateComplicationsBlocking(
+            interactiveInstance,
+            mapOf(
+                EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID to
+                    timelineComplication.toApiComplicationData()
+            )
+        )
+
+        // A tap should initially lead to TapTargetServiceA getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceA.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+
+        // Simulate the passage of time and force timeline entry selection by drawing.
+        TestExampleCanvasAnalogWatchFaceService.systemTimeMillis += 15 * 1000
+        watchFaceImpl.onDraw()
+
+        // A tap should now lead to TapTargetServiceB getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceB.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+
+        // Simulate the passage of time and force timeline entry selection by drawing.
+        TestExampleCanvasAnalogWatchFaceService.systemTimeMillis += 20 * 1000
+        watchFaceImpl.onDraw()
+
+        // A tap should now lead to TapTargetServiceC getting bound.
+        tapOnComplication(interactiveInstance, EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID)
+
+        assertTrue(ObservableServiceC.awaitForServiceToBeBound(UPDATE_TIMEOUT_MILLIS))
+    }
 }
 
 @RunWith(AndroidJUnit4::class)
@@ -1277,7 +1410,8 @@
         val wallpaperService =
             TestExampleOpenGLBackgroundInitWatchFaceService(context, surfaceHolder2)
 
-        val interactiveInstance = getOrCreateTestSubject(wallpaperService,
+        val interactiveInstance = getOrCreateTestSubject(
+            wallpaperService,
             complications = emptyMap()
         )
 
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index 2f7342b..ff7ac43 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -86,6 +86,9 @@
   public final class DataKt {
   }
 
+  public final class DynamicFloatKt {
+  }
+
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
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 565b9cd..c92a5a5 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
@@ -89,6 +89,14 @@
   public final class DataKt {
   }
 
+  @androidx.wear.watchface.complications.data.ComplicationExperimental public abstract class DynamicFloat {
+    ctor public DynamicFloat();
+    method public abstract byte[] asByteArray();
+  }
+
+  public final class DynamicFloatKt {
+  }
+
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
@@ -97,6 +105,7 @@
   @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 @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
     method public androidx.wear.watchface.complications.data.SmallImage? getSmallImage();
     method public float getTargetValue();
@@ -105,6 +114,7 @@
     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 @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
     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;
@@ -117,6 +127,7 @@
 
   @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public static final class GoalProgressComplicationData.Builder {
     ctor public GoalProgressComplicationData.Builder(float value, float targetValue, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public GoalProgressComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, 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);
@@ -265,6 +276,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 @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.DynamicFloat? getDynamicValue();
     method public float getMax();
     method public float getMin();
     method public androidx.wear.watchface.complications.data.MonochromaticImage? getMonochromaticImage();
@@ -275,6 +287,7 @@
     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 @androidx.wear.watchface.complications.data.ComplicationExperimental public final androidx.wear.watchface.complications.data.DynamicFloat? dynamicValue;
     property public final float max;
     property public final float min;
     property public final androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage;
@@ -292,6 +305,7 @@
 
   public static final class RangedValueComplicationData.Builder {
     ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
+    ctor @androidx.wear.watchface.complications.data.ComplicationExperimental public RangedValueComplicationData.Builder(androidx.wear.watchface.complications.data.DynamicFloat dynamicValue, 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);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index 95ce87f..db4540b 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -86,6 +86,9 @@
   public final class DataKt {
   }
 
+  public final class DynamicFloatKt {
+  }
+
   public final class EmptyComplicationData extends androidx.wear.watchface.complications.data.ComplicationData {
     ctor public EmptyComplicationData();
     field public static final androidx.wear.watchface.complications.data.ComplicationType TYPE;
diff --git a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
index 02dc741..19f21c4 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/android/support/wearable/complications/ComplicationData.kt
@@ -13,6 +13,9 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
+@file:OptIn(ComplicationExperimental::class)
+
 package android.support.wearable.complications
 
 import android.annotation.SuppressLint
@@ -31,8 +34,11 @@
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicies
 import androidx.wear.watchface.complications.data.ComplicationDisplayPolicy
+import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicies
 import androidx.wear.watchface.complications.data.ComplicationPersistencePolicy
+import androidx.wear.watchface.complications.data.DynamicFloat
+import androidx.wear.watchface.complications.data.toDynamicFloat
 import java.io.IOException
 import java.io.InvalidObjectException
 import java.io.ObjectInputStream
@@ -171,6 +177,11 @@
             if (isFieldValidForType(FIELD_VALUE, type)) {
                 oos.writeFloat(complicationData.rangedValue)
             }
+            if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+                oos.writeNullable(complicationData.rangedDynamicValue) {
+                    oos.writeByteArray(it.asByteArray())
+                }
+            }
             if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
                 oos.writeInt(complicationData.rangedValueType)
             }
@@ -184,49 +195,16 @@
                 oos.writeFloat(complicationData.targetValue)
             }
             if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
-                val colors = complicationData.colorRamp
-                if (colors != null) {
-                    oos.writeBoolean(true)
-                    oos.writeInt(colors.size)
-                    for (color in colors) {
-                        oos.writeInt(color)
-                    }
-                } else {
-                    oos.writeBoolean(false)
-                }
+                oos.writeNullable(complicationData.colorRamp, oos::writeIntArray)
             }
             if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
-                val isColorRampSmoothShaded = complicationData.isColorRampInterpolated
-                if (isColorRampSmoothShaded != null) {
-                    oos.writeBoolean(true)
-                    oos.writeBoolean(isColorRampSmoothShaded)
-                } else {
-                    oos.writeBoolean(false)
-                }
+                oos.writeNullable(complicationData.isColorRampInterpolated, oos::writeBoolean)
             }
             if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
-                val weights = complicationData.elementWeights
-                if (weights != null) {
-                    oos.writeBoolean(true)
-                    oos.writeInt(weights.size)
-                    for (weight in weights) {
-                        oos.writeFloat(weight)
-                    }
-                } else {
-                    oos.writeBoolean(false)
-                }
+                oos.writeNullable(complicationData.elementWeights, oos::writeFloatArray)
             }
             if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
-                val colors = complicationData.elementColors
-                if (colors != null) {
-                    oos.writeBoolean(true)
-                    oos.writeInt(colors.size)
-                    for (color in colors) {
-                        oos.writeInt(color)
-                    }
-                } else {
-                    oos.writeBoolean(false)
-                }
+                oos.writeNullable(complicationData.elementColors, oos::writeIntArray)
             }
             if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
                 oos.writeInt(complicationData.elementBackgroundColor)
@@ -242,31 +220,13 @@
                 oos.writeInt(complicationData.listStyleHint)
             }
             if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_INTERACTIVE, type)) {
-                val bytes = complicationData.interactiveLayout
-                if (bytes == null) {
-                    oos.writeInt(0)
-                } else {
-                    oos.writeInt(bytes.size)
-                    oos.write(bytes)
-                }
+                oos.writeByteArray(complicationData.interactiveLayout ?: byteArrayOf())
             }
             if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_AMBIENT, type)) {
-                val bytes = complicationData.ambientLayout
-                if (bytes == null) {
-                    oos.writeInt(0)
-                } else {
-                    oos.writeInt(bytes.size)
-                    oos.write(bytes)
-                }
+                oos.writeByteArray(complicationData.ambientLayout ?: byteArrayOf())
             }
             if (isFieldValidForType(EXP_FIELD_PROTO_LAYOUT_RESOURCES, type)) {
-                val bytes = complicationData.layoutResources
-                if (bytes == null) {
-                    oos.writeInt(0)
-                } else {
-                    oos.writeInt(bytes.size)
-                    oos.write(bytes)
-                }
+                oos.writeByteArray(complicationData.layoutResources ?: byteArrayOf())
             }
             if (isFieldValidForType(FIELD_DATA_SOURCE, type)) {
                 val componentName = complicationData.dataSource
@@ -286,32 +246,18 @@
             val end = complicationData.fields.getLong(FIELD_TIMELINE_END_TIME, -1)
             oos.writeLong(end)
             oos.writeInt(complicationData.fields.getInt(FIELD_TIMELINE_ENTRY_TYPE))
-            val listEntries = complicationData.listEntries
-            val listEntriesLength = listEntries?.size ?: 0
-            oos.writeInt(listEntriesLength)
-            if (listEntries != null) {
-                for (data in listEntries) {
-                    SerializedForm(data).writeObject(oos)
-                }
+            oos.writeList(complicationData.listEntries ?: listOf()) {
+                SerializedForm(it).writeObject(oos)
             }
             if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
-                val placeholder = complicationData.placeholder
-                if (placeholder == null) {
-                    oos.writeBoolean(false)
-                } else {
-                    oos.writeBoolean(true)
-                    SerializedForm(placeholder).writeObject(oos)
+                oos.writeNullable(complicationData.placeholder) {
+                    SerializedForm(it).writeObject(oos)
                 }
             }
 
             // This has to be last, since it's recursive.
-            val timeline = complicationData.timelineEntries
-            val timelineLength = timeline?.size ?: 0
-            oos.writeInt(timelineLength)
-            if (timeline != null) {
-                for (data in timeline) {
-                    SerializedForm(data).writeObject(oos)
-                }
+            oos.writeList(complicationData.timelineEntries ?: listOf()) {
+                SerializedForm(it).writeObject(oos)
             }
         }
 
@@ -371,6 +317,11 @@
             if (isFieldValidForType(FIELD_VALUE, type)) {
                 fields.putFloat(FIELD_VALUE, ois.readFloat())
             }
+            if (isFieldValidForType(FIELD_DYNAMIC_VALUE, type)) {
+                ois.readNullable { ois.readByteArray() }?.let {
+                    fields.putByteArray(FIELD_DYNAMIC_VALUE, it)
+                }
+            }
             if (isFieldValidForType(FIELD_VALUE_TYPE, type)) {
                 fields.putInt(FIELD_VALUE_TYPE, ois.readInt())
             }
@@ -383,32 +334,25 @@
             if (isFieldValidForType(FIELD_TARGET_VALUE, type)) {
                 fields.putFloat(FIELD_TARGET_VALUE, ois.readFloat())
             }
-            if (isFieldValidForType(FIELD_COLOR_RAMP, type) && ois.readBoolean()) {
-                val numColors = ois.readInt()
-                val colors = IntArray(numColors)
-                for (i in 0 until numColors) {
-                    colors[i] = ois.readInt()
+            if (isFieldValidForType(FIELD_COLOR_RAMP, type)) {
+                ois.readNullable { ois.readIntArray() }?.let {
+                    fields.putIntArray(FIELD_COLOR_RAMP, it)
                 }
-                fields.putIntArray(FIELD_COLOR_RAMP, colors)
             }
-            if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type) && ois.readBoolean()) {
-                fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, ois.readBoolean())
-            }
-            if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type) && ois.readBoolean()) {
-                val numWeights = ois.readInt()
-                val weights = FloatArray(numWeights)
-                for (i in 0 until numWeights) {
-                    weights[i] = ois.readFloat()
+            if (isFieldValidForType(FIELD_COLOR_RAMP_INTERPOLATED, type)) {
+                ois.readNullable { ois.readBoolean() }?.let {
+                    fields.putBoolean(FIELD_COLOR_RAMP_INTERPOLATED, it)
                 }
-                fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, weights)
             }
-            if (isFieldValidForType(FIELD_ELEMENT_COLORS, type) && ois.readBoolean()) {
-                val numColors = ois.readInt()
-                val colors = IntArray(numColors)
-                for (i in 0 until numColors) {
-                    colors[i] = ois.readInt()
+            if (isFieldValidForType(FIELD_ELEMENT_WEIGHTS, type)) {
+                ois.readNullable { ois.readFloatArray() }?.let {
+                    fields.putFloatArray(FIELD_ELEMENT_WEIGHTS, it)
                 }
-                fields.putIntArray(FIELD_ELEMENT_COLORS, colors)
+            }
+            if (isFieldValidForType(FIELD_ELEMENT_COLORS, type)) {
+                ois.readNullable { ois.readIntArray() }?.let {
+                    fields.putIntArray(FIELD_ELEMENT_COLORS, it)
+                }
             }
             if (isFieldValidForType(FIELD_ELEMENT_BACKGROUND_COLOR, type)) {
                 fields.putInt(FIELD_ELEMENT_BACKGROUND_COLOR, ois.readInt())
@@ -475,47 +419,29 @@
             if (timelineEntryType != 0) {
                 fields.putInt(FIELD_TIMELINE_ENTRY_TYPE, timelineEntryType)
             }
-            val listEntriesLength = ois.readInt()
-            if (listEntriesLength != 0) {
-                val parcels = arrayOfNulls<Parcelable>(listEntriesLength)
-                for (i in 0 until listEntriesLength) {
-                    val entry = SerializedForm()
-                    entry.readObject(ois)
-                    parcels[i] = entry.complicationData!!.fields
-                }
-                fields.putParcelableArray(EXP_FIELD_LIST_ENTRIES, parcels)
-            }
+            ois.readList { SerializedForm().apply { readObject(ois) } }
+                .map { it.complicationData!!.fields }
+                .takeIf { it.isNotEmpty() }
+                ?.let { fields.putParcelableArray(EXP_FIELD_LIST_ENTRIES, it.toTypedArray()) }
             if (isFieldValidForType(FIELD_PLACEHOLDER_FIELDS, type)) {
-                if (ois.readBoolean()) {
-                    val serializedPlaceholder = SerializedForm()
-                    serializedPlaceholder.readObject(ois)
-                    fields.putInt(
-                        FIELD_PLACEHOLDER_TYPE,
-                        serializedPlaceholder.complicationData!!.type
-                    )
-                    fields.putBundle(
-                        FIELD_PLACEHOLDER_FIELDS,
-                        serializedPlaceholder.complicationData!!.fields
-                    )
+                ois.readNullable { SerializedForm().apply { readObject(ois) } }?.let {
+                    fields.putInt(FIELD_PLACEHOLDER_TYPE, it.complicationData!!.type)
+                    fields.putBundle(FIELD_PLACEHOLDER_FIELDS, it.complicationData!!.fields)
                 }
             }
-            val timelineLength = ois.readInt()
-            if (timelineLength != 0) {
-                val parcels = arrayOfNulls<Parcelable>(timelineLength)
-                for (i in 0 until timelineLength) {
-                    val entry = SerializedForm()
-                    entry.readObject(ois)
-                    parcels[i] = entry.complicationData!!.fields
+            ois.readList { SerializedForm().apply { readObject(ois) } }
+                .map { it.complicationData!!.fields }
+                .takeIf { it.isNotEmpty() }
+                ?.let {
+                    fields.putParcelableArray(FIELD_TIMELINE_ENTRIES, it.toTypedArray())
                 }
-                fields.putParcelableArray(FIELD_TIMELINE_ENTRIES, parcels)
-            }
             complicationData = ComplicationData(type, fields)
         }
 
         fun readResolve(): Any = complicationData!!
 
         companion object {
-            private const val VERSION_NUMBER = 19
+            private const val VERSION_NUMBER = 20
             internal fun putIfNotNull(fields: Bundle, field: String, value: Parcelable?) {
                 if (value != null) {
                     fields.putParcelable(field, value)
@@ -650,7 +576,7 @@
         }
 
     /**
-     * Returns true if the ComplicationData contains a ranged max value. I.e. if [rangedValue] can
+     * Returns true if the ComplicationData contains a ranged value. I.e. if [rangedValue] can
      * succeed.
      */
     fun hasRangedValue(): Boolean = isFieldValidForType(FIELD_VALUE, type)
@@ -658,8 +584,8 @@
     /**
      * Returns the *value* field for this complication.
      *
-     * Valid only if the type of this complication data is [TYPE_RANGED_VALUE], otherwise returns
-     * zero.
+     * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
+     * [TYPE_GOAL_PROGRESS], otherwise returns zero.
      */
     val rangedValue: Float
         get() {
@@ -668,6 +594,24 @@
         }
 
     /**
+     * Returns true if the ComplicationData contains a ranged dynamic value. I.e. if
+     * [rangedDynamicValue] can succeed.
+     */
+    fun hasRangedDynamicValue(): Boolean = isFieldValidForType(FIELD_DYNAMIC_VALUE, type)
+
+    /**
+     * Returns the *dynamicValue* field for this complication.
+     *
+     * Valid only if the type of this complication data is [TYPE_RANGED_VALUE] and
+     * [TYPE_GOAL_PROGRESS].
+     */
+    val rangedDynamicValue: DynamicFloat?
+        get() {
+            checkFieldValidForTypeWithoutThrowingException(FIELD_DYNAMIC_VALUE, type)
+            return fields.getByteArray(FIELD_DYNAMIC_VALUE)?.toDynamicFloat()
+        }
+
+    /**
      * Returns true if the ComplicationData contains a ranged max type. I.e. if [rangedValueType]
      * can succeed.
      */
@@ -1119,10 +1063,12 @@
                 !fields.containsKey(FIELD_PLACEHOLDER_TYPE)
             ) {
                 null
-            } else ComplicationData(
-                fields.getInt(FIELD_PLACEHOLDER_TYPE),
-                fields.getBundle(FIELD_PLACEHOLDER_FIELDS)!!
-            )
+            } else {
+                ComplicationData(
+                    fields.getInt(FIELD_PLACEHOLDER_TYPE),
+                    fields.getBundle(FIELD_PLACEHOLDER_FIELDS)!!
+                )
+            }
         }
 
     /** Returns the bytes of the proto layout. */
@@ -1315,7 +1261,18 @@
          *
          * @throws IllegalStateException if this field is not valid for the complication type
          */
-        fun setRangedValue(value: Float) = apply { putFloatField(FIELD_VALUE, value) }
+        fun setRangedValue(value: Float?) = apply { putOrRemoveField(FIELD_VALUE, value) }
+
+        /**
+         * Sets the *dynamicValue* field. It is evaluated to a value with the same limitations as
+         * [setRangedValue].
+         *
+         * Returns this Builder to allow chaining.
+         *
+         * @throws IllegalStateException if this field is not valid for the complication type
+         */
+        fun setRangedDynamicValue(value: DynamicFloat?) =
+            apply { putOrRemoveField(FIELD_DYNAMIC_VALUE, value?.asByteArray()) }
 
         /**
          * Sets the *value type* field which provides meta data about the value. This is
@@ -1707,6 +1664,11 @@
                         " is provided."
                 }
             }
+            for (requiredOneOfFieldGroup in REQUIRED_ONE_OF_FIELDS[type]!!) {
+                check(requiredOneOfFieldGroup.count { fields.containsKey(it) } >= 1) {
+                    "One of $requiredOneOfFieldGroup must be provided."
+                }
+            }
             return ComplicationData(this)
         }
 
@@ -1735,8 +1697,10 @@
             when (obj) {
                 is Boolean -> fields.putBoolean(field, obj)
                 is Int -> fields.putInt(field, obj)
+                is Float -> fields.putFloat(field, obj)
                 is String -> fields.putString(field, obj)
                 is Parcelable -> fields.putParcelable(field, obj)
+                is ByteArray -> fields.putByteArray(field, obj)
                 is IntArray -> fields.putIntArray(field, obj)
                 is FloatArray -> fields.putFloatArray(field, obj)
                 else -> throw IllegalArgumentException("Unexpected object type: " + obj.javaClass)
@@ -1959,6 +1923,7 @@
         private const val FIELD_TIMELINE_ENTRIES = "TIMELINE"
         private const val FIELD_TIMELINE_ENTRY_TYPE = "TIMELINE_ENTRY_TYPE"
         private const val FIELD_VALUE = "VALUE"
+        private const val FIELD_DYNAMIC_VALUE = "DYNAMIC_VALUE"
         private const val FIELD_VALUE_TYPE = "VALUE_TYPE"
 
         // Experimental fields, these are subject to change without notice.
@@ -1997,7 +1962,7 @@
             TYPE_EMPTY to setOf(),
             TYPE_SHORT_TEXT to setOf(FIELD_SHORT_TEXT),
             TYPE_LONG_TEXT to setOf(FIELD_LONG_TEXT),
-            TYPE_RANGED_VALUE to setOf(FIELD_VALUE, FIELD_MIN_VALUE, FIELD_MAX_VALUE),
+            TYPE_RANGED_VALUE to setOf(FIELD_MIN_VALUE, FIELD_MAX_VALUE),
             TYPE_ICON to setOf(FIELD_ICON),
             TYPE_SMALL_IMAGE to setOf(FIELD_SMALL_IMAGE, FIELD_IMAGE_STYLE),
             TYPE_LARGE_IMAGE to setOf(FIELD_LARGE_IMAGE),
@@ -2006,18 +1971,39 @@
             EXP_TYPE_PROTO_LAYOUT to setOf(
                 EXP_FIELD_PROTO_LAYOUT_AMBIENT,
                 EXP_FIELD_PROTO_LAYOUT_INTERACTIVE,
-                EXP_FIELD_PROTO_LAYOUT_RESOURCES
+                EXP_FIELD_PROTO_LAYOUT_RESOURCES,
             ),
             EXP_TYPE_LIST to setOf(EXP_FIELD_LIST_ENTRIES),
-            TYPE_GOAL_PROGRESS to setOf(FIELD_VALUE, FIELD_TARGET_VALUE),
+            TYPE_GOAL_PROGRESS to setOf(FIELD_TARGET_VALUE),
             TYPE_WEIGHTED_ELEMENTS to setOf(
                 FIELD_ELEMENT_WEIGHTS,
                 FIELD_ELEMENT_COLORS,
-                FIELD_ELEMENT_BACKGROUND_COLOR
+                FIELD_ELEMENT_BACKGROUND_COLOR,
             ),
         )
 
-        // Used for validation. OPTIONAL_FIELDS[i] is an array containing all the fields which are
+        // Used for validation. REQUIRED_ONE_OF_FIELDS[i] is a list of field groups of which at
+        // least one field must be populated for @ComplicationType i.
+        // If a field is also in REQUIRED_FIELDS[i], it is not required if another field in the one
+        // of group is populated.
+        private val REQUIRED_ONE_OF_FIELDS: Map<Int, Set<Set<String>>> = mapOf(
+            TYPE_NOT_CONFIGURED to setOf(),
+            TYPE_EMPTY to setOf(),
+            TYPE_SHORT_TEXT to setOf(),
+            TYPE_LONG_TEXT to setOf(),
+            TYPE_RANGED_VALUE to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+            TYPE_ICON to setOf(),
+            TYPE_SMALL_IMAGE to setOf(),
+            TYPE_LARGE_IMAGE to setOf(),
+            TYPE_NO_PERMISSION to setOf(),
+            TYPE_NO_DATA to setOf(),
+            EXP_TYPE_PROTO_LAYOUT to setOf(),
+            EXP_TYPE_LIST to setOf(),
+            TYPE_GOAL_PROGRESS to setOf(setOf(FIELD_VALUE, FIELD_DYNAMIC_VALUE)),
+            TYPE_WEIGHTED_ELEMENTS to setOf(),
+        )
+
+        // Used for validation. OPTIONAL_FIELDS[i] is a list containing all the fields which are
         // valid but not required for type i.
         private val OPTIONAL_FIELDS: Map<Int, Set<String>> = mapOf(
             TYPE_NOT_CONFIGURED to setOf(),
@@ -2033,7 +2019,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_LONG_TEXT to setOf(
                 FIELD_LONG_TITLE,
@@ -2046,7 +2032,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_RANGED_VALUE to setOf(
                 FIELD_SHORT_TEXT,
@@ -2063,7 +2049,7 @@
                 FIELD_COLOR_RAMP_INTERPOLATED,
                 FIELD_PERSISTENCE_POLICY,
                 FIELD_DISPLAY_POLICY,
-                FIELD_VALUE_TYPE
+                FIELD_VALUE_TYPE,
             ),
             TYPE_ICON to setOf(
                 FIELD_TAP_ACTION,
@@ -2071,7 +2057,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_SMALL_IMAGE to setOf(
                 FIELD_TAP_ACTION,
@@ -2079,14 +2065,14 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_LARGE_IMAGE to setOf(
                 FIELD_TAP_ACTION,
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_NO_PERMISSION to setOf(
                 FIELD_SHORT_TEXT,
@@ -2099,7 +2085,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_NO_DATA to setOf(
                 FIELD_CONTENT_DESCRIPTION,
@@ -2119,17 +2105,18 @@
                 FIELD_SMALL_IMAGE_BURN_IN_PROTECTION,
                 FIELD_TAP_ACTION,
                 FIELD_VALUE,
+                FIELD_DYNAMIC_VALUE,
                 FIELD_VALUE_TYPE,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             EXP_TYPE_PROTO_LAYOUT to setOf(
                 FIELD_TAP_ACTION,
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             EXP_TYPE_LIST to setOf(
                 FIELD_TAP_ACTION,
@@ -2137,7 +2124,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_GOAL_PROGRESS to setOf(
                 FIELD_SHORT_TEXT,
@@ -2153,7 +2140,7 @@
                 FIELD_COLOR_RAMP,
                 FIELD_COLOR_RAMP_INTERPOLATED,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
             TYPE_WEIGHTED_ELEMENTS to setOf(
                 FIELD_SHORT_TEXT,
@@ -2167,7 +2154,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
         )
 
@@ -2179,35 +2166,28 @@
         }
 
         fun isFieldValidForType(field: String, @ComplicationType type: Int): Boolean {
-            val requiredFields = REQUIRED_FIELDS[type] ?: return false
-            for (requiredField in requiredFields) {
-                if (requiredField == field) {
-                    return true
-                }
-            }
-            for (optionalField in OPTIONAL_FIELDS[type]!!) {
-                if (optionalField == field) {
-                    return true
-                }
-            }
-            return false
+            return REQUIRED_FIELDS[type]!!.contains(field) ||
+                REQUIRED_ONE_OF_FIELDS[type]!!.any { it.contains(field) } ||
+                OPTIONAL_FIELDS[type]!!.contains(field)
         }
 
         private fun isTypeSupported(type: Int) = type in VALID_TYPES
 
-        /** The unparceling logic needs to remain backward compatible. */
+        /**
+         * The unparceling logic needs to remain backward compatible.
+         * Validates that a value of the given field type can be assigned
+         * to the given complication type.
+         */
         internal fun checkFieldValidForTypeWithoutThrowingException(
-            field: String,
-            @ComplicationType type: Int,
+            fieldType: String,
+            @ComplicationType complicationType: Int,
         ) {
-            if (!isTypeSupported(type)) {
-                Log.w(TAG, "Type $type can not be recognized")
+            if (!isTypeSupported(complicationType)) {
+                Log.w(TAG, "Type $complicationType can not be recognized")
                 return
             }
-            if (!isFieldValidForType(field, type)) {
-                if (Log.isLoggable(TAG, Log.DEBUG)) {
-                    Log.d(TAG, "Field $field is not supported for type $type")
-                }
+            if (!isFieldValidForType(fieldType, complicationType)) {
+                Log.d(TAG, "Field $fieldType is not supported for type $complicationType")
             }
         }
 
@@ -2232,4 +2212,60 @@
             if (!shouldRedact() || unredacted == PLACEHOLDER_STRING) unredacted
             else "REDACTED"
     }
-}
\ No newline at end of file
+}
+
+/** Writes a [ByteArray] by writing the size, then the bytes. To be used with [readByteArray]. */
+internal fun ObjectOutputStream.writeByteArray(value: ByteArray) {
+    writeInt(value.size)
+    write(value)
+}
+
+/** Reads a [ByteArray] written with [writeByteArray]. */
+internal fun ObjectInputStream.readByteArray() = ByteArray(readInt()).also { readFully(it) }
+
+/** Writes an [IntArray] by writing the size, then the bytes. To be used with [readIntArray]. */
+internal fun ObjectOutputStream.writeIntArray(value: IntArray) {
+    writeInt(value.size)
+    value.forEach(this::writeInt)
+}
+
+/** Reads an [IntArray] written with [writeIntArray]. */
+internal fun ObjectInputStream.readIntArray() = IntArray(readInt()).also {
+    for (i in it.indices) it[i] = readInt()
+}
+
+/** Writes a [FloatArray] by writing the size, then the bytes. To be used with [readFloatArray]. */
+internal fun ObjectOutputStream.writeFloatArray(value: FloatArray) {
+    writeInt(value.size)
+    value.forEach(this::writeFloat)
+}
+
+/** Reads a [FloatArray] written with [writeFloatArray]. */
+internal fun ObjectInputStream.readFloatArray() = FloatArray(readInt()).also {
+    for (i in it.indices) it[i] = readFloat()
+}
+
+/** Writes a generic [List] by writing the size, then the objects. To be used with [readList]. */
+internal fun <T> ObjectOutputStream.writeList(value: List<T>, writer: (T) -> Unit) {
+    writeInt(value.size)
+    value.forEach(writer)
+}
+
+/** Reads a list written with [readList]. */
+internal fun <T> ObjectInputStream.readList(reader: () -> T) = List(readInt()) { reader() }
+
+/**
+ * Writes a nullable object by writing a boolean, then the object. To be used with [readNullable].
+ */
+internal fun <T> ObjectOutputStream.writeNullable(value: T?, writer: (T) -> Unit) {
+    if (value != null) {
+        writeBoolean(true)
+        writer(value)
+    } else {
+        writeBoolean(false)
+    }
+}
+
+/** Reads a nullable value written with [writeNullable]. */
+internal fun <T> ObjectInputStream.readNullable(reader: () -> T): T? =
+    if (readBoolean()) reader() else null
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 73e985a..47f0028 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
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ComplicationExperimental::class)
+
 package androidx.wear.watchface.complications.data
 
 import android.app.PendingIntent
@@ -23,17 +25,14 @@
 import android.os.Build
 import android.util.Log
 import androidx.annotation.ColorInt
-import androidx.annotation.IntDef
 import androidx.annotation.FloatRange
+import androidx.annotation.IntDef
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import java.time.Instant
 
-/** The wire format for [ComplicationData]. */
-internal typealias WireComplicationData = android.support.wearable.complications.ComplicationData
-
-/** The builder for [WireComplicationData]. */
-internal typealias WireComplicationDataBuilder =
+typealias WireComplicationData = android.support.wearable.complications.ComplicationData
+typealias WireComplicationDataBuilder =
     android.support.wearable.complications.ComplicationData.Builder
 
 internal const val TAG = "Data.kt"
@@ -248,7 +247,7 @@
     cachedWireComplicationData,
     dataSource = null,
     persistencePolicy =
-        placeholder?.persistencePolicy ?: ComplicationPersistencePolicies.CACHING_ALLOWED,
+    placeholder?.persistencePolicy ?: ComplicationPersistencePolicies.CACHING_ALLOWED,
     displayPolicy = placeholder?.displayPolicy ?: ComplicationDisplayPolicies.ALWAYS_DISPLAY
 ) {
 
@@ -907,8 +906,8 @@
  * Type used for complications including a numerical value within a range, such as a percentage.
  * The value may be accompanied by an icon and/or short text and title.
  *
- * The [value], [min], and [max] fields are required for this type and the value within the
- * range is expected to always be displayed.
+ * The [min] and [max] fields are required for this type, as well as one of [value] or
+ * [dynamicValue]. The value within the range is expected to always be displayed.
  *
  * The icon, title, and text fields are optional and the watch face may choose which of these
  * fields to display, if any.
@@ -923,6 +922,9 @@
  * [PLACEHOLDER]. If it's equal to [PLACEHOLDER] the renderer must treat it as a placeholder rather
  * than rendering normally, its suggested to be drawn as a grey arc with a percentage value selected
  * by the renderer. The semantic meaning of value is described by [valueType].
+ * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
+ * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
+ * ignore this field.
  * @property min The minimum [Float] value for this complication.
  * @property max The maximum [Float] value for this complication.
  * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
@@ -958,6 +960,10 @@
  */
 public class RangedValueComplicationData internal constructor(
     public val value: Float,
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @ComplicationExperimental
+    @get:ComplicationExperimental
+    public val dynamicValue: DynamicFloat?,
     public val min: Float,
     public val max: Float,
     public val monochromaticImage: MonochromaticImage?,
@@ -989,22 +995,55 @@
     /**
      * Builder for [RangedValueComplicationData].
      *
-     * You must at a minimum set the [value], [min], [max] and [contentDescription] fields and at
-     * least one of [monochromaticImage], [smallImage], [text] or [title].
-     *
-     * @param value The value of the ranged complication which should be in the range
-     * [[min]] .. [[max]]. The semantic meaning of value can be specified via [setValueType].
-     * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
-     * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For [TYPE_PERCENTAGE]
-     * this must be 100f.
-     * @param contentDescription Localized description for use by screen readers
+     * You must at a minimum set the [min], [max] and [contentDescription] fields, at least one of
+     * [value] or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text]
+     * or [title].
      */
-    public class Builder(
+    public class Builder
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public constructor(
         private val value: Float,
+        private val dynamicValue: DynamicFloat?,
         private val min: Float,
         private val max: Float,
         private var contentDescription: ComplicationText
     ) : BaseBuilder<Builder, RangedValueComplicationData>() {
+        /**
+         * Creates a [Builder] for a [RangedValueComplicationData] with a [Float] value.
+         *
+         * @param value The value of the ranged complication which should be in the range [[min]] ..
+         * [[max]]. The semantic meaning of value can be specified via [setValueType].
+         * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
+         * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
+         * [TYPE_PERCENTAGE] this must be 0f.
+         * @param contentDescription Localized description for use by screen readers
+         */
+        public constructor(
+            value: Float,
+            min: Float,
+            max: Float,
+            contentDescription: ComplicationText
+        ) : this(value, dynamicValue = null, min, max, contentDescription)
+
+        /**
+         * Creates a [Builder] for a [RangedValueComplicationData] with a [DynamicFloat] value.
+         *
+         * @param dynamicValue The [DynamicFloat] of the ranged complication which will be evaluated
+         * into a value dynamically, and should be in the range [[min]] .. [[max]]. The semantic
+         * meaning of value can be specified via [setValueType].
+         * @param min The minimum value. For [TYPE_PERCENTAGE] this must be 0f.
+         * @param max The maximum value. This must be less than [Float.MAX_VALUE]. For
+         * [TYPE_PERCENTAGE] this must be 0f.
+         * @param contentDescription Localized description for use by screen readers
+         */
+        @ComplicationExperimental
+        public constructor(
+            dynamicValue: DynamicFloat,
+            min: Float,
+            max: Float,
+            contentDescription: ComplicationText
+        ) : this(value = min /* sensible default */, dynamicValue, min, max, contentDescription)
+
         private var tapAction: PendingIntent? = null
         private var validTimeRange: TimeRange? = null
         private var monochromaticImage: MonochromaticImage? = null
@@ -1012,6 +1051,7 @@
         private var title: ComplicationText? = null
         private var text: ComplicationText? = null
         private var colorRamp: ColorRamp? = null
+
         @RangedValueType
         private var valueType: Int = TYPE_UNDEFINED
 
@@ -1082,6 +1122,7 @@
             }
             return RangedValueComplicationData(
                 value,
+                dynamicValue,
                 min,
                 max,
                 monochromaticImage,
@@ -1114,6 +1155,7 @@
 
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
+        builder.setRangedDynamicValue(dynamicValue)
         builder.setRangedMinValue(min)
         builder.setRangedMaxValue(max)
         monochromaticImage?.addToWireComplicationData(builder)
@@ -1143,6 +1185,7 @@
         other as RangedValueComplicationData
 
         if (value != other.value) return false
+        if (dynamicValue != other.dynamicValue) return false
         if (valueType != other.valueType) return false
         if (min != other.min) return false
         if (max != other.max) return false
@@ -1164,6 +1207,7 @@
 
     override fun hashCode(): Int {
         var result = value.hashCode()
+        result = 31 * result + (dynamicValue?.hashCode() ?: 0)
         result = 31 * result + valueType
         result = 31 * result + min.hashCode()
         result = 31 * result + max.hashCode()
@@ -1188,7 +1232,13 @@
         } else {
             value.toString()
         }
-        return "RangedValueComplicationData(value=$valueString, valueType=$valueType, min=$min, " +
+        val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+            "REDACTED"
+        } else {
+            dynamicValue.toString()
+        }
+        return "RangedValueComplicationData(value=$valueString, " +
+            "dynamicValue=$dynamicValueString, valueType=$valueType, min=$min, " +
             "max=$max, monochromaticImage=$monochromaticImage, smallImage=$smallImage, " +
             "title=$title, text=$text, contentDescription=$contentDescription), " +
             "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1255,8 +1305,8 @@
  * 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.
+ * The [targetValue] field is required for this type, as well as one of [value] or
+ * [dynamicValue]. The progress is expected to always be displayed.
  *
  * The icon, title, and text fields are optional and the watch face may choose which of these
  * fields to display, if any.
@@ -1269,12 +1319,16 @@
  *
  * If you want to represent a score for something that's not based on the user (e.g. air quality
  * index) then you should instead use a [RangedValueComplicationData] and pass
- * [RangedValueComplicationData.TYPE_RATING] into [RangedValueComplicationData.Builder.setValueType].
+ * [RangedValueComplicationData.TYPE_RATING] 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
  * rather than rendering normally, its suggested to be drawn as a grey arc with a percentage value
  * selected by the renderer.
+ * @property dynamicValue The [DynamicFloat] optionally set by the data source. If present the
+ * system will dynamically evaluate this and store the result in [value]. Watch faces can typically
+ * ignore this field.
  * @property targetValue The target [Float] value for this complication.
  * @property monochromaticImage A simple [MonochromaticImage] image that can be tinted by the watch
  * face. If the monochromaticImage is equal to [MonochromaticImage.PLACEHOLDER] the renderer must
@@ -1308,6 +1362,10 @@
 public class GoalProgressComplicationData
 internal constructor(
     public val value: Float,
+    @Suppress("OPT_IN_MARKER_ON_WRONG_TARGET")
+    @ComplicationExperimental
+    @get:ComplicationExperimental
+    public val dynamicValue: DynamicFloat?,
     public val targetValue: Float,
     public val monochromaticImage: MonochromaticImage?,
     public val smallImage: SmallImage?,
@@ -1333,19 +1391,52 @@
     /**
      * Builder for [GoalProgressComplicationData].
      *
-     * You must at a minimum set the [value], [targetValue] and [contentDescription] fields and at
-     * least one of [monochromaticImage], [smallImage], [text] or [title].
-     *
-     * @param value The value of the ranged complication which should be >= 0.
-     * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
-     * @param contentDescription Localized description for use by screen readers
+     * You must at a minimum set the [targetValue] and [contentDescription] fields, one of [value]
+     * or [dynamicValue], and at least one of [monochromaticImage], [smallImage], [text] or
+     * [title].
      */
     @RequiresApi(Build.VERSION_CODES.TIRAMISU)
-    public class Builder(
+    public class Builder
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public constructor(
         private val value: Float,
+        private val dynamicValue: DynamicFloat?,
         private val targetValue: Float,
         private var contentDescription: ComplicationText
     ) : BaseBuilder<Builder, GoalProgressComplicationData>() {
+        /**
+         * Creates a [Builder] for a [GoalProgressComplicationData] with a [Float] value.
+         *
+         * @param value The value of the goal complication which should be >= 0.
+         * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
+         * @param contentDescription Localized description for use by screen readers
+         */
+        public constructor(
+            value: Float,
+            targetValue: Float,
+            contentDescription: ComplicationText
+        ) : this(value, dynamicValue = null, targetValue, contentDescription)
+
+        /**
+         * Creates a [Builder] for a [GoalProgressComplicationData] with a [DynamicFloat] value.
+         *
+         * @param dynamicValue The [DynamicFloat] of the goal complication which will be evaluated
+         * into a value dynamically, and should be >= 0.
+         * @param targetValue The target value. This must be less than [Float.MAX_VALUE].
+         * @param contentDescription Localized description for use by screen readers
+         */
+        @ComplicationExperimental
+        public constructor(
+            dynamicValue: DynamicFloat,
+            targetValue: Float,
+            contentDescription: ComplicationText
+        ) : this(
+            value = 0f /* sensible default */,
+            dynamicValue,
+            targetValue,
+            contentDescription
+        )
+
         private var tapAction: PendingIntent? = null
         private var validTimeRange: TimeRange? = null
         private var monochromaticImage: MonochromaticImage? = null
@@ -1408,6 +1499,7 @@
             }
             return GoalProgressComplicationData(
                 value,
+                dynamicValue,
                 targetValue,
                 monochromaticImage,
                 smallImage,
@@ -1438,6 +1530,7 @@
 
     override fun fillWireComplicationDataBuilder(builder: WireComplicationDataBuilder) {
         builder.setRangedValue(value)
+        builder.setRangedDynamicValue(dynamicValue)
         builder.setTargetValue(targetValue)
         monochromaticImage?.addToWireComplicationData(builder)
         smallImage?.addToWireComplicationData(builder)
@@ -1465,6 +1558,7 @@
         other as GoalProgressComplicationData
 
         if (value != other.value) return false
+        if (dynamicValue != other.dynamicValue) return false
         if (targetValue != other.targetValue) return false
         if (monochromaticImage != other.monochromaticImage) return false
         if (smallImage != other.smallImage) return false
@@ -1484,6 +1578,7 @@
 
     override fun hashCode(): Int {
         var result = value.hashCode()
+        result = 31 * result + (dynamicValue?.hashCode() ?: 0)
         result = 31 * result + targetValue.hashCode()
         result = 31 * result + (monochromaticImage?.hashCode() ?: 0)
         result = 31 * result + (smallImage?.hashCode() ?: 0)
@@ -1506,7 +1601,13 @@
         } else {
             value.toString()
         }
-        return "GoalProgressComplicationData(value=$valueString, targetValue=$targetValue, " +
+        val dynamicValueString = if (WireComplicationData.shouldRedact()) {
+            "REDACTED"
+        } else {
+            dynamicValue.toString()
+        }
+        return "GoalProgressComplicationData(value=$valueString, " +
+            "dynamicValue=$dynamicValueString, targetValue=$targetValue, " +
             "monochromaticImage=$monochromaticImage, smallImage=$smallImage, title=$title, " +
             "text=$text, contentDescription=$contentDescription), " +
             "tapActionLostDueToSerialization=$tapActionLostDueToSerialization, " +
@@ -1684,7 +1785,8 @@
         elements: List<Element>,
         private var contentDescription: ComplicationText
     ) : BaseBuilder<Builder, WeightedElementsComplicationData>() {
-        @ColorInt private var elementBackgroundColor: Int = Color.TRANSPARENT
+        @ColorInt
+        private var elementBackgroundColor: Int = Color.TRANSPARENT
         private var tapAction: PendingIntent? = null
         private var validTimeRange: TimeRange? = null
         private var monochromaticImage: MonochromaticImage? = null
@@ -2564,6 +2666,7 @@
             RangedValueComplicationData.TYPE.toWireComplicationType() ->
                 RangedValueComplicationData.Builder(
                     value = rangedValue,
+                    dynamicValue = rangedDynamicValue,
                     min = rangedMinValue,
                     max = rangedMaxValue,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
@@ -2622,6 +2725,7 @@
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
                 GoalProgressComplicationData.Builder(
                     value = rangedValue,
+                    dynamicValue = rangedDynamicValue,
                     targetValue = targetValue,
                     contentDescription?.toApiComplicationText() ?: ComplicationText.EMPTY
                 ).apply {
@@ -2738,7 +2842,9 @@
 
             RangedValueComplicationData.TYPE.toWireComplicationType() ->
                 RangedValueComplicationData.Builder(
-                    value = rangedValue, min = rangedMinValue,
+                    value = rangedValue,
+                    dynamicValue = rangedDynamicValue,
+                    min = rangedMinValue,
                     max = rangedMaxValue,
                     contentDescription = contentDescription?.toApiComplicationText()
                         ?: ComplicationText.EMPTY
@@ -2813,6 +2919,7 @@
             GoalProgressComplicationData.TYPE.toWireComplicationType() ->
                 GoalProgressComplicationData.Builder(
                     value = rangedValue,
+                    dynamicValue = rangedDynamicValue,
                     targetValue = targetValue,
                     contentDescription = contentDescription?.toApiComplicationText()
                         ?: ComplicationText.EMPTY
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
new file mode 100644
index 0000000..0374f72
--- /dev/null
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/DynamicFloat.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.data
+
+import androidx.annotation.RestrictTo
+
+/** Placeholder for DynamicFloat implementation by tiles. */
+// TODO(b/257413268): Replace this with the real implementation.
+@ComplicationExperimental
+abstract class DynamicFloat {
+    abstract fun asByteArray(): ByteArray
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        // Not checking for exact same class because it's not implemented yet.
+        if (other !is DynamicFloat) return false
+        return asByteArray().contentEquals(other.asByteArray())
+    }
+
+    override fun hashCode() = asByteArray().contentHashCode()
+
+    override fun toString() = "DynamicFloatPlaceholder${asByteArray().contentToString()}"
+}
+
+/** Placeholder parser for [DynamicFloat] from [ByteArray]. */
+@ComplicationExperimental
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+fun ByteArray.toDynamicFloat() = object : DynamicFloat() {
+    override fun asByteArray() = this@toDynamicFloat
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
index d264e3f..4494346 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/android/support/wearable/complications/ComplicationDataTest.kt
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ComplicationExperimental::class)
+
 package android.support.wearable.complications
 
 import android.app.PendingIntent
@@ -23,7 +25,9 @@
 import android.support.wearable.complications.ComplicationText.TimeDifferenceBuilder
 import android.support.wearable.complications.ComplicationText.TimeFormatBuilder
 import androidx.test.core.app.ApplicationProvider
+import androidx.wear.watchface.complications.data.ComplicationExperimental
 import androidx.wear.watchface.complications.data.SharedRobolectricTestRunner
+import androidx.wear.watchface.complications.data.toDynamicFloat
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.Assert.assertThrows
@@ -79,7 +83,7 @@
     }
 
     @Test
-    public fun testRangedValueFields() {
+    public fun testRangedValueFieldsWithFixedValue() {
         // GIVEN complication data of the RANGED_VALUE type created by the Builder...
         val data =
             ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
@@ -93,6 +97,30 @@
         // WHEN the relevant getters are called on the resulting data
         // THEN the correct values are returned.
         Assert.assertEquals(data.rangedValue, 57f, 0f)
+        Assert.assertNull(data.rangedDynamicValue)
+        Assert.assertEquals(data.rangedMinValue, 5f, 0f)
+        Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
+        Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
+            .isEqualTo("title")
+        Truth.assertThat(data.shortText!!.getTextAt(mResources, 0))
+            .isEqualTo("text")
+    }
+
+    @Test
+    public fun testRangedValueFieldsWithDynamicValue() {
+        // GIVEN complication data of the RANGED_VALUE type created by the Builder...
+        val data =
+            ComplicationData.Builder(ComplicationData.TYPE_RANGED_VALUE)
+                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setRangedMinValue(5f)
+                .setRangedMaxValue(150f)
+                .setShortTitle(ComplicationText.plainText("title"))
+                .setShortText(ComplicationText.plainText("text"))
+                .build()
+
+        // WHEN the relevant getters are called on the resulting data
+        // THEN the correct values are returned.
+        Truth.assertThat(data.rangedDynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
         Assert.assertEquals(data.rangedMinValue, 5f, 0f)
         Assert.assertEquals(data.rangedMaxValue, 150f, 0f)
         Truth.assertThat(data.shortTitle!!.getTextAt(mResources, 0))
@@ -128,7 +156,7 @@
     }
 
     @Test
-    public fun testRangedValueMustContainValue() {
+    public fun testRangedValueMustContainFixedOrDynamicValue() {
         // GIVEN a complication builder of the RANGED_VALUE type, with the value field not
         // populated...
         val builder =
@@ -1106,7 +1134,7 @@
                         .setLongText(
                             ComplicationText.plainText(ComplicationData.PLACEHOLDER_STRING)
                         )
-                            .build()
+                        .build()
                 )
                 .build()
         timelineEntry.timelineStartEpochSecond = 100
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 7251d4b..44c76f8 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
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+@file:OptIn(ComplicationExperimental::class)
+
 package androidx.wear.watchface.complications.data
 
 import android.annotation.SuppressLint
@@ -399,7 +401,7 @@
     }
 
     @Test
-    public fun rangedValueComplicationData() {
+    public fun rangedValueComplicationData_withFixedValue() {
         val data = RangedValueComplicationData.Builder(
             value = 95f, min = 0f, max = 100f,
             contentDescription = "content description".complicationText
@@ -426,6 +428,7 @@
         assertThat(deserialized.max).isEqualTo(100f)
         assertThat(deserialized.min).isEqualTo(0f)
         assertThat(deserialized.value).isEqualTo(95f)
+        assertThat(deserialized.dynamicValue).isNull()
         assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
             .isEqualTo("content description")
         assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
@@ -452,7 +455,95 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
+            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+                "valueType=0, min=0.0, max=100.0, " +
+                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
+                "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "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}, colorRamp=null, persistencePolicy=0, " +
+                "displayPolicy=0)"
+        )
+    }
+
+    @Test
+    public fun rangedValueComplicationData_withDynamicValue() {
+        val data = RangedValueComplicationData.Builder(
+            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            min = 5f,
+            max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                    .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                    .setRangedValue(5f) // min as a sensible default
+                    .setRangedValueType(RangedValueComplicationData.TYPE_UNDEFINED)
+                    .setRangedMinValue(5f)
+                    .setRangedMaxValue(100f)
+                    .setShortTitle(WireComplicationText.plainText("battery"))
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as RangedValueComplicationData
+        assertThat(deserialized.max).isEqualTo(100f)
+        assertThat(deserialized.min).isEqualTo(5f)
+        assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.value).isEqualTo(5f) // min as a sensible default
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+        assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("battery")
+
+        val sameData = RangedValueComplicationData.Builder(
+            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            min = 5f,
+            max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+        val diffDataFixedValue = RangedValueComplicationData.Builder(
+            value = 5f, // Even though it's the sensible default
+            min = 5f,
+            max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+        val diffDataDynamicValue = RangedValueComplicationData.Builder(
+            dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+            min = 5f,
+            max = 100f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("battery".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+
+        assertThat(data).isEqualTo(sameData)
+        assertThat(data).isNotEqualTo(diffDataFixedValue)
+        assertThat(data).isNotEqualTo(diffDataDynamicValue)
+        assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(diffDataFixedValue.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(diffDataDynamicValue.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "RangedValueComplicationData(value=5.0, " +
+                "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
+                "valueType=0, min=5.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
                 "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -535,7 +626,8 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, valueType=1, min=0.0, max=100.0, " +
+            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+                "valueType=1, min=0.0, max=100.0, " +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=battery," +
@@ -550,7 +642,144 @@
     }
 
     @Test
-    public fun goalProgressComplicationData_with_ColorRamp() {
+    public fun goalProgressComplicationData_withFixedValue() {
+        val data = GoalProgressComplicationData.Builder(
+            value = 1200f, targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                    .setRangedValue(1200f)
+                    .setTargetValue(10000f)
+                    .setShortTitle(WireComplicationText.plainText("steps"))
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
+        assertThat(deserialized.value).isEqualTo(1200f)
+        assertThat(deserialized.dynamicValue).isNull()
+        assertThat(deserialized.targetValue).isEqualTo(10000f)
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+        assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("steps")
+
+        val sameData = GoalProgressComplicationData.Builder(
+            value = 1200f, targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+
+        val diffData = GoalProgressComplicationData.Builder(
+            value = 1201f, targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceB)
+            .build()
+
+        assertThat(data).isEqualTo(sameData)
+        assertThat(data).isNotEqualTo(diffData)
+        assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+                "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+                "text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "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}, colorRamp=null, " +
+                "persistencePolicy=0, displayPolicy=0)"
+        )
+    }
+
+    @Test
+    public fun goalProgressComplicationData_withDynamicValue() {
+        val data = GoalProgressComplicationData.Builder(
+            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+        ParcelableSubject.assertThat(data.asWireComplicationData())
+            .hasSameSerializationAs(
+                WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                    .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                    .setRangedValue(0f) // sensible default
+                    .setTargetValue(10000f)
+                    .setShortTitle(WireComplicationText.plainText("steps"))
+                    .setContentDescription(WireComplicationText.plainText("content description"))
+                    .setDataSource(dataSourceA)
+                    .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                    .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                    .build()
+            )
+        testRoundTripConversions(data)
+        val deserialized = serializeAndDeserialize(data) as GoalProgressComplicationData
+        assertThat(deserialized.dynamicValue!!.asByteArray()).isEqualTo(byteArrayOf(42, 107))
+        assertThat(deserialized.value).isEqualTo(0f) // sensible default
+        assertThat(deserialized.targetValue).isEqualTo(10000f)
+        assertThat(deserialized.contentDescription!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("content description")
+        assertThat(deserialized.title!!.getTextAt(resources, Instant.EPOCH))
+            .isEqualTo("steps")
+
+        val sameData = GoalProgressComplicationData.Builder(
+            dynamicValue = byteArrayOf(42, 107).toDynamicFloat(),
+            targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceA)
+            .build()
+
+        val diffData = GoalProgressComplicationData.Builder(
+            dynamicValue = byteArrayOf(43, 108).toDynamicFloat(),
+            targetValue = 10000f,
+            contentDescription = "content description".complicationText
+        )
+            .setTitle("steps".complicationText)
+            .setDataSource(dataSourceB)
+            .build()
+
+        assertThat(data).isEqualTo(sameData)
+        assertThat(data).isNotEqualTo(diffData)
+        assertThat(data.hashCode()).isEqualTo(sameData.hashCode())
+        assertThat(data.hashCode()).isNotEqualTo(diffData.hashCode())
+        assertThat(data.toString()).isEqualTo(
+            "GoalProgressComplicationData(value=0.0, " +
+                "dynamicValue=DynamicFloatPlaceholder[42, 107], " +
+                "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+                "text=null, " +
+                "contentDescription=ComplicationText{mSurroundingText=content description, " +
+                "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}, colorRamp=null, " +
+                "persistencePolicy=0, displayPolicy=0)"
+        )
+    }
+
+    @Test
+    public fun goalProgressComplicationData_withColorRamp() {
         val data = GoalProgressComplicationData.Builder(
             value = 1200f, targetValue = 10000f,
             contentDescription = "content description".complicationText
@@ -605,9 +834,10 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=1200.0, targetValue=10000.0, " +
-                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=steps, mTimeDependentText=null}, text=null, " +
+            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+                "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=steps, mTimeDependentText=null}, " +
+                "text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
                 "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
                 "tapAction=null, validTimeRange=TimeRange(startDateTimeMillis=" +
@@ -620,7 +850,7 @@
 
     @RequiresApi(Build.VERSION_CODES.P)
     @Test
-    public fun goalProgressComplicationData_with_ColorRamp_and_Images() {
+    public fun goalProgressComplicationData_withColorRampAndImages() {
         val data = GoalProgressComplicationData.Builder(
             value = 1200f, targetValue = 10000f,
             contentDescription = "content description".complicationText
@@ -687,7 +917,8 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=1200.0, targetValue=10000.0, " +
+            "GoalProgressComplicationData(value=1200.0, dynamicValue=null, " +
+                "targetValue=10000.0, " +
                 "monochromaticImage=MonochromaticImage(image=Icon(typ=URI uri=someuri), " +
                 "ambientImage=null), smallImage=SmallImage(image=Icon(typ=URI uri=someuri2), " +
                 "type=PHOTO, ambientImage=null), title=ComplicationText{mSurroundingText=steps, " +
@@ -703,7 +934,7 @@
     }
 
     @Test
-    public fun rangedValueComplicationData_with_ColorRamp() {
+    public fun rangedValueComplicationData_withColorRamp() {
         val data = RangedValueComplicationData.Builder(
             value = 95f, min = 0f, max = 100f,
             contentDescription = "content description".complicationText
@@ -761,7 +992,8 @@
         assertThat(data.hashCode()).isEqualTo(data2.hashCode())
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
-            "RangedValueComplicationData(value=95.0, valueType=0, min=0.0, max=100.0, " +
+            "RangedValueComplicationData(value=95.0, dynamicValue=null, " +
+                "valueType=0, min=0.0, max=100.0, " +
                 "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
                 "mSurroundingText=battery, mTimeDependentText=null}, text=null, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
@@ -1093,6 +1325,7 @@
 
         assertThat(deserialized.smallImage.image.type).isEqualTo(Icon.TYPE_BITMAP)
         val getBitmap = deserialized.smallImage.image.javaClass.getDeclaredMethod("getBitmap")
+
         @SuppressLint("BanUncheckedReflection")
         val bitmap = getBitmap.invoke(deserialized.smallImage.image) as Bitmap
 
@@ -1455,8 +1688,8 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=RangedValueComplicationData(" +
-                "value=3.4028235E38, valueType=0, min=0.0, max=100.0, monochromaticImage=null, " +
-                "smallImage=null, title=null, text=ComplicationText{" +
+                "value=3.4028235E38, dynamicValue=null, valueType=0, min=0.0, max=100.0, " +
+                "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
                 "contentDescription=ComplicationText{mSurroundingText=" +
                 "content description, mTimeDependentText=null}), " +
@@ -1538,8 +1771,8 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=GoalProgressComplicationData(" +
-                "value=3.4028235E38, targetValue=10000.0, monochromaticImage=null, " +
-                "smallImage=null, title=null, text=ComplicationText{" +
+                "value=3.4028235E38, dynamicValue=null, targetValue=10000.0, " +
+                "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
                 "contentDescription=ComplicationText{mSurroundingText=content description, " +
                 "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
@@ -1722,8 +1955,8 @@
         assertThat(data.hashCode()).isNotEqualTo(data3.hashCode())
         assertThat(data.toString()).isEqualTo(
             "NoDataComplicationData(placeholder=RangedValueComplicationData(" +
-                "value=3.4028235E38, valueType=1, min=0.0, max=100.0, monochromaticImage=null, " +
-                "smallImage=null, title=null, text=ComplicationText{" +
+                "value=3.4028235E38, dynamicValue=null, valueType=1, min=0.0, max=100.0, " +
+                "monochromaticImage=null, smallImage=null, title=null, text=ComplicationText{" +
                 "mSurroundingText=__placeholder__, mTimeDependentText=null}, " +
                 "contentDescription=ComplicationText{mSurroundingText=" +
                 "content description, mTimeDependentText=null}), " +
@@ -2018,7 +2251,7 @@
     }
 
     @Test
-    public fun rangedValueComplicationData() {
+    public fun rangedValueComplicationData_withFixedValue() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
                 .setRangedValue(95f)
@@ -2034,6 +2267,22 @@
     }
 
     @Test
+    public fun rangedValueComplicationData_withDynamicValue() {
+        assertRoundtrip(
+            WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
+                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setRangedMinValue(0f)
+                .setRangedMaxValue(100f)
+                .setShortTitle(WireComplicationText.plainText("battery"))
+                .setContentDescription(WireComplicationText.plainText("content description"))
+                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                .build(),
+            ComplicationType.RANGED_VALUE
+        )
+    }
+
+    @Test
     public fun rangedValueComplicationData_drawSegmented() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_RANGED_VALUE)
@@ -2050,7 +2299,7 @@
     }
 
     @Test
-    public fun goalProgressComplicationData() {
+    public fun goalProgressComplicationData_withFixedValue() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
                 .setRangedValue(1200f)
@@ -2067,6 +2316,23 @@
     }
 
     @Test
+    public fun goalProgressComplicationData_withDynamicValue() {
+        assertRoundtrip(
+            WireComplicationDataBuilder(WireComplicationData.TYPE_GOAL_PROGRESS)
+                .setRangedDynamicValue(byteArrayOf(42, 107).toDynamicFloat())
+                .setTargetValue(10000f)
+                .setShortTitle(WireComplicationText.plainText("steps"))
+                .setContentDescription(WireComplicationText.plainText("content description"))
+                .setColorRamp(intArrayOf(Color.RED, Color.GREEN, Color.BLUE))
+                .setColorRampIsSmoothShaded(false)
+                .setPersistencePolicy(ComplicationPersistencePolicies.CACHING_ALLOWED)
+                .setDisplayPolicy(ComplicationDisplayPolicies.ALWAYS_DISPLAY)
+                .build(),
+            ComplicationType.GOAL_PROGRESS
+        )
+    }
+
+    @Test
     public fun weightedElementsComplicationData() {
         assertRoundtrip(
             WireComplicationDataBuilder(WireComplicationData.TYPE_WEIGHTED_ELEMENTS)
@@ -3006,13 +3272,14 @@
             .setTitle("title".complicationText)
             .build()
 
-        assertThat(data.toString()).isEqualTo("LongTextComplicationData(text=" +
-            "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
-            "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-            "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
-            "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
-            "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
-            "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
+        assertThat(data.toString()).isEqualTo(
+            "LongTextComplicationData(text=" +
+                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, title=" +
+                "ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+                "monochromaticImage=null, smallImage=null, contentDescription=ComplicationText" +
+                "{mSurroundingText=REDACTED, mTimeDependentText=null}), " +
+                "tapActionLostDueToSerialization=false, tapAction=null, validTimeRange=TimeRange" +
+                "(REDACTED), dataSource=null, persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=4, mFields=REDACTED}"
@@ -3031,14 +3298,15 @@
             .setTitle("title".complicationText)
             .build()
 
-        assertThat(data.toString()).isEqualTo("RangedValueComplicationData(value=REDACTED, " +
-            "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
-            "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-            "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
-            "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
-            "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
-            "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
-            "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
+        assertThat(data.toString()).isEqualTo(
+            "RangedValueComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+                "valueType=0, min=0.0, max=100.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+                "text=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+                "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+                "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
+                "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
+                "colorRamp=null, persistencePolicy=0, displayPolicy=0)"
         )
         assertThat(data.asWireComplicationData().toString()).isEqualTo(
             "ComplicationData{mType=5, mFields=REDACTED}"
@@ -3057,10 +3325,10 @@
             .build()
 
         assertThat(data.toString()).isEqualTo(
-            "GoalProgressComplicationData(value=REDACTED, targetValue=10000.0, " +
-                "monochromaticImage=null, smallImage=null, title=ComplicationText{" +
-                "mSurroundingText=REDACTED, mTimeDependentText=null}, text=null, " +
-                "contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
+            "GoalProgressComplicationData(value=REDACTED, dynamicValue=REDACTED, " +
+                "targetValue=10000.0, monochromaticImage=null, smallImage=null, " +
+                "title=ComplicationText{mSurroundingText=REDACTED, mTimeDependentText=null}, " +
+                "text=null, contentDescription=ComplicationText{mSurroundingText=REDACTED, " +
                 "mTimeDependentText=null}), tapActionLostDueToSerialization=false, " +
                 "tapAction=null, validTimeRange=TimeRange(REDACTED), dataSource=null, " +
                 "colorRamp=ColorRamp(colors=[-65536, -16711936, -16776961], interpolated=true), " +
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 f5f9868..b014c3b 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
@@ -429,9 +429,9 @@
     }
 
     private class RangedArcsTestData {
-        public int min;
-        public int max;
-        public int value;
+        public float min;
+        public float max;
+        public float value;
         public float progress;
         public float remaining;
         public float gap;
@@ -498,9 +498,9 @@
     @Test
     public void rangedValueIsDrawnCorrectlyInActiveMode() {
         // GIVEN a complication renderer with ranged value complication data
-        int min = 0;
-        int max = 100;
-        int value = (max - min) / 2;
+        float min = 0;
+        float max = 100;
+        float value = (max - min) / 2;
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(TYPE_RANGED_VALUE)
                         .setRangedValue(value)
@@ -532,9 +532,9 @@
     @Test
     public void rangedValueIsDrawnCorrectlyInAmbientMode() {
         // GIVEN a complication renderer with ranged value complication data
-        int min = 0;
-        int max = 100;
-        int value = (max - min) / 2;
+        float min = 0;
+        float max = 100;
+        float value = (max - min) / 2;
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(TYPE_RANGED_VALUE)
                         .setRangedValue(value)
@@ -1005,9 +1005,9 @@
                 new ComplicationData.Builder(TYPE_RANGED_VALUE)
                         .setShortText(ComplicationText.plainText("foo"))
                         .setShortTitle(ComplicationText.plainText("bar"))
-                        .setRangedMinValue(1)
-                        .setRangedValue(5)
-                        .setRangedMaxValue(10)
+                        .setRangedMinValue(1f)
+                        .setRangedValue(5f)
+                        .setRangedMaxValue(10f)
                         .build(),
                 true);
         mComplicationRenderer.setRangedValueProgressHidden(true);
@@ -1029,9 +1029,9 @@
         mComplicationRenderer.setComplicationData(
                 new ComplicationData.Builder(TYPE_RANGED_VALUE)
                         .setIcon(mMockIcon)
-                        .setRangedMinValue(1)
-                        .setRangedValue(5)
-                        .setRangedMaxValue(10)
+                        .setRangedMinValue(1f)
+                        .setRangedValue(5f)
+                        .setRangedMaxValue(10f)
                         .build(),
                 true);
         mComplicationRenderer.setRangedValueProgressHidden(true);
diff --git a/wear/watchface/watchface-style/api/current.txt b/wear/watchface/watchface-style/api/current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/current.txt
+++ b/wear/watchface/watchface-style/api/current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-style/api/public_plus_experimental_current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/api/restricted_current.txt b/wear/watchface/watchface-style/api/restricted_current.txt
index f79c03d..274acb9 100644
--- a/wear/watchface/watchface-style/api/restricted_current.txt
+++ b/wear/watchface/watchface-style/api/restricted_current.txt
@@ -69,6 +69,7 @@
 
   public final class UserStyleSchema {
     ctor public UserStyleSchema(java.util.List<? extends androidx.wear.watchface.style.UserStyleSetting> userStyleSettings);
+    method public androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption? findComplicationSlotsOptionForUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
     method public operator androidx.wear.watchface.style.UserStyleSetting? get(androidx.wear.watchface.style.UserStyleSetting.Id settingId);
     method public byte[] getDigestHash();
     method public java.util.List<androidx.wear.watchface.style.UserStyleSetting> getRootUserStyleSettings();
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
index 24ce1c1..1d98429 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/CurrentUserStyleRepository.kt
@@ -19,9 +19,11 @@
 import android.content.res.Resources
 import android.content.res.XmlResourceParser
 import android.graphics.drawable.Icon
+import android.os.Build
 import androidx.annotation.RestrictTo
 import androidx.wear.watchface.complications.IllegalNodeException
 import androidx.wear.watchface.complications.iterate
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
 import androidx.wear.watchface.style.UserStyleSetting.Option
 import androidx.wear.watchface.style.data.UserStyleSchemaWireFormat
 import androidx.wear.watchface.style.data.UserStyleWireFormat
@@ -422,8 +424,10 @@
  *
  * @param userStyleSettings The user configurable style categories associated with this watch face.
  * Empty if the watch face doesn't support user styling. Note we allow at most one
- * [UserStyleSetting.ComplicationSlotsUserStyleSetting] and one
- * [UserStyleSetting.CustomValueUserStyleSetting] in the list.
+ * [UserStyleSetting.CustomValueUserStyleSetting] in the list. Prior to android T ot most one
+ * [UserStyleSetting.ComplicationSlotsUserStyleSetting] is allowed, however from android T it's
+ * possible with hierarchical styles for there to be more than one, but at most one can be active at
+ * any given time.
  */
 public class UserStyleSchema constructor(
     userStyleSettings: List<UserStyleSetting>
@@ -521,9 +525,12 @@
             }
         }
 
-        // This requirement makes it easier to implement companion editors.
-        require(complicationSlotsUserStyleSettingCount <= 1) {
-            "At most only one ComplicationSlotsUserStyleSetting is allowed"
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            validateComplicationSettings(rootUserStyleSettings, null)
+        } else {
+            require(complicationSlotsUserStyleSettingCount <= 1) {
+               "Prior to Android T, at most only one ComplicationSlotsUserStyleSetting is allowed"
+            }
         }
 
         // There's a hard limit to how big Schema + UserStyle can be and since this data is sent
@@ -535,6 +542,28 @@
         }
     }
 
+    private fun validateComplicationSettings(
+        settings: Collection<UserStyleSetting>,
+        initialPrevSetting: UserStyleSetting.ComplicationSlotsUserStyleSetting?
+    ) {
+        var prevSetting = initialPrevSetting
+        for (setting in settings) {
+            if (setting is UserStyleSetting.ComplicationSlotsUserStyleSetting) {
+                require(prevSetting == null) {
+                    "From Android T multiple ComplicationSlotsUserStyleSettings are allowed, but" +
+                        " at most one can be active for any permutation of UserStyle. Note: " +
+                        "$setting and $prevSetting"
+                }
+                prevSetting = setting
+            }
+        }
+        for (setting in settings) {
+            for (option in setting.options) {
+                validateComplicationSettings(option.childSettings, prevSetting)
+            }
+        }
+    }
+
     /** @hide */
     @Suppress("Deprecation") // userStyleSettings
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
@@ -638,6 +667,40 @@
     private class NullOutputStream : OutputStream() {
         override fun write(value: Int) {}
     }
+
+    private fun findActiveComplicationSetting(
+        settings: Collection<UserStyleSetting>,
+        userStyle: UserStyle
+    ): UserStyleSetting.ComplicationSlotsUserStyleSetting? {
+        for (setting in settings) {
+            if (setting is UserStyleSetting.ComplicationSlotsUserStyleSetting) {
+                return setting
+            }
+            findActiveComplicationSetting(userStyle[setting]!!.childSettings, userStyle)?.let {
+                return it
+            }
+        }
+        return null
+    }
+
+    /**
+     * At most one [UserStyleSetting.ComplicationSlotsUserStyleSetting] can be active at a time
+     * based on the hierarchy of styles for any given [UserStyle]. This function finds the current
+     * active [UserStyleSetting.ComplicationSlotsUserStyleSetting] based upon the [userStyle] and,
+     * if there is one, it returns the corresponding selected [ComplicationSlotsOption]. Otherwise
+     * it returns `null`.
+     *
+     * @param userStyle The [UserStyle] for which the function will search for the selected
+     * [ComplicationSlotsOption], if any.
+     * @return The selected [ComplicationSlotsOption] for the [userStyle] if any, or `null`
+     * otherwise.
+     */
+    public fun findComplicationSlotsOptionForUserStyle(
+        userStyle: UserStyle
+    ): ComplicationSlotsOption? =
+        findActiveComplicationSetting(rootUserStyleSettings, userStyle)?.let {
+            userStyle[it] as ComplicationSlotsOption
+        }
 }
 
 /**
diff --git a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 6da2687..4691492 100644
--- a/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/watchface/watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -878,8 +878,11 @@
      * The ComplicationsManager listens for style changes with this setting and when a
      * [ComplicationSlotsOption] is selected the overrides are automatically applied. Note its
      * suggested that the default [ComplicationSlotOverlay] (the first entry in the list) does
-     * not apply any overrides. Only a single [ComplicationSlotsUserStyleSetting] is permitted in
-     * the [UserStyleSchema].
+     * not apply any overrides.
+     *
+     * From android T multiple [ComplicationSlotsUserStyleSetting] are allowed in a style hierarchy
+     * as long as at  most one is active for any permutation of [UserStyle]. Prior to android T only
+     * a single ComplicationSlotsUserStyleSetting was allowed.
      *
      * Not to be confused with complication data source selection.
      */
diff --git a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
index 6b96dcf..8afe47f 100644
--- a/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
+++ b/wear/watchface/watchface-style/src/test/java/androidx/wear/watchface/style/CurrentUserStyleRepositoryTest.kt
@@ -20,6 +20,9 @@
 import android.os.Build
 import androidx.annotation.RequiresApi
 import androidx.wear.watchface.style.UserStyleSetting.BooleanUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.CustomValueUserStyleSetting.CustomValueOption
 import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
@@ -32,6 +35,7 @@
 import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
 
 private val redStyleOption =
     ListUserStyleSetting.ListOption(Option.Id("red_style"), "Red", icon = null)
@@ -121,6 +125,7 @@
 private val optionTrue = BooleanUserStyleSetting.BooleanOption.TRUE
 private val optionFalse = BooleanUserStyleSetting.BooleanOption.FALSE
 
+@Config(minSdk = Build.VERSION_CODES.TIRAMISU)
 @RunWith(StyleTestRunner::class)
 class CurrentUserStyleRepositoryTest {
 
@@ -736,6 +741,307 @@
         assertThat(watchHandLengthStyleSetting.hasParent).isTrue()
         assertThat(watchHandStyleSetting.hasParent).isTrue()
     }
+
+    @Test
+    fun invalid_multiple_ComplicationSlotsUserStyleSettings_same_level() {
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1, complicationSetting2)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null,
+            childSettings = listOf(complicationSetting2)
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            UserStyleSchema(
+                listOf(
+                    ListUserStyleSetting(
+                        UserStyleSetting.Id("a123"),
+                        displayName = "A123",
+                        description = "A123",
+                        icon = null,
+                        listOf(optionA1, optionA2),
+                        WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+                    ),
+                    complicationSetting1,
+                    complicationSetting2
+                )
+            )
+        }
+    }
+
+    @Test
+    fun invalid_multiple_ComplicationSlotsUserStyleSettings_different_levels() {
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting1"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null
+        )
+
+        assertThrows(IllegalArgumentException::class.java) {
+            UserStyleSchema(
+                listOf(
+                    ListUserStyleSetting(
+                        UserStyleSetting.Id("a123"),
+                        displayName = "A123",
+                        description = "A123",
+                        icon = null,
+                        listOf(optionA1, optionA2),
+                        WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+                    ),
+                    complicationSetting1,
+                    complicationSetting2
+                )
+            )
+        }
+    }
+
+    @Test
+    @Suppress("deprecation")
+    fun multiple_ComplicationSlotsUserStyleSettings() {
+        // The code below constructs the following hierarchy:
+        //
+        //                                  rootABChoice
+        //          rootOptionA   ---------/            \---------  rootOptionB
+        //               |                                               |
+        //          a123Choice                                       b12Choice
+        //         /    |     \                                      /      \
+        // optionA1  optionA2  optionA3                        optionB1    optionB2
+        //   |          |                                         |
+        //   |      complicationSetting2                 complicationSetting1
+        // complicationSetting1
+
+        val leftComplicationID = 101
+        val rightComplicationID = 102
+        val leftAndRightComplications = ComplicationSlotsOption(
+            Option.Id("LEFT_AND_RIGHT_COMPLICATIONS"),
+            displayName = "Both",
+            icon = null,
+            emptyList()
+        )
+        val noComplications = ComplicationSlotsOption(
+            Option.Id("NO_COMPLICATIONS"),
+            displayName = "None",
+            icon = null,
+            listOf(
+                ComplicationSlotOverlay(leftComplicationID, enabled = false),
+                ComplicationSlotOverlay(rightComplicationID, enabled = false)
+            )
+        )
+        val complicationSetting1 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftAndRightComplications, noComplications),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val leftComplication = ComplicationSlotsOption(
+            Option.Id("LEFT_COMPLICATION"),
+            displayName = "Left",
+            icon = null,
+            listOf(ComplicationSlotOverlay(rightComplicationID, enabled = false))
+        )
+        val rightComplication = ComplicationSlotsOption(
+            Option.Id("RIGHT_COMPLICATION"),
+            displayName = "Right",
+            icon = null,
+            listOf(ComplicationSlotOverlay(leftComplicationID, enabled = false))
+        )
+        val complicationSetting2 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting2"),
+            displayName = "Complications",
+            description = "Number and position",
+            icon = null,
+            complicationConfig = listOf(leftComplication, rightComplication),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val normal = ComplicationSlotsOption(
+            Option.Id("Normal"),
+            displayName = "Normal",
+            icon = null,
+            emptyList()
+        )
+        val traversal = ComplicationSlotsOption(
+            Option.Id("Traversal"),
+            displayName = "Traversal",
+            icon = null,
+            listOf(
+                ComplicationSlotOverlay(leftComplicationID, accessibilityTraversalIndex = 3),
+                ComplicationSlotOverlay(rightComplicationID, accessibilityTraversalIndex = 2)
+            )
+        )
+        val complicationSetting3 = ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("complications_style_setting3"),
+            displayName = "Traversal Order",
+            description = "Traversal Order",
+            icon = null,
+            complicationConfig = listOf(normal, traversal),
+            listOf(WatchFaceLayer.COMPLICATIONS)
+        )
+
+        val optionA1 = ListUserStyleSetting.ListOption(
+            Option.Id("a1_style"),
+            displayName = "A1",
+            icon = null,
+            childSettings = listOf(complicationSetting1)
+        )
+        val optionA2 = ListUserStyleSetting.ListOption(
+            Option.Id("a2_style"),
+            displayName = "A2",
+            icon = null,
+            childSettings = listOf(complicationSetting2)
+        )
+        val optionA3 =
+            ListUserStyleSetting.ListOption(Option.Id("a3_style"), "A3", icon = null)
+
+        val a123Choice = ListUserStyleSetting(
+            UserStyleSetting.Id("a123"),
+            displayName = "A123",
+            description = "A123",
+            icon = null,
+            listOf(optionA1, optionA2, optionA3),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val optionB1 = ListUserStyleSetting.ListOption(
+            Option.Id("b1_style"),
+            displayName = "B1",
+            icon = null,
+            childSettings = listOf(complicationSetting3)
+        )
+        val optionB2 =
+            ListUserStyleSetting.ListOption(Option.Id("b2_style"), "B2", icon = null)
+
+        val b12Choice = ListUserStyleSetting(
+            UserStyleSetting.Id("b12"),
+            displayName = "B12",
+            "B12",
+            icon = null,
+            listOf(optionB1, optionB2),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val rootOptionA = ListUserStyleSetting.ListOption(
+            Option.Id("a_style"),
+            displayName = "A",
+            icon = null,
+            childSettings = listOf(a123Choice)
+        )
+        val rootOptionB = ListUserStyleSetting.ListOption(
+            Option.Id("b_style"),
+            displayName = "B",
+            icon = null,
+            childSettings = listOf(b12Choice)
+        )
+
+        val rootABChoice = ListUserStyleSetting(
+            UserStyleSetting.Id("root_ab"),
+            displayName = "AB",
+            description = "AB",
+            icon = null,
+            listOf(rootOptionA, rootOptionB),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        val schema = UserStyleSchema(
+            listOf(
+                rootABChoice,
+                a123Choice,
+                b12Choice,
+                complicationSetting1,
+                complicationSetting2,
+                complicationSetting3
+            )
+        )
+
+        val userStyleMap = mutableMapOf(
+            rootABChoice to rootOptionA,
+            a123Choice to optionA1,
+            b12Choice to optionB1,
+            complicationSetting1 to leftAndRightComplications,
+            complicationSetting2 to rightComplication,
+            complicationSetting3 to traversal
+        )
+
+        // Test various userStyleMap permutations to ensure the correct ComplicationSlotsOption is
+        // returned.
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(leftAndRightComplications)
+
+        userStyleMap[a123Choice] = optionA2
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(rightComplication)
+
+        userStyleMap[a123Choice] = optionA3
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isNull()
+
+        userStyleMap[rootABChoice] = rootOptionB
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isEqualTo(traversal)
+
+        userStyleMap[b12Choice] = optionB2
+        assertThat(schema.findComplicationSlotsOptionForUserStyle(UserStyle(userStyleMap)))
+            .isNull()
+    }
 }
 
 @RunWith(StyleTestRunner::class)
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 45a6c9b3..1ae8d16 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
@@ -16,14 +16,18 @@
 
 package androidx.wear.watchface.samples
 
+import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Color
 import android.graphics.Paint
 import android.graphics.Rect
+import android.graphics.RectF
 import android.graphics.drawable.Icon
 import android.view.SurfaceHolder
 import androidx.annotation.Px
+import androidx.wear.watchface.CanvasComplicationFactory
 import androidx.wear.watchface.CanvasType
+import androidx.wear.watchface.ComplicationSlot
 import androidx.wear.watchface.ComplicationSlotsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.RenderParameters
@@ -32,18 +36,24 @@
 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.style.CurrentUserStyleRepository
 import androidx.wear.watchface.style.UserStyleSchema
 import androidx.wear.watchface.style.UserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.DoubleRangeUserStyleSetting.DoubleRangeOption
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
+import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay
 import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting.ListOption
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting
-import androidx.wear.watchface.style.UserStyleSetting.LongRangeUserStyleSetting.LongRangeOption
 import androidx.wear.watchface.style.WatchFaceLayer
 import java.time.ZonedDateTime
 import kotlin.math.cos
 import kotlin.math.sin
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 open class ExampleHierarchicalStyleWatchFaceService : WatchFaceService() {
 
@@ -52,7 +62,7 @@
             UserStyleSetting.Option.Id("12_style"),
             resources,
             R.string.digital_clock_style_12,
-            icon = null
+            Icon.createWithResource(this, R.drawable.red_style)
         )
     }
 
@@ -61,7 +71,48 @@
             UserStyleSetting.Option.Id("24_style"),
             resources,
             R.string.digital_clock_style_24,
-            icon = null
+            Icon.createWithResource(this, R.drawable.red_style)
+        )
+    }
+
+    @Suppress("Deprecation")
+    private val digitalComplicationSettings by lazy {
+        ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("DigitalComplications"),
+            resources,
+            R.string.digital_complications_setting,
+            R.string.digital_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("On"),
+                    resources,
+                    R.string.digital_complication_on_screen_name,
+                    Icon.createWithResource(this, R.drawable.on),
+                    listOf(
+                        ComplicationSlotOverlay(
+                            COMPLICATION1_ID,
+                            enabled = true,
+                            complicationSlotBounds =
+                                ComplicationSlotBounds(RectF(0.1f, 0.4f, 0.3f, 0.6f))
+                        ),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    ),
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Off"),
+                    resources,
+                    R.string.digital_complication_off_screen_name,
+                    Icon.createWithResource(this, R.drawable.off),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
         )
     }
 
@@ -120,31 +171,63 @@
         )
     }
 
-    internal val watchHandLengthStyleSetting by lazy {
-        DoubleRangeUserStyleSetting(
-            UserStyleSetting.Id(WATCH_HAND_LENGTH_STYLE_SETTING),
+    internal val drawHoursSetting by lazy {
+        UserStyleSetting.BooleanUserStyleSetting(
+            UserStyleSetting.Id(HOURS_STYLE_SETTING),
             resources,
-            R.string.watchface_hand_length_setting,
-            R.string.watchface_hand_length_setting_description,
-            null,
-            0.25,
-            1.0,
-            listOf(WatchFaceLayer.COMPLICATIONS_OVERLAY),
-            0.75
+            R.string.watchface_draw_hours_setting,
+            R.string.watchface_draw_hours_setting_description,
+            icon = null,
+            listOf(WatchFaceLayer.BASE),
+            defaultValue = true,
+            watchFaceEditorData = null
         )
     }
 
-    internal val hoursDrawFreqStyleSetting by lazy {
-        LongRangeUserStyleSetting(
-            UserStyleSetting.Id(HOURS_DRAW_FREQ_STYLE_SETTING),
+    @Suppress("Deprecation")
+    private val analogComplicationSettings by lazy {
+        ComplicationSlotsUserStyleSetting(
+            UserStyleSetting.Id("AnalogComplications"),
             resources,
-            R.string.watchface_draw_hours_freq_setting,
-            R.string.watchface_draw_hours_freq_setting_description,
-            null,
-            HOURS_DRAW_FREQ_MIN,
-            HOURS_DRAW_FREQ_MAX,
-            listOf(WatchFaceLayer.BASE),
-            HOURS_DRAW_FREQ_DEFAULT
+            R.string.watchface_complications_setting,
+            R.string.watchface_complications_setting_description,
+            icon = null,
+            complicationConfig = listOf(
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("One"),
+                    resources,
+                    R.string.analog_complication_one_screen_name,
+                    Icon.createWithResource(this, R.drawable.one),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = false),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Two"),
+                    resources,
+                    R.string.analog_complication_two_screen_name,
+                    Icon.createWithResource(this, R.drawable.two),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = false)
+                    )
+                ),
+                ComplicationSlotsUserStyleSetting.ComplicationSlotsOption(
+                    UserStyleSetting.Option.Id("Three"),
+                    resources,
+                    R.string.analog_complication_three_screen_name,
+                    Icon.createWithResource(this, R.drawable.three),
+                    listOf(
+                        ComplicationSlotOverlay(COMPLICATION1_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION2_ID, enabled = true),
+                        ComplicationSlotOverlay(COMPLICATION3_ID, enabled = true)
+                    )
+                )
+            ),
+            listOf(WatchFaceLayer.COMPLICATIONS)
         )
     }
 
@@ -153,8 +236,12 @@
             UserStyleSetting.Option.Id("digital"),
             resources,
             R.string.style_digital_watch,
-            icon = null,
-            childSettings = listOf(digitalClockStyleSetting, colorStyleSetting)
+            icon = Icon.createWithResource(this, R.drawable.d),
+            childSettings = listOf(
+                digitalClockStyleSetting,
+                colorStyleSetting,
+                digitalComplicationSettings
+            )
         )
     }
 
@@ -163,8 +250,12 @@
             UserStyleSetting.Option.Id("analog"),
             resources,
             R.string.style_analog_watch,
-            icon = null,
-            childSettings = listOf(watchHandLengthStyleSetting, hoursDrawFreqStyleSetting)
+            icon = Icon.createWithResource(this, R.drawable.a),
+            childSettings = listOf(
+                colorStyleSetting,
+                drawHoursSetting,
+                analogComplicationSettings
+            )
         )
     }
 
@@ -185,11 +276,97 @@
             watchFaceType,
             digitalClockStyleSetting,
             colorStyleSetting,
-            watchHandLengthStyleSetting,
-            hoursDrawFreqStyleSetting
+            drawHoursSetting,
+            digitalComplicationSettings,
+            analogComplicationSettings
         )
     )
 
+    private val watchFaceStyle by lazy {
+        WatchFaceColorStyle.create(this, "red_style")
+    }
+
+    public override fun createComplicationSlotsManager(
+        currentUserStyleRepository: CurrentUserStyleRepository
+    ): ComplicationSlotsManager {
+        val canvasComplicationFactory =
+            CanvasComplicationFactory { watchState, listener ->
+                CanvasComplicationDrawable(
+                    watchFaceStyle.getDrawable(this@ExampleHierarchicalStyleWatchFaceService)!!,
+                    watchState,
+                    listener
+                )
+            }
+
+        val complicationOne = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION1_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_WATCH_BATTERY,
+                ComplicationType.RANGED_VALUE
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.1f, 0.8f, 0.3f))
+        ).setNameResourceId(R.string.hierarchical_complication1_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication1_screen_reader_name
+            ).build()
+
+        val complicationTwo = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION2_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_TIME_AND_DATE,
+                ComplicationType.SHORT_TEXT
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.4f, 0.8f, 0.6f))
+        ).setNameResourceId(R.string.hierarchical_complication2_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication2_screen_reader_name
+            ).build()
+
+        val complicationThree = ComplicationSlot.createRoundRectComplicationSlotBuilder(
+            COMPLICATION3_ID,
+            canvasComplicationFactory,
+            listOf(
+                ComplicationType.RANGED_VALUE,
+                ComplicationType.GOAL_PROGRESS,
+                ComplicationType.WEIGHTED_ELEMENTS,
+                ComplicationType.SHORT_TEXT,
+                ComplicationType.MONOCHROMATIC_IMAGE,
+                ComplicationType.SMALL_IMAGE
+            ),
+            DefaultComplicationDataSourcePolicy(
+                SystemDataSources.DATA_SOURCE_SUNRISE_SUNSET,
+                ComplicationType.SHORT_TEXT
+            ),
+            ComplicationSlotBounds(RectF(0.6f, 0.7f, 0.8f, 0.9f))
+        ).setNameResourceId(R.string.hierarchical_complication3_screen_name)
+            .setScreenReaderNameResourceId(
+                R.string.hierarchical_complication3_screen_reader_name
+            ).build()
+
+        return ComplicationSlotsManager(
+            listOf(complicationOne, complicationTwo, complicationThree),
+            currentUserStyleRepository
+        )
+    }
+
     override suspend fun createWatchFace(
         surfaceHolder: SurfaceHolder,
         watchState: WatchState,
@@ -206,6 +383,32 @@
             16L
         ) {
             val renderer = ExampleHierarchicalStyleWatchFaceRenderer()
+            val context: Context = this@ExampleHierarchicalStyleWatchFaceService
+
+            init {
+                CoroutineScope(Dispatchers.Main.immediate).launch {
+                    currentUserStyleRepository.userStyle.collect { userStyle ->
+                        for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                            (complication.renderer as CanvasComplicationDrawable).drawable =
+                                when (userStyle[colorStyleSetting]) {
+                                    redStyle ->
+                                        WatchFaceColorStyle.create(context, "red_style")
+                                            .getDrawable(context)!!
+                                    greenStyle ->
+                                        WatchFaceColorStyle.create(context, "green_style")
+                                            .getDrawable(context)!!
+                                    blueStyle ->
+                                        WatchFaceColorStyle.create(context, "blue_style")
+                                             .getDrawable(context)!!
+                                    else -> throw IllegalArgumentException(
+                                        "Unrecognized colorStyleSetting "
+                                    )
+                                }
+                        }
+                    }
+                }
+            }
+
             override fun render(canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime) {
                 val currentStyle = currentUserStyleRepository.userStyle.value
                 when (currentStyle[watchFaceType]) {
@@ -232,10 +435,8 @@
                         bounds,
                         zonedDateTime,
                         renderParameters,
-                        (currentStyle[hoursDrawFreqStyleSetting]!!
-                            as LongRangeOption).value.toInt(),
-                        (currentStyle[watchHandLengthStyleSetting]!!
-                            as DoubleRangeOption).value.toFloat()
+                        (currentStyle[drawHoursSetting]!!
+                            as UserStyleSetting.BooleanUserStyleSetting.BooleanOption).value,
                     )
 
                     else -> {
@@ -245,6 +446,12 @@
                         )
                     }
                 }
+
+                for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                    if (complication.enabled) {
+                        complication.render(canvas, zonedDateTime, renderParameters)
+                    }
+                }
             }
 
             override fun renderHighlightLayer(
@@ -252,7 +459,11 @@
                 bounds: Rect,
                 zonedDateTime: ZonedDateTime
             ) {
-                // Nothing to do.
+                for ((_, complication) in complicationSlotsManager.complicationSlots) {
+                    if (complication.enabled) {
+                        complication.renderHighlightLayer(canvas, zonedDateTime, renderParameters)
+                    }
+                }
             }
         }
     )
@@ -352,8 +563,7 @@
             bounds: Rect,
             zonedDateTime: ZonedDateTime,
             renderParameters: RenderParameters,
-            hoursDrawFreq: Int,
-            watchHandLength: Float
+            drawHourPips: Boolean
         ) {
             val isActive = renderParameters.drawMode !== DrawMode.AMBIENT
 
@@ -362,8 +572,8 @@
 
             paint.color = Color.WHITE
             paint.textSize = 20.0f
-            if (isActive) {
-                for (i in 12 downTo 1 step hoursDrawFreq) {
+            if (isActive && drawHourPips) {
+                for (i in 12 downTo 1 step 3) {
                     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()
@@ -386,8 +596,8 @@
             val hourRot = (hours + minutes / 60.0f + seconds / 3600.0f) / 12.0f * 360.0f
             val minuteRot = (minutes + seconds / 60.0f) / 60.0f * 360.0f
 
-            val hourXRadius = bounds.width() * watchHandLength * 0.35f
-            val hourYRadius = bounds.height() * watchHandLength * 0.35f
+            val hourXRadius = bounds.width() * 0.3f
+            val hourYRadius = bounds.height() * 0.3f
 
             paint.strokeWidth = if (isActive) 8f else 5f
             canvas.drawLine(
@@ -398,8 +608,8 @@
                 paint
             )
 
-            val minuteXRadius = bounds.width() * watchHandLength * 0.499f
-            val minuteYRadius = bounds.height() * watchHandLength * 0.499f
+            val minuteXRadius = bounds.width() * 0.4f
+            val minuteYRadius = bounds.height() * 0.4f
 
             paint.strokeWidth = if (isActive) 4f else 2.5f
             canvas.drawLine(
@@ -418,11 +628,7 @@
         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 HOURS_STYLE_SETTING = "hours_style_setting"
         private const val NUMBER_RADIUS_FRACTION = 0.45f
 
         private val timeText = charArrayOf('1', '0', ':', '0', '9')
@@ -439,5 +645,9 @@
 
         @Px
         private val TEXT_PADDING = 12
+
+        const val COMPLICATION1_ID = 101
+        const val COMPLICATION2_ID = 102
+        const val COMPLICATION3_ID = 103
     }
 }
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp
new file mode 100644
index 0000000..c837655
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/a.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp
new file mode 100644
index 0000000..f47294e
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/d.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp
new file mode 100644
index 0000000..e2db0da
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/off.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp
new file mode 100644
index 0000000..91e5649
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/on.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp
new file mode 100644
index 0000000..8a3622b
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/one.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp
new file mode 100644
index 0000000..0c5ffc1
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/three.webp
Binary files differ
diff --git a/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp
new file mode 100644
index 0000000..ceaa89a
--- /dev/null
+++ b/wear/watchface/watchface/samples/src/main/res/drawable-nodpi/two.webp
Binary files differ
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 a5d1d53..de9835e 100644
--- a/wear/watchface/watchface/samples/src/main/res/values/strings.xml
+++ b/wear/watchface/watchface/samples/src/main/res/values/strings.xml
@@ -25,7 +25,7 @@
     <string name="gl_background_init_watch_face_name"
         translatable="false">Background Init Watchface</string>
     <string name="hierarchical_watch_face_name"
-        translatable="false">Example Hierarchical Watchface</string>
+        translatable="false">Hierarchical Watchface (Needs T)</string>
 
     <!-- Name of watchface style [CHAR LIMIT=20] -->
     <string name="red_style_name">Red Style</string>
@@ -116,6 +116,14 @@
     hours labeling [CHAR LIMIT=20] -->
     <string name="watchface_draw_hours_freq_setting_description">Labeling frequency</string>
 
+    <!-- Menu option to select a widget that lets us configure whether to show hour pips
+    [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting">Hour pips</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure whether to show
+    hour pips [CHAR LIMIT=20] -->
+    <string name="watchface_draw_hours_setting_description">Display on/off</string>
+
     <!-- Name of the left complication for use visually in the companion editor. [CHAR LIMIT=20] -->
     <string name="left_complication_screen_name">Left</string>
 
@@ -177,4 +185,55 @@
 
     <!-- Sub title for the menu option to select an analog or digital clock [CHAR LIMIT=20] -->
     <string name="clock_type_description">Select analog or digital</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting">Complication</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) for the digital watch [CHAR LIMIT=20] -->
+    <string name="digital_complications_setting_description">On or off</string>
+
+    <!-- List entry, enabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_on_screen_name">On</string>
+
+    <!-- List entry, disabling the complication for the digital watch. [CHAR LIMIT=20] -->
+    <string name="digital_complication_off_screen_name">Off</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_one_screen_name">One</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_two_screen_name">Two</string>
+
+    <!-- List entry setting the number of complications enabled for the analog watch.
+    [CHAR LIMIT=20] -->
+    <string name="analog_complication_three_screen_name">Three</string>
+
+    <!-- Name of a complication at the top of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication1_screen_name">Top</string>
+
+    <!-- Name of a complication at the top of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication1_screen_reader_name">Top complication</string>
+
+    <!-- Name of a complication at the middle of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication2_screen_name">Middle</string>
+
+    <!-- Name of a complication at the middle of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication2_screen_reader_name">Middle complication</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use visually in the companion editor.
+    [CHAR LIMIT=20] -->
+    <string name="hierarchical_complication3_screen_name">Bottom</string>
+
+    <!-- Name of a complication at the bottom of the screen, for use by the companion editor screen
+    reader. -->
+    <string name="hierarchical_complication3_screen_reader_name">Bottom complication</string>
+
 </resources>
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 548403e..7ce080b 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
@@ -415,7 +415,7 @@
         sendComplications()
 
         handler.post {
-            engineWrapper.draw()
+            engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
         }
 
         assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -430,7 +430,7 @@
 
         handler.post {
             setAmbient(true)
-            engineWrapper.draw()
+            engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
         }
 
         assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -526,7 +526,7 @@
 
         handler.post {
             assertThat(engineWrapper.mutableWatchState.watchFaceInstanceId.value).isEqualTo(newId)
-            engineWrapper.draw()
+            engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
         }
 
         assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -544,7 +544,7 @@
         )
 
         handler.post {
-            engineWrapper.draw()
+            engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull())
         }
 
         assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
@@ -939,7 +939,7 @@
             engineWrapper.deferredWatchFaceImpl.await()
         }
 
-        handler.post { engineWrapper.draw() }
+        handler.post { engineWrapper.draw(engineWrapper.getWatchFaceImplOrNull()) }
 
         assertThat(renderDoneLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)).isTrue()
         try {
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
index 4ad2be0..b422068 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/ComplicationSlotsManager.kt
@@ -42,7 +42,6 @@
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting
 import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.launch
 import java.time.Instant
 
@@ -152,31 +151,47 @@
         }
     }
 
+    private fun applyInitialComplicationConfig() {
+        for ((id, complication) in complicationSlots) {
+            val initialConfig = initialComplicationConfigs[id]!!
+            complication.complicationSlotBounds = initialConfig.complicationSlotBounds
+            complication.enabled = initialConfig.enabled
+            complication.accessibilityTraversalIndex = initialConfig.accessibilityTraversalIndex
+            complication.nameResourceId = initialConfig.nameResourceId
+            complication.screenReaderNameResourceId = initialConfig.screenReaderNameResourceId
+        }
+        onComplicationsUpdated()
+    }
+
     /** @hide */
     @WorkerThread
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @Suppress("Deprecation") // userStyleSettings
     public fun listenForStyleChanges(coroutineScope: CoroutineScope) {
-        val complicationsStyleCategory =
-            currentUserStyleRepository.schema.userStyleSettings.firstOrNull {
-                it is ComplicationSlotsUserStyleSetting
-            } ?: return
-
-        var previousOption: ComplicationSlotsOption = currentUserStyleRepository.userStyle.value[
-            complicationsStyleCategory
-        ]!! as ComplicationSlotsOption
+        var previousOption =
+            currentUserStyleRepository.schema.findComplicationSlotsOptionForUserStyle(
+                currentUserStyleRepository.userStyle.value
+            )
 
         // Apply the initial settings on the worker thread.
-        applyComplicationSlotsStyleCategoryOption(previousOption)
+        previousOption?.let {
+            applyComplicationSlotsStyleCategoryOption(it)
+        }
 
         // Add a listener so we can track changes and automatically apply them on the UIThread
         coroutineScope.launch {
-            currentUserStyleRepository.userStyle.collect { userStyle ->
+            currentUserStyleRepository.userStyle.collect {
                 val newlySelectedOption =
-                    userStyle[complicationsStyleCategory]!! as ComplicationSlotsOption
+                    currentUserStyleRepository.schema.findComplicationSlotsOptionForUserStyle(
+                        currentUserStyleRepository.userStyle.value
+                    )
+
                 if (previousOption != newlySelectedOption) {
                     previousOption = newlySelectedOption
-                    applyComplicationSlotsStyleCategoryOption(newlySelectedOption)
+                    if (newlySelectedOption == null) {
+                        applyInitialComplicationConfig()
+                    } else {
+                        applyComplicationSlotsStyleCategoryOption(newlySelectedOption)
+                    }
                 }
             }
         }
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index 0b6d480..40c59a8 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -934,9 +934,8 @@
         }
     }
 
-    /** @hide */
     @UiThread
-    internal fun onDraw() {
+    fun onDraw() {
         val startTime = getZonedDateTime()
         val startInstant = startTime.toInstant()
         val startTimeMillis = systemTimeProvider.getSystemTimeMillis()
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 92c05ee..073f8a4 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
@@ -808,31 +808,30 @@
     ) = TraceEvent(
         "WatchFaceService.writeComplicationCache"
     ).use {
-        try {
-            val stream = ByteArrayOutputStream()
-            val objectOutputStream = ObjectOutputStream(stream)
-            objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
-            for (slot in complicationSlotsManager.complicationSlots) {
-                objectOutputStream.writeInt(slot.key)
-                objectOutputStream.writeObject(
-                    if ((slot.value.complicationData.value.persistencePolicy and
-                            ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
-                    ) {
-                        NoDataComplicationData().asWireComplicationData()
-                    } else {
-                        slot.value.complicationData.value.asWireComplicationData()
-                    }
-                )
-            }
-            objectOutputStream.close()
-            val byteArray = stream.toByteArray()
-
-            // File IO can be slow so perform the write from a background thread.
-            getBackgroundThreadHandler().post {
+        // File IO can be slow so perform the write from a background thread.
+        getBackgroundThreadHandler().post {
+            try {
+                val stream = ByteArrayOutputStream()
+                val objectOutputStream = ObjectOutputStream(stream)
+                objectOutputStream.writeInt(complicationSlotsManager.complicationSlots.size)
+                for (slot in complicationSlotsManager.complicationSlots) {
+                    objectOutputStream.writeInt(slot.key)
+                    objectOutputStream.writeObject(
+                        if ((slot.value.complicationData.value.persistencePolicy and
+                              ComplicationPersistencePolicies.DO_NOT_PERSIST) != 0
+                        ) {
+                            NoDataComplicationData().asWireComplicationData()
+                        } else {
+                            slot.value.complicationData.value.asWireComplicationData()
+                        }
+                    )
+                }
+                objectOutputStream.close()
+                val byteArray = stream.toByteArray()
                 writeComplicationDataCacheByteArray(context, fileName, byteArray)
+            } catch (e: Exception) {
+                Log.w(TAG, "Failed to write to complication cache due to exception", e)
             }
-        } catch (e: Exception) {
-            Log.w(TAG, "Failed to write to complication cache due to exception", e)
         }
     }
 
@@ -1226,7 +1225,7 @@
                  * there's no point drawing.
                  */
                 if (watchFaceImpl?.renderer?.shouldAnimate() != false) {
-                    draw()
+                    draw(watchFaceImpl)
                 }
             }
         }
@@ -1416,7 +1415,8 @@
                     // loaded yet (if that did happen then draw would be a NOP). The watch face will
                     // render at least once upon loading so we don't need to do anything special
                     // here.
-                    draw()
+                    val watchFaceImpl: WatchFaceImpl? = getWatchFaceImplOrNull()
+                    draw(watchFaceImpl)
                 } catch (t: Throwable) {
                     Log.e(TAG, "ambientTickUpdate failed", t)
                 } finally {
@@ -2455,7 +2455,7 @@
             }
         }
 
-        internal fun draw() {
+        internal fun draw(watchFaceImpl: WatchFaceImpl?) {
             try {
                 if (TRACE_DRAW) {
                     Trace.beginSection("onDraw")
@@ -2463,8 +2463,6 @@
                 if (LOG_VERBOSE) {
                     Log.v(TAG, "drawing frame")
                 }
-
-                val watchFaceImpl: WatchFaceImpl? = getWatchFaceImplOrNull()
                 watchFaceImpl?.onDraw()
             } finally {
                 if (TRACE_DRAW) {
@@ -2489,7 +2487,7 @@
                 "The estimated wire size of the supplied UserStyleSchemas for watch face " +
                     "$packageName is too big at $estimatedBytes bytes. UserStyleSchemas get sent " +
                     "to the companion over bluetooth and should be as small as possible for this " +
-                    "to be performant."
+                    "to be performant. The maximum size is " + MAX_REASONABLE_SCHEMA_WIRE_SIZE_BYTES
             }
         }
 
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 0bdb170..97ca016 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
@@ -409,6 +409,17 @@
         ),
         affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
     )
+    private val complicationsStyleSetting2 = ComplicationSlotsUserStyleSetting(
+        UserStyleSetting.Id("complications_style_setting2"),
+        "AllComplicationSlots",
+        "Number and position",
+        icon = null,
+        complicationConfig = listOf(
+            leftOnlyComplicationsOption,
+            rightOnlyComplicationsOption
+        ),
+        affectsWatchFaceLayers = listOf(WatchFaceLayer.COMPLICATIONS)
+    )
 
     private lateinit var renderer: TestRenderer
     private lateinit var complicationSlotsManager: ComplicationSlotsManager
@@ -2701,6 +2712,79 @@
     }
 
     @Test
+    fun hierarchical_complicationsStyleSetting() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+            return
+        }
+
+        val option1 = ListUserStyleSetting.ListOption(
+            Option.Id("1"),
+            displayName = "1",
+            icon = null,
+            childSettings = listOf(complicationsStyleSetting)
+        )
+        val option2 = ListUserStyleSetting.ListOption(
+            Option.Id("2"),
+            displayName = "2",
+            icon = null,
+            childSettings = listOf(complicationsStyleSetting2)
+        )
+        val option3 = ListUserStyleSetting.ListOption(Option.Id("3"), "3", icon = null)
+        val choice = ListUserStyleSetting(
+            UserStyleSetting.Id("123"),
+            displayName = "123",
+            description = "123",
+            icon = null,
+            listOf(option1, option2, option3),
+            WatchFaceLayer.ALL_WATCH_FACE_LAYERS
+        )
+
+        initEngine(
+            WatchFaceType.DIGITAL,
+            listOf(leftComplication, rightComplication),
+            UserStyleSchema(listOf(choice, complicationsStyleSetting, complicationsStyleSetting2)),
+            apiVersion = 4
+        )
+
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option1,
+                    complicationsStyleSetting to noComplicationsOption, // Active
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption
+                )
+            )
+        )
+        assertFalse(leftComplication.enabled)
+        assertFalse(rightComplication.enabled)
+
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option2,
+                    complicationsStyleSetting to noComplicationsOption,
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption // Active
+                )
+            )
+        )
+        assertFalse(leftComplication.enabled)
+        assertTrue(rightComplication.enabled)
+
+        // By default all complications are active if no complicationsStyleSetting applies.
+        currentUserStyleRepository.updateUserStyle(
+            UserStyle(
+                mapOf(
+                    choice to option3,
+                    complicationsStyleSetting to noComplicationsOption,
+                    complicationsStyleSetting2 to rightOnlyComplicationsOption
+                )
+            )
+        )
+        assertTrue(leftComplication.enabled)
+        assertTrue(rightComplication.enabled)
+    }
+
+    @Test
     public fun observeComplicationData() {
         initWallpaperInteractiveWatchFaceInstance(
             WatchFaceType.ANALOG,
diff --git a/wear/wear/build.gradle b/wear/wear/build.gradle
index f055c6f..0b8d171 100644
--- a/wear/wear/build.gradle
+++ b/wear/wear/build.gradle
@@ -13,6 +13,7 @@
     api("androidx.recyclerview:recyclerview:1.1.0")
     api("androidx.core:core:1.6.0")
     api("androidx.versionedparcelable:versionedparcelable:1.1.1")
+    api('androidx.dynamicanimation:dynamicanimation:1.0.0')
 
     androidTestImplementation(project(":test:screenshot:screenshot"))
     androidTestImplementation(libs.kotlinStdlib)
diff --git a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
index 8638f8b..3a12522 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissController.java
@@ -24,8 +24,6 @@
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
-import android.view.animation.AccelerateInterpolator;
-import android.view.animation.DecelerateInterpolator;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
@@ -41,41 +39,26 @@
 @UiThread
 class SwipeDismissController extends DismissController {
     private static final String TAG = "SwipeDismissController";
-
     public static final float DEFAULT_DISMISS_DRAG_WIDTH_RATIO = .33f;
+    private float mDismissMinDragWidthRatio = DEFAULT_DISMISS_DRAG_WIDTH_RATIO;
     // A value between 0.0 and 1.0 determining the percentage of the screen on the left-hand-side
     // where edge swipe gestures are permitted to begin.
     private static final float EDGE_SWIPE_THRESHOLD = 0.1f;
-    private static final float TRANSLATION_MIN_ALPHA = 0.5f;
-    private static final float DEFAULT_INTERPOLATION_FACTOR = 1.5f;
-
+    private static final int VELOCITY_UNIT = 1000;
     // Cached ViewConfiguration and system-wide constant value
-    private int mSlop;
-    private int mMinFlingVelocity;
-    private float mGestureThresholdPx;
-
-    // Transient properties
+    private final int mSlop;
+    private final int mMinFlingVelocity;
+    private final float mGestureThresholdPx;
+    private final SwipeDismissTransitionHelper mSwipeDismissTransitionHelper;
     private int mActiveTouchId;
     private float mDownX;
     private float mDownY;
+    private float mLastX;
     private boolean mSwiping;
-    // This variable holds information about whether the initial move of a longer swipe
-    // (consisting of multiple move events) has conformed to the definition of a horizontal
-    // swipe-to-dismiss. A swipe gesture is only ever allowed to be recognized if this variable is
-    // set to true. Otherwise, the motion events will be allowed to propagate to the children.
-    private boolean mCanStartSwipe = true;
     private boolean mDismissed;
     private boolean mDiscardIntercept;
-    private VelocityTracker mVelocityTracker;
-    private float mTranslationX;
-    private float mLastX;
-    private float mDismissMinDragWidthRatio = DEFAULT_DISMISS_DRAG_WIDTH_RATIO;
-    boolean mStarted;
-    final int mAnimationTime;
 
-    final DecelerateInterpolator mCancelInterpolator;
-    final AccelerateInterpolator mDismissInterpolator;
-    final DecelerateInterpolator mCompleteDismissGestureInterpolator;
+    private boolean mBlockGesture = false;
 
     SwipeDismissController(Context context, DismissibleFrameLayout layout) {
         super(context, layout);
@@ -85,12 +68,8 @@
         mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
         mGestureThresholdPx =
                 Resources.getSystem().getDisplayMetrics().widthPixels * EDGE_SWIPE_THRESHOLD;
-        mAnimationTime = context.getResources().getInteger(
-                android.R.integer.config_shortAnimTime);
-        mCancelInterpolator = new DecelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
-        mDismissInterpolator = new AccelerateInterpolator(DEFAULT_INTERPOLATION_FACTOR);
-        mCompleteDismissGestureInterpolator = new DecelerateInterpolator(
-                DEFAULT_INTERPOLATION_FACTOR);
+
+        mSwipeDismissTransitionHelper = new SwipeDismissTransitionHelper(context, layout);
     }
 
     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
@@ -108,8 +87,16 @@
     }
 
     boolean onInterceptTouchEvent(MotionEvent ev) {
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+        checkGesture(ev);
+        if (mBlockGesture) {
+            return true;
+        }
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        float offsetX = ev.getRawX() - ev.getX();
+        float offsetY = 0.0f;
+        ev.offsetLocation(offsetX, offsetY);
 
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
@@ -117,8 +104,8 @@
                 mDownX = ev.getRawX();
                 mDownY = ev.getRawY();
                 mActiveTouchId = ev.getPointerId(0);
-                mVelocityTracker = VelocityTracker.obtain();
-                mVelocityTracker.addMovement(ev);
+                mSwipeDismissTransitionHelper.obtainVelocityTracker();
+                mSwipeDismissTransitionHelper.getVelocityTracker().addMovement(ev);
                 break;
 
             case MotionEvent.ACTION_POINTER_DOWN:
@@ -141,7 +128,8 @@
                 break;
 
             case MotionEvent.ACTION_MOVE:
-                if (mVelocityTracker == null || mDiscardIntercept) {
+                if (mSwipeDismissTransitionHelper.getVelocityTracker() == null
+                        || mDiscardIntercept) {
                     break;
                 }
 
@@ -155,15 +143,15 @@
                 float x = ev.getX(pointerIndex);
                 float y = ev.getY(pointerIndex);
 
-                if (dx != 0 && mDownX >= mGestureThresholdPx
-                        && canScroll(mLayout, false, dx, x, y)) {
+                if (dx != 0 && mDownX >= mGestureThresholdPx && canScroll(mLayout, false, dx, x,
+                        y)) {
                     mDiscardIntercept = true;
                     break;
                 }
                 updateSwiping(ev);
                 break;
         }
-
+        ev.offsetLocation(-offsetX, -offsetY);
         return (!mDiscardIntercept && mSwiping);
     }
 
@@ -187,114 +175,61 @@
     }
 
     public boolean onTouchEvent(@NonNull MotionEvent ev) {
-        if (mVelocityTracker == null) {
+        checkGesture(ev);
+        if (mBlockGesture) {
+            return true;
+        }
+
+        if (mSwipeDismissTransitionHelper.getVelocityTracker() == null) {
             return false;
         }
 
-        // offset because the view is translated during swipe
-        ev.offsetLocation(mTranslationX, 0);
+        // Offset because the view is translated during swipe, match X with raw X. Active touch
+        // coordinates are mostly used by the velocity tracker, so offset it to match the raw
+        // coordinates which is what is primarily used elsewhere.
+        float offsetX = ev.getRawX() - ev.getX();
+        float offsetY = 0.0f;
+        ev.offsetLocation(offsetX, offsetY);
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_UP:
                 updateDismiss(ev);
+                // Fall through, don't update gesture tracker with the event for ACTION_CANCEL
+            case MotionEvent.ACTION_CANCEL:
                 if (mDismissed) {
-                    dismiss();
-                } else if (mSwiping) {
-                    cancel();
+                    mSwipeDismissTransitionHelper.animateDismissal(mDismissListener);
+                } else if (mSwiping
+                        // Only trigger animation if we had a MOVE event that would shift the
+                        // underlying view, otherwise the animation would be janky.
+                        && mLastX != Integer.MIN_VALUE) {
+                    mSwipeDismissTransitionHelper.animateRecovery(mDismissListener);
                 }
                 resetSwipeDetectMembers();
                 break;
-
-            case MotionEvent.ACTION_CANCEL:
-                cancel();
-                resetSwipeDetectMembers();
-                break;
-
             case MotionEvent.ACTION_MOVE:
-                mVelocityTracker.addMovement(ev);
+                mSwipeDismissTransitionHelper.getVelocityTracker().addMovement(ev);
                 mLastX = ev.getRawX();
                 updateSwiping(ev);
                 if (mSwiping) {
-                    setProgress(ev.getRawX() - mDownX);
+                    mSwipeDismissTransitionHelper.onSwipeProgressChanged(ev.getRawX() - mDownX, ev);
                     break;
                 }
         }
+        ev.offsetLocation(-offsetX, -offsetY);
         return true;
     }
 
-    private void setProgress(float deltaX) {
-        mTranslationX = deltaX;
-        mLayout.setTranslationX(deltaX);
-        mLayout.setAlpha(1 - (deltaX / mLayout.getWidth() * TRANSLATION_MIN_ALPHA));
-        mStarted = true;
-
-        if (mDismissListener != null && deltaX >= 0) {
-            mDismissListener.onDismissStarted();
-        }
-    }
-
-    void dismiss() {
-        mLayout.animate()
-                .translationX(mLayout.getWidth())
-                .alpha(0)
-                .setDuration(mAnimationTime)
-                .setInterpolator(
-                        mStarted ? mCompleteDismissGestureInterpolator
-                                : mDismissInterpolator)
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mDismissListener != null) {
-                                    mDismissListener.onDismissed();
-                                }
-                                resetTranslationAndAlpha();
-                            }
-                        });
-    }
-
-    void cancel() {
-        mStarted = false;
-        mLayout.animate()
-                .translationX(0)
-                .alpha(1)
-                .setDuration(mAnimationTime)
-                .setInterpolator(mCancelInterpolator)
-                .withEndAction(
-                        new Runnable() {
-                            @Override
-                            public void run() {
-                                if (mDismissListener != null) {
-                                    mDismissListener.onDismissCanceled();
-                                }
-                                resetTranslationAndAlpha();
-                            }
-                        });
-    }
-
-    /**
-     * Resets this view to the original state. This method cancels any pending animations on this
-     * view and resets the alpha as well as x translation values.
-     */
-    void resetTranslationAndAlpha() {
-        mLayout.animate().cancel();
-        mLayout.setTranslationX(0);
-        mLayout.setAlpha(1);
-        mStarted = false;
-    }
-
     /** Resets internal members when canceling or finishing a given gesture. */
     private void resetSwipeDetectMembers() {
-        if (mVelocityTracker != null) {
-            mVelocityTracker.recycle();
+        if (mSwipeDismissTransitionHelper.getVelocityTracker() != null) {
+            mSwipeDismissTransitionHelper.getVelocityTracker().recycle();
         }
-        mVelocityTracker = null;
-        mTranslationX = 0;
+        mSwipeDismissTransitionHelper.resetVelocityTracker();
         mDownX = 0;
         mDownY = 0;
         mSwiping = false;
+        mLastX = Integer.MIN_VALUE;
         mDismissed = false;
         mDiscardIntercept = false;
-        mCanStartSwipe = true;
     }
 
     private void updateSwiping(MotionEvent ev) {
@@ -302,33 +237,40 @@
             float deltaX = ev.getRawX() - mDownX;
             float deltaY = ev.getRawY() - mDownY;
             if (isPotentialSwipe(deltaX, deltaY)) {
-                // There are three conditions on which we want want to start swiping:
-                // 1. The swipe is from left to right AND
-                // 2. It is horizontal AND
-                // 3. We actually can start swiping
-                mSwiping = mCanStartSwipe && Math.abs(deltaY) < Math.abs(deltaX) && deltaX > 0;
-                mCanStartSwipe = mSwiping;
+                mSwiping = deltaX > mSlop * 2
+                        && Math.abs(deltaY) < Math.abs(deltaX);
+            } else {
+                mSwiping = false;
             }
         }
     }
 
     private void updateDismiss(@NonNull MotionEvent ev) {
         float deltaX = ev.getRawX() - mDownX;
-        mVelocityTracker.addMovement(ev);
-        mVelocityTracker.computeCurrentVelocity(1000);
+        // Don't add the motion event as an UP event would clear the velocity tracker
+        VelocityTracker velocityTracker = mSwipeDismissTransitionHelper.getVelocityTracker();
+        velocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        float xVelocity = velocityTracker.getXVelocity();
+        float yVelocity = velocityTracker.getYVelocity();
+        if (mLastX == Integer.MIN_VALUE) {
+            // If there's no changes to mLastX, we have only one point of data, and therefore no
+            // velocity. Estimate velocity from just the up and down event in that case.
+            xVelocity = deltaX / ((ev.getEventTime() - ev.getDownTime()) / 1000f);
+        }
+
         if (!mDismissed) {
             if ((deltaX > (mLayout.getWidth() * mDismissMinDragWidthRatio)
                     && ev.getRawX() >= mLastX)
-                    || (mVelocityTracker.getXVelocity() >= mMinFlingVelocity
-                    && mVelocityTracker.getXVelocity() > Math.abs(
-                    mVelocityTracker.getYVelocity()))) {
+                    || (xVelocity >= mMinFlingVelocity
+                    && xVelocity > Math.abs(
+                    yVelocity)))  {
                 mDismissed = true;
             }
         }
         // Check if the user tried to undo this.
         if (mDismissed && mSwiping) {
             // Check if the user's finger is actually flinging back to left
-            if (mVelocityTracker.getXVelocity() < -mMinFlingVelocity) {
+            if (xVelocity < -mMinFlingVelocity) {
                 mDismissed = false;
             }
         }
@@ -367,4 +309,10 @@
 
         return checkV && v.canScrollHorizontally((int) -dx);
     }
-}
+
+    private void checkGesture(MotionEvent ev) {
+        if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mBlockGesture = mSwipeDismissTransitionHelper.isAnimating();
+        }
+    }
+}
\ No newline at end of file
diff --git a/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java
new file mode 100644
index 0000000..75ff715
--- /dev/null
+++ b/wear/wear/src/main/java/androidx/wear/widget/SwipeDismissTransitionHelper.java
@@ -0,0 +1,340 @@
+/*
+ * 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.widget;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Color;
+import android.graphics.ColorFilter;
+import android.graphics.Outline;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewOutlineProvider;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.dynamicanimation.animation.DynamicAnimation;
+import androidx.dynamicanimation.animation.FloatValueHolder;
+import androidx.dynamicanimation.animation.SpringAnimation;
+import androidx.dynamicanimation.animation.SpringForce;
+
+/**
+ * A helper class to handle transition of swiping to dismiss and dismiss animation.
+ */
+class SwipeDismissTransitionHelper {
+
+    private static final String TAG = "SwipeDismissTransitionHelper";
+    private static final float SCALE_MIN = 0.7f;
+    private static final float SCALE_MAX = 1.0f;
+    public static final float SCRIM_BACKGROUND_MAX = 0.5f;
+    private static final float DIM_FOREGROUND_PROGRESS_FACTOR = 2.0f;
+    private static final float DIM_FOREGROUND_MIN = 0.3f;
+    private static final int VELOCITY_UNIT = 1000;
+    // Spring properties
+    private static final float SPRING_STIFFNESS = 600f;
+    private static final float SPRING_DAMPING_RATIO = SpringForce.DAMPING_RATIO_NO_BOUNCY;
+    private static final float SPRING_MIN_VISIBLE_CHANGE = 0.5f;
+    private static final int SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX = 5;
+    private final DismissibleFrameLayout mLayout;
+
+    private final int mScreenWidth;
+    private final SparseArray<ColorFilter> mDimmingColorFilterCache = new SparseArray<>();
+    private final View mScrimBackground;
+    private final boolean mIsScreenRound;
+    private final Paint mCompositingPaint = new Paint();
+
+    private VelocityTracker mVelocityTracker;
+    private boolean mStarted;
+    private int mOriginalViewWidth;
+    private float mTranslationX;
+    private float mScale;
+    private float mProgress;
+    private float mDimming;
+    private SpringAnimation mDismissalSpring;
+    private SpringAnimation mRecoverySpring;
+
+    SwipeDismissTransitionHelper(@NonNull Context context,
+            @NonNull DismissibleFrameLayout layout) {
+        mLayout = layout;
+        mIsScreenRound = layout.getResources().getConfiguration().isScreenRound();
+        mScreenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
+        mScrimBackground = new View(context);
+        clipOutline(mScrimBackground, mIsScreenRound);
+        mScrimBackground.setLayoutParams(
+                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.MATCH_PARENT));
+        mScrimBackground.setBackgroundColor(Color.BLACK);
+    }
+
+    private static void clipOutline(@NonNull View view, boolean useRoundShape) {
+        view.setOutlineProvider(new ViewOutlineProvider() {
+            @Override
+            public void getOutline(View view, Outline outline) {
+                if (useRoundShape) {
+                    outline.setOval(0, 0, view.getWidth(), view.getHeight());
+                } else {
+                    outline.setRect(0, 0, view.getWidth(), view.getHeight());
+                }
+                outline.setAlpha(0);
+            }
+        });
+        view.setClipToOutline(true);
+    }
+
+
+    private static float lerp(float min, float max, float value) {
+        return min + (max - min) * value;
+    }
+
+    private static float clamp(float min, float max, float value) {
+        return max(min, min(max, value));
+    }
+
+    private static float lerpInv(float min, float max, float value) {
+        return min != max ? ((value - min) / (max - min)) : 0.0f;
+    }
+
+    private ColorFilter createDimmingColorFilter(float level) {
+        level = clamp(0, 1, level);
+        int alpha = (int) (0xFF * level);
+        int color = Color.argb(alpha, 0, 0, 0);
+        ColorFilter colorFilter = mDimmingColorFilterCache.get(alpha);
+        if (colorFilter != null) {
+            return colorFilter;
+        }
+        colorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+        mDimmingColorFilterCache.put(alpha, colorFilter);
+        return colorFilter;
+    }
+
+    private SpringAnimation createSpringAnimation(float startValue,
+            float finalValue,
+            float startVelocity,
+            DynamicAnimation.OnAnimationUpdateListener onUpdateListener,
+            DynamicAnimation.OnAnimationEndListener onEndListener) {
+        SpringAnimation animation = new SpringAnimation(new FloatValueHolder());
+        animation.setStartValue(startValue);
+        animation.setMinimumVisibleChange(SPRING_MIN_VISIBLE_CHANGE);
+        SpringForce spring = new SpringForce();
+        spring.setFinalPosition(finalValue);
+        spring.setDampingRatio(SPRING_DAMPING_RATIO);
+        spring.setStiffness(SPRING_STIFFNESS);
+        animation.setMinValue(0.0f);
+        animation.setMaxValue(mScreenWidth);
+        animation.setStartVelocity(startVelocity);
+        animation.setSpring(spring);
+        animation.addUpdateListener(onUpdateListener);
+        animation.addEndListener(onEndListener);
+        animation.start();
+        return animation;
+    }
+
+    /**
+     * Updates the swipe progress
+     *
+     * @param deltaX The X delta of gesture
+     * @param ev     The motion event
+     */
+    void onSwipeProgressChanged(float deltaX, @NonNull MotionEvent ev) {
+        if (!mStarted) {
+            initializeTransition();
+        }
+
+        mVelocityTracker.addMovement(ev);
+        mOriginalViewWidth = mLayout.getWidth();
+        // For swiping, mProgress is directly manipulated
+        // mProgress = 0 (no swipe) - 0.5 (swiped to mid screen) - 1 (swipe to right of screen)
+        mProgress = deltaX / mOriginalViewWidth;
+        // Solve for other variables
+        // Scale = lerp 100% -> 70% when swiping from left edge to right edge
+        mScale = lerp(SCALE_MAX, SCALE_MIN, mProgress);
+        // Translation: make sure the right edge of mOriginalView touches right edge of screen
+        mTranslationX = max(0f, 1 - mScale) * mLayout.getWidth() / 2.0f;
+        mDimming = Math.min(DIM_FOREGROUND_MIN, mProgress / DIM_FOREGROUND_PROGRESS_FACTOR);
+
+        updateView();
+    }
+
+    private void onDismissalRecoveryAnimationProgressChanged(float translationX) {
+        mOriginalViewWidth = mLayout.getWidth();
+        mTranslationX = translationX;
+
+        mScale = 1 - mTranslationX * 2 / mOriginalViewWidth;
+        // Clamp mScale so that we can solve for mProgress
+        mScale = Math.max(SCALE_MIN, Math.min(mScale, SCALE_MAX));
+        float nextProgress = lerpInv(SCALE_MAX, SCALE_MIN, mScale);
+        if (nextProgress > mProgress) {
+            mProgress = nextProgress;
+        }
+        mDimming = Math.min(DIM_FOREGROUND_MIN, mProgress / DIM_FOREGROUND_PROGRESS_FACTOR);
+        updateView();
+    }
+
+    private void updateView() {
+        mLayout.setScaleX(mScale);
+        mLayout.setScaleY(mScale);
+        mLayout.setTranslationX(mTranslationX);
+        updateDim();
+        updateScrim();
+    }
+
+    private void updateDim() {
+        mCompositingPaint.setColorFilter(createDimmingColorFilter(mDimming));
+        mLayout.setLayerPaint(mCompositingPaint);
+    }
+
+    private void updateScrim() {
+        float alpha =  SCRIM_BACKGROUND_MAX * (1 - mProgress);
+        mScrimBackground.setAlpha(alpha);
+    }
+
+    private void initializeTransition() {
+        mStarted = true;
+        ViewGroup originalParentView = getOriginalParentView();
+        ViewParent scrimBackgroundParent = mScrimBackground.getParent();
+
+        if (originalParentView == null) return;
+
+        // Check if scrim background is already attached to the parent view.
+        if (scrimBackgroundParent != originalParentView) {
+            originalParentView.addView(mScrimBackground);
+            mLayout.bringToFront();
+        }
+
+        mCompositingPaint.setColorFilter(null);
+        mLayout.setLayerType(View.LAYER_TYPE_HARDWARE, mCompositingPaint);
+        clipOutline(mLayout, mIsScreenRound);
+    }
+
+    private void resetTranslationAndAlpha() {
+        // resetting variables
+        mStarted = false;
+        mTranslationX = 0;
+        mProgress = 0;
+        mScale = 1;
+        // resetting layout params
+        mLayout.setTranslationX(0);
+        mLayout.setScaleX(1);
+        mLayout.setScaleY(1);
+        mLayout.setAlpha(1);
+        mScrimBackground.setAlpha(0);
+
+        mCompositingPaint.setColorFilter(null);
+        mLayout.setLayerType(View.LAYER_TYPE_NONE, null);
+        mLayout.setClipToOutline(false);
+    }
+
+    /**
+     * @return If dismiss or recovery animation is running.
+     */
+    boolean isAnimating() {
+        return (mDismissalSpring != null && mDismissalSpring.isRunning()) || (
+                mRecoverySpring != null && mRecoverySpring.isRunning());
+    }
+
+    /**
+     * Triggers the recovery animation.
+     */
+    void animateRecovery(@Nullable DismissController.OnDismissListener dismissListener) {
+        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        mRecoverySpring = createSpringAnimation(mTranslationX, 0, mVelocityTracker.getXVelocity(),
+                (animation, value, velocity) -> {
+                    float distanceRemaining = Math.max(0, (value - 0));
+                    if (distanceRemaining <= SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX
+                            && mRecoverySpring != null) {
+                        // Skip last 2% of animation.
+                        mRecoverySpring.skipToEnd();
+                    }
+                    onDismissalRecoveryAnimationProgressChanged(value);
+                }, (animation, canceled, value, velocity) -> {
+
+                    resetTranslationAndAlpha();
+                    if (dismissListener != null) {
+                        dismissListener.onDismissCanceled();
+                    }
+                });
+    }
+
+    /**
+     * Triggers the dismiss animation.
+     */
+    void animateDismissal(@Nullable DismissController.OnDismissListener dismissListener) {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT);
+        // Dismissal has started
+        if (dismissListener != null) {
+            dismissListener.onDismissStarted();
+        }
+
+        mDismissalSpring = createSpringAnimation(mTranslationX, mScreenWidth,
+                mVelocityTracker.getXVelocity(), (animation, value, velocity) -> {
+                    float distanceRemaining = Math.max(0, (mScreenWidth - value));
+                    if (distanceRemaining <= SPRING_ANIMATION_PROGRESS_FINISH_THRESHOLD_PX
+                            && mDismissalSpring != null) {
+                        // Skip last 2% of animation.
+                        mDismissalSpring.skipToEnd();
+                    }
+                    onDismissalRecoveryAnimationProgressChanged(value);
+                }, (animation, canceled, value, velocity) -> {
+                    resetTranslationAndAlpha();
+                    if (dismissListener != null) {
+                        dismissListener.onDismissed();
+                    }
+                });
+    }
+
+    private @Nullable ViewGroup getOriginalParentView() {
+        if (mLayout.getParent() instanceof ViewGroup) {
+            return (ViewGroup) mLayout.getParent();
+        }
+        return null;
+    }
+
+    /**
+     * @return The velocity tracker.
+     */
+    @Nullable
+    VelocityTracker getVelocityTracker() {
+        return mVelocityTracker;
+    }
+
+    /**
+     * Obtain velocity tracker.
+     */
+    void obtainVelocityTracker() {
+        mVelocityTracker = VelocityTracker.obtain();
+    }
+
+    /**
+     * Reset velocity tracker to null.
+     */
+    void resetVelocityTracker() {
+        mVelocityTracker = null;
+    }
+}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index db9f65c..d2e5209 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -42,7 +42,7 @@
     private final int mFinishSecondaryWithPrimary;
     private final boolean mClearTop;
 
-    SplitPairRule(float splitRatio, @LayoutDir int layoutDirection,
+    SplitPairRule(float splitRatio, @LayoutDirection int layoutDirection,
             @SplitFinishBehavior int finishPrimaryWithSecondary,
             @SplitFinishBehavior int finishSecondaryWithPrimary, boolean clearTop,
             @NonNull Predicate<Pair<Activity, Activity>> activityPairPredicate,
@@ -115,7 +115,7 @@
         @NonNull
         private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
         private float mSplitRatio;
-        @LayoutDir
+        @LayoutDirection
         private int mLayoutDirection;
         private boolean mClearTop;
         @SplitFinishBehavior
@@ -140,7 +140,7 @@
 
         /** @see SplitRule#getLayoutDirection() */
         @NonNull
-        public Builder setLayoutDirection(@LayoutDir int layoutDirection) {
+        public Builder setLayoutDirection(@LayoutDirection int layoutDirection) {
             mLayoutDirection = layoutDirection;
             return this;
         }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
index 1f3aea3..7b30849 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
@@ -58,7 +58,7 @@
     private final int mFinishPrimaryWithPlaceholder;
 
     SplitPlaceholderRule(@NonNull Intent placeholderIntent,
-            float splitRatio, @LayoutDir int layoutDirection, boolean isSticky,
+            float splitRatio, @LayoutDirection int layoutDirection, boolean isSticky,
             @SplitPlaceholderFinishBehavior int finishPrimaryWithPlaceholder,
             @NonNull Predicate<Activity> activityPredicate,
             @NonNull Predicate<Intent> intentPredicate,
@@ -137,7 +137,7 @@
         @NonNull
         private final Intent mPlaceholderIntent;
         private float mSplitRatio;
-        @LayoutDir
+        @LayoutDirection
         private int mLayoutDirection;
         private boolean mIsSticky = false;
         @SplitPlaceholderFinishBehavior
@@ -162,7 +162,7 @@
 
         /** @see SplitRule#getLayoutDirection() */
         @NonNull
-        public Builder setLayoutDirection(@LayoutDir int layoutDirection) {
+        public Builder setLayoutDirection(@LayoutDirection int layoutDirection) {
             mLayoutDirection = layoutDirection;
             return this;
         }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 39b7df8..bb24318 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -16,9 +16,12 @@
 
 package androidx.window.extensions.embedding;
 
+import static android.util.LayoutDirection.LOCALE;
+import static android.util.LayoutDirection.LTR;
+import static android.util.LayoutDirection.RTL;
+
 import android.annotation.SuppressLint;
 import android.os.Build;
-import android.util.LayoutDirection;
 import android.view.WindowMetrics;
 
 import androidx.annotation.IntDef;
@@ -40,17 +43,12 @@
     @NonNull
     private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
     private final float mSplitRatio;
-    @LayoutDir
+    @LayoutDirection
     private final int mLayoutDirection;
 
-    @IntDef({
-            LayoutDirection.LTR,
-            LayoutDirection.RTL,
-            LayoutDirection.LOCALE
-    })
+    @IntDef({LTR, RTL, LOCALE})
     @Retention(RetentionPolicy.SOURCE)
-    // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection
-    @interface LayoutDir {}
+    @interface LayoutDirection {}
     /**
      * Never finish the associated container.
      * @see SplitFinishBehavior
@@ -91,7 +89,7 @@
     @interface SplitFinishBehavior {}
 
     SplitRule(@NonNull Predicate<WindowMetrics> parentWindowMetricsPredicate, float splitRatio,
-            @LayoutDir int layoutDirection) {
+            @LayoutDirection int layoutDirection) {
         mParentWindowMetricsPredicate = parentWindowMetricsPredicate;
         mSplitRatio = splitRatio;
         mLayoutDirection = layoutDirection;
@@ -110,7 +108,7 @@
         return mSplitRatio;
     }
 
-    @LayoutDir
+    @LayoutDirection
     public int getLayoutDirection() {
         return mLayoutDirection;
     }
diff --git a/window/window-samples/src/main/AndroidManifest.xml b/window/window-samples/src/main/AndroidManifest.xml
index 2952118..9c79acd 100644
--- a/window/window-samples/src/main/AndroidManifest.xml
+++ b/window/window-samples/src/main/AndroidManifest.xml
@@ -18,6 +18,18 @@
         android:label="@string/app_name"
         android:supportsRtl="true"
         android:theme="@style/AppTheme">
+
+        <service android:name="androidx.window.sample.TestIme"
+            android:label="@string/test_ime"
+            android:permission="android.permission.BIND_INPUT_METHOD"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.view.InputMethod"/>
+            </intent-filter>
+            <meta-data android:name="android.view.im"
+                android:resource="@xml/method"/>
+        </service>
+
         <activity android:name=".demos.WindowDemosActivity"
             android:exported="true"
             android:label="@string/windowManagerDemos">
@@ -172,6 +184,13 @@
             android:taskAffinity="androidx.window.sample.split_pip">
         </activity>
 
+        <!-- The demo app that shows various IME-related use cases -->
+
+        <activity android:name=".ImeActivity"
+            android:exported="false"
+            android:configChanges="orientation|screenSize|screenLayout|screenSize"
+            android:label="@string/ime"/>
+
         <!-- ActivityEmbedding Initializer -->
 
         <provider android:name="androidx.startup.InitializationProvider"
diff --git a/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
new file mode 100644
index 0000000..5ee6536
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/ImeActivity.kt
@@ -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.window.sample
+
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Demo app that shows various IME-related features.
+ */
+class ImeActivity : AppCompatActivity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_ime)
+
+        findViewById<Button>(R.id.ime_button_settings).apply {
+            setOnClickListener {
+                val intent = Intent(Settings.ACTION_INPUT_METHOD_SETTINGS)
+                startActivity(intent)
+            }
+        }
+
+        findViewById<Button>(R.id.ime_button_switch_default).apply {
+            setOnClickListener {
+                val imm = getSystemService(InputMethodManager::class.java)
+                imm.showInputMethodPicker()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt b/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
new file mode 100644
index 0000000..d337e11
--- /dev/null
+++ b/window/window-samples/src/main/java/androidx/window/sample/TestIme.kt
@@ -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.window.sample
+
+import android.inputmethodservice.InputMethodService
+import android.view.View
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+
+/**
+ * A test IME that currently provides a minimal UI containing a "Close" button. To use this, go to
+ * "Settings > System > Languages & Input > On-screen keyboard" and enable "Test IME". Remember you
+ * may still need to switch to this IME after the default on-screen keyboard pops up.
+ */
+internal class TestIme : InputMethodService() {
+
+    override fun onCreateInputView(): View {
+        return layoutInflater.inflate(R.layout.test_ime, null).apply {
+            findViewById<Button>(R.id.button_close).setOnClickListener {
+                requestHideSelf(InputMethodManager.HIDE_NOT_ALWAYS)
+            }
+        }
+    }
+
+    override fun onEvaluateFullscreenMode(): Boolean {
+        return false
+    }
+}
\ No newline at end of file
diff --git a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt b/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
index 6b39067..dd2f206 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/demos/WindowDemosActivity.kt
@@ -21,6 +21,7 @@
 import androidx.recyclerview.widget.RecyclerView
 import androidx.window.sample.DisplayFeaturesConfigChangeActivity
 import androidx.window.sample.DisplayFeaturesNoConfigChangeActivity
+import androidx.window.sample.ImeActivity
 import androidx.window.sample.PresentationActivity
 import androidx.window.sample.R
 import androidx.window.sample.R.string.display_features_config_change
@@ -63,6 +64,11 @@
                 buttonTitle = getString(R.string.presentation),
                 description = getString(R.string.presentation_demo_description),
                 clazz = PresentationActivity::class.java
+            ),
+            DemoItem(
+                buttonTitle = getString(R.string.ime),
+                description = getString(R.string.ime_demo_description),
+                clazz = ImeActivity::class.java
             )
         )
         val recyclerView = findViewById<RecyclerView>(R.id.demo_recycler_view)
diff --git a/window/window-samples/src/main/res/layout/activity_ime.xml b/window/window-samples/src/main/res/layout/activity_ime.xml
new file mode 100644
index 0000000..5947c94
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/activity_ime.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_demo_reminder"/>
+
+    <Button
+        android:id="@+id/ime_button_settings"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_button_settings"/>
+
+    <Button
+        android:id="@+id/ime_button_switch_default"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/ime_button_switch_default"/>
+
+    <Space
+        android:layout_width="match_parent"
+        android:layout_height="16dp"/>
+
+    <EditText
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:hint="@string/window_metrics_ime_hint"/>
+
+</androidx.appcompat.widget.LinearLayoutCompat>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/layout/test_ime.xml b/window/window-samples/src/main/res/layout/test_ime.xml
new file mode 100644
index 0000000..feda1d7
--- /dev/null
+++ b/window/window-samples/src/main/res/layout/test_ime.xml
@@ -0,0 +1,28 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <Button
+        android:id="@+id/button_close"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/test_ime_button_close"/>
+
+</LinearLayout>
\ No newline at end of file
diff --git a/window/window-samples/src/main/res/values/strings.xml b/window/window-samples/src/main/res/values/strings.xml
index 5faaae6..45e3de2 100644
--- a/window/window-samples/src/main/res/values/strings.xml
+++ b/window/window-samples/src/main/res/values/strings.xml
@@ -51,4 +51,12 @@
     <string name="occlusion_is_none">Occlusion is none</string>
     <string name="window_metrics">Window metrics</string>
     <string name="window_metrics_description">Demo of using WindowMetrics API with activity handling rotations.</string>
+    <string name="test_ime">Test IME</string>
+    <string name="test_ime_button_close">Close Test IME</string>
+    <string name="window_metrics_ime_hint">Tap to open IME</string>
+    <string name="ime">IME</string>
+    <string name="ime_demo_description">Demo of using various APIs from within IME.</string>
+    <string name="ime_demo_reminder">Reminder: To use the Test IME bundled with this application, remember to enable it in System Settings.</string>
+    <string name="ime_button_settings">System IME Settings</string>
+    <string name="ime_button_switch_default">Switch default IME</string>
 </resources>
diff --git a/window/window-samples/src/main/res/xml/method.xml b/window/window-samples/src/main/res/xml/method.xml
new file mode 100644
index 0000000..6d61dd9
--- /dev/null
+++ b/window/window-samples/src/main/res/xml/method.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.
+  -->
+
+<!-- Note that this file is required even if it contains nothing. -->
+<input-method>
+</input-method>
\ No newline at end of file
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 27cf3e7..2e7de91 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -101,7 +101,7 @@
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
   }
 
@@ -120,7 +120,7 @@
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
   }
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 93e20f5..361f795 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -108,7 +108,7 @@
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
   }
 
@@ -127,7 +127,7 @@
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
   }
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 27cf3e7..2e7de91 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -101,7 +101,7 @@
     method public androidx.window.embedding.SplitPairRule.Builder setClearTop(boolean clearTop);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int finishPrimaryWithSecondary);
     method public androidx.window.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int finishSecondaryWithPrimary);
-    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPairRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPairRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
   }
 
@@ -120,7 +120,7 @@
     ctor public SplitPlaceholderRule.Builder(java.util.Set<androidx.window.embedding.ActivityFilter> filters, android.content.Intent placeholderIntent, @IntRange(from=0L) int minWidth, @IntRange(from=0L) int minSmallestWidth);
     method public androidx.window.embedding.SplitPlaceholderRule build();
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithPlaceholder(int finishPrimaryWithPlaceholder);
-    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDir(int layoutDir);
+    method public androidx.window.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int layoutDirection);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSplitRatio(@FloatRange(from=0.0, to=1.0) float splitRatio);
     method public androidx.window.embedding.SplitPlaceholderRule.Builder setSticky(boolean isSticky);
   }
diff --git a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
index 07b2d1e..164c867 100644
--- a/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
+++ b/window/window/src/androidTest/java/androidx/window/embedding/EmbeddingRuleConstructionTests.kt
@@ -106,7 +106,7 @@
             .setFinishSecondaryWithPrimary(FINISH_ADJACENT)
             .setClearTop(true)
             .setSplitRatio(0.3f)
-            .setLayoutDir(LayoutDirection.LTR)
+            .setLayoutDirection(LayoutDirection.LTR)
             .build()
         assertEquals(FINISH_ADJACENT, rule.finishPrimaryWithSecondary)
         assertEquals(FINISH_ADJACENT, rule.finishSecondaryWithPrimary)
@@ -221,7 +221,7 @@
             .setFinishPrimaryWithPlaceholder(FINISH_ADJACENT)
             .setSticky(true)
             .setSplitRatio(0.3f)
-            .setLayoutDir(LayoutDirection.LTR)
+            .setLayoutDirection(LayoutDirection.LTR)
             .build()
         assertEquals(FINISH_ADJACENT, rule.finishPrimaryWithPlaceholder)
         assertEquals(true, rule.isSticky)
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index 2d88b02..0c6555e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -19,7 +19,6 @@
 import android.app.Activity
 import android.content.Context
 import androidx.core.util.Consumer
-import androidx.window.embedding.SplitController.Companion
 import java.util.concurrent.Executor
 import java.util.concurrent.locks.ReentrantLock
 import kotlin.concurrent.withLock
@@ -27,14 +26,15 @@
 /**
  * Controller class that will be used to get information about the currently active activity splits,
  * as well as provide interaction points to customize them and form new splits. A split is a pair of
- * containers that host activities in the same or different tasks, combined under the same parent
- * window of the hosting task.
+ * containers that host activities in the same or different processes, combined under the same
+ * parent window of the hosting task.
  * <p>A pair of activities can be put into split by providing a static or runtime split rule and
- * launching activity in the same task and process using [android.content.Context.startActivity].
- * <p>This class should be configured before [android.app.Application.onCreate] for upcoming
- * activity launches using the split rules statically defined in an XML using
- * [androidx.startup.Initializer] and [Companion.initialize]. See Jetpack App Startup reference
- * for more information.
+ * launching activity in the same task using [android.app.Activity.startActivity].
+ * <p>This class is recommended to be configured in [androidx.startup.Initializer] or
+ * [android.app.Application.onCreate], so that the rules are applied early in the application
+ * startup before any activities complete initialization. The rule updates only apply to future
+ * [android.app.Activity] launches and do not apply to already running activities.
+ * @see initialize
  */
 class SplitController private constructor() {
     private val embeddingBackend: EmbeddingBackend = ExtensionEmbeddingBackend.getInstance()
@@ -169,7 +169,8 @@
          * app-provided XML. The rules will be kept for the lifetime of the application process.
          * <p>It's recommended to set the static rules via an [androidx.startup.Initializer], or
          * [android.app.Application.onCreate], so that they are applied early in the application
-         * startup before any activities appear.
+         * startup before any activities complete initialization. The rule updates only apply to
+         * future [android.app.Activity] launches and do not apply to already running activities.
          * <p>Note that it is not necessary to call this function in order to use [SplitController].
          * If the app doesn't have any static rule, it can use [registerRule] to register rules at
          * any time.
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 24c497f..e6c083b 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairRule.kt
@@ -16,7 +16,7 @@
 
 package androidx.window.embedding
 
-import android.util.LayoutDirection
+import android.util.LayoutDirection.LOCALE
 import androidx.annotation.FloatRange
 import androidx.annotation.IntRange
 import androidx.core.util.Preconditions.checkArgument
@@ -69,8 +69,8 @@
         @IntRange(from = 0) minWidth: Int,
         @IntRange(from = 0) minSmallestWidth: Int,
         @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
-        @LayoutDir layoutDir: Int = LayoutDirection.LOCALE
-    ) : super(minWidth, minSmallestWidth, splitRatio, layoutDir) {
+        @LayoutDirection layoutDirection: Int = LOCALE
+    ) : super(minWidth, minSmallestWidth, splitRatio, layoutDirection) {
         checkArgumentNonnegative(minWidth, "minWidth must be non-negative")
         checkArgumentNonnegative(minSmallestWidth, "minSmallestWidth must be non-negative")
         checkArgument(splitRatio in 0.0..1.0, "splitRatio must be in 0.0..1.0 range")
@@ -100,8 +100,8 @@
         private var clearTop: Boolean = false
         @FloatRange(from = 0.0, to = 1.0)
         private var splitRatio: Float = 0.5f
-        @LayoutDir
-        private var layoutDir: Int = LayoutDirection.LOCALE
+        @LayoutDirection
+        private var layoutDirection: Int = LOCALE
 
         /**
          * @see SplitPairRule.finishPrimaryWithSecondary
@@ -135,12 +135,11 @@
         /**
          * @see SplitPairRule.layoutDirection
          */
-        @SuppressWarnings("MissingGetterMatchingBuilder")
-        fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
-            apply { this.layoutDir = layoutDir }
+        fun setLayoutDirection(@LayoutDirection layoutDirection: Int): Builder =
+            apply { this.layoutDirection = layoutDirection }
 
         fun build() = SplitPairRule(filters, finishPrimaryWithSecondary, finishSecondaryWithPrimary,
-            clearTop, minWidth, minSmallestWidth, splitRatio, layoutDir)
+            clearTop, minWidth, minSmallestWidth, splitRatio, layoutDirection)
     }
 
     /**
@@ -156,7 +155,7 @@
             .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
             .setClearTop(clearTop)
             .setSplitRatio(splitRatio)
-            .setLayoutDir(layoutDirection)
+            .setLayoutDirection(layoutDirection)
             .build()
     }
 
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 5c2c6ee..3d10737 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPlaceholderRule.kt
@@ -17,7 +17,7 @@
 package androidx.window.embedding
 
 import android.content.Intent
-import android.util.LayoutDirection
+import android.util.LayoutDirection.LOCALE
 import androidx.annotation.FloatRange
 import androidx.annotation.IntDef
 import androidx.annotation.IntRange
@@ -25,7 +25,16 @@
 import androidx.core.util.Preconditions.checkArgumentNonnegative
 
 /**
- * Configuration rules for split placeholders.
+ * Configuration rules for split placeholders. A placeholder activity is usually a mostly empty
+ * activity that occupies an area of split. It might provide some additional optional features, but
+ * must not host important UI elements exclusively, since the placeholder would not show on some
+ * devices and screen configurations. It is expected to be replaced when other activity with content
+ * is launched in a dedicated [SplitPairRule]. The placeholder activity will then be occluded by
+ * the new launched activity.
+ *
+ * See the
+ * [Placeholders](https://developer.android.com/guide/topics/large-screens/activity-embedding#placeholders)
+ * section in the official documentation for visual samples and references.
  */
 class SplitPlaceholderRule : SplitRule {
 
@@ -71,7 +80,7 @@
         @IntRange(from = 0) minWidth: Int = 0,
         @IntRange(from = 0) minSmallestWidth: Int = 0,
         @FloatRange(from = 0.0, to = 1.0) splitRatio: Float = 0.5f,
-        @LayoutDir layoutDirection: Int = LayoutDirection.LOCALE
+        @LayoutDirection layoutDirection: Int = LOCALE
     ) : super(minWidth, minSmallestWidth, splitRatio, layoutDirection) {
         checkArgumentNonnegative(minWidth, "minWidth must be non-negative")
         checkArgumentNonnegative(minSmallestWidth, "minSmallestWidth must be non-negative")
@@ -105,8 +114,8 @@
         private var isSticky: Boolean = false
         @FloatRange(from = 0.0, to = 1.0)
         private var splitRatio: Float = 0.5f
-        @LayoutDir
-        private var layoutDir: Int = LayoutDirection.LOCALE
+        @LayoutDirection
+        private var layoutDirection: Int = LOCALE
 
         /**
          * @see SplitPlaceholderRule.finishPrimaryWithPlaceholder
@@ -133,12 +142,11 @@
         /**
          * @see SplitPlaceholderRule.layoutDirection
          */
-        @SuppressWarnings("MissingGetterMatchingBuilder")
-        fun setLayoutDir(@LayoutDir layoutDir: Int): Builder =
-            apply { this.layoutDir = layoutDir }
+        fun setLayoutDirection(@LayoutDirection layoutDirection: Int): Builder =
+            apply { this.layoutDirection = layoutDirection }
 
         fun build() = SplitPlaceholderRule(filters, placeholderIntent, isSticky,
-            finishPrimaryWithPlaceholder, minWidth, minSmallestWidth, splitRatio, layoutDir)
+            finishPrimaryWithPlaceholder, minWidth, minSmallestWidth, splitRatio, layoutDirection)
     }
 
     /**
@@ -153,7 +161,7 @@
             .setSticky(isSticky)
             .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
             .setSplitRatio(splitRatio)
-            .setLayoutDir(layoutDirection)
+            .setLayoutDirection(layoutDirection)
             .build()
     }
 
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
index 0bda0a0..d588888 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRule.kt
@@ -18,7 +18,9 @@
 
 import android.graphics.Rect
 import android.os.Build
-import android.util.LayoutDirection
+import android.util.LayoutDirection.LOCALE
+import android.util.LayoutDirection.LTR
+import android.util.LayoutDirection.RTL
 import android.view.WindowMetrics
 import androidx.annotation.DoNotInline
 import androidx.annotation.FloatRange
@@ -63,14 +65,13 @@
     /**
      * The layout direction for the split.
      */
-    @LayoutDir
-    val layoutDirection: Int = LayoutDirection.LOCALE
+    @LayoutDirection
+    val layoutDirection: Int = LOCALE
 ) : EmbeddingRule() {
 
-    @IntDef(LayoutDirection.LTR, LayoutDirection.RTL, LayoutDirection.LOCALE)
+    @IntDef(LTR, RTL, LOCALE)
     @Retention(AnnotationRetention.SOURCE)
-    // Not called LayoutDirection to avoid conflict with android.util.LayoutDirection
-    internal annotation class LayoutDir
+    internal annotation class LayoutDirection
 
     /**
      * Determines what happens with the associated container when all activities are finished in
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 5cfea09..7e54dc0 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitRuleParser.kt
@@ -161,7 +161,7 @@
             .setFinishSecondaryWithPrimary(finishSecondaryWithPrimary)
             .setClearTop(clearTop)
             .setSplitRatio(ratio)
-            .setLayoutDir(layoutDir)
+            .setLayoutDirection(layoutDir)
             .build()
     }
 
@@ -223,7 +223,7 @@
         ).setSticky(stickyPlaceholder)
             .setFinishPrimaryWithPlaceholder(finishPrimaryWithPlaceholder)
             .setSplitRatio(ratio)
-            .setLayoutDir(layoutDir)
+            .setLayoutDirection(layoutDir)
             .build()
     }
 
diff --git a/work/work-gcm/api/2.8.0-beta03.txt b/work/work-gcm/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-gcm/api/res-2.8.0-beta03.txt b/work/work-gcm/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-gcm/api/res-2.8.0-beta03.txt
diff --git a/work/work-gcm/api/restricted_2.8.0-beta03.txt b/work/work-gcm/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/work/work-gcm/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/work/work-multiprocess/api/2.8.0-beta03.txt b/work/work-multiprocess/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-multiprocess/api/res-2.8.0-beta03.txt b/work/work-multiprocess/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-multiprocess/api/res-2.8.0-beta03.txt
diff --git a/work/work-multiprocess/api/restricted_2.8.0-beta03.txt b/work/work-multiprocess/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..bd27cfb
--- /dev/null
+++ b/work/work-multiprocess/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,26 @@
+// Signature format: 4.0
+package androidx.work.multiprocess {
+
+  public abstract class RemoteCoroutineWorker extends androidx.work.multiprocess.RemoteListenableWorker {
+    ctor public RemoteCoroutineWorker(android.content.Context context, androidx.work.WorkerParameters parameters);
+    method public abstract suspend Object? doRemoteWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method public final void onStopped();
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startRemoteWork();
+  }
+
+  public abstract class RemoteListenableWorker extends androidx.work.ListenableWorker {
+    ctor public RemoteListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startRemoteWork();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+    field public static final String ARGUMENT_CLASS_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_CLASS_NAME";
+    field public static final String ARGUMENT_PACKAGE_NAME = "androidx.work.impl.workers.RemoteListenableWorker.ARGUMENT_PACKAGE_NAME";
+  }
+
+  public class RemoteWorkerService extends android.app.Service {
+    ctor public RemoteWorkerService();
+    method public android.os.IBinder? onBind(android.content.Intent);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/2.8.0-beta03.txt b/work/work-runtime-ktx/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime-ktx/api/res-2.8.0-beta03.txt b/work/work-runtime-ktx/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime-ktx/api/res-2.8.0-beta03.txt
diff --git a/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt b/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..efdea4c
--- /dev/null
+++ b/work/work-runtime-ktx/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,30 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class CoroutineWorker extends androidx.work.ListenableWorker {
+    ctor public CoroutineWorker(android.content.Context appContext, androidx.work.WorkerParameters params);
+    method public abstract suspend Object? doWork(kotlin.coroutines.Continuation<? super androidx.work.ListenableWorker.Result>);
+    method @Deprecated public kotlinx.coroutines.CoroutineDispatcher getCoroutineContext();
+    method public suspend Object? getForegroundInfo(kotlin.coroutines.Continuation<? super androidx.work.ForegroundInfo>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo> getForegroundInfoAsync();
+    method public final void onStopped();
+    method public final suspend Object? setForeground(androidx.work.ForegroundInfo foregroundInfo, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final suspend Object? setProgress(androidx.work.Data data, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result> startWork();
+    property @Deprecated public kotlinx.coroutines.CoroutineDispatcher coroutineContext;
+  }
+
+  public final class DataKt {
+    method public static inline <reified T> boolean hasKeyWithValueOfType(androidx.work.Data, String key);
+    method public static inline androidx.work.Data workDataOf(kotlin.Pair<java.lang.String,?>... pairs);
+  }
+
+  public final class ListenableFutureKt {
+  }
+
+  public final class OperationKt {
+    method public static suspend inline Object? await(androidx.work.Operation, kotlin.coroutines.Continuation<? super androidx.work.Operation.State.SUCCESS>);
+  }
+
+}
+
diff --git a/work/work-runtime/api/2.8.0-beta03.txt b/work/work-runtime/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-runtime/api/res-2.8.0-beta03.txt b/work/work-runtime/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-runtime/api/res-2.8.0-beta03.txt
diff --git a/work/work-runtime/api/restricted_2.8.0-beta03.txt b/work/work-runtime/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..446ee00
--- /dev/null
+++ b/work/work-runtime/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,491 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public final class ArrayCreatingInputMerger extends androidx.work.InputMerger {
+    ctor public ArrayCreatingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data> inputs);
+  }
+
+  public enum BackoffPolicy {
+    method public static androidx.work.BackoffPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.BackoffPolicy[] values();
+    enum_constant public static final androidx.work.BackoffPolicy EXPONENTIAL;
+    enum_constant public static final androidx.work.BackoffPolicy LINEAR;
+  }
+
+  public final class Configuration {
+    method public String? getDefaultProcessName();
+    method public java.util.concurrent.Executor getExecutor();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getInitializationExceptionHandler();
+    method public androidx.work.InputMergerFactory getInputMergerFactory();
+    method public int getMaxJobSchedulerId();
+    method public int getMinJobSchedulerId();
+    method public androidx.work.RunnableScheduler getRunnableScheduler();
+    method public androidx.core.util.Consumer<java.lang.Throwable!>? getSchedulingExceptionHandler();
+    method public java.util.concurrent.Executor getTaskExecutor();
+    method public androidx.work.WorkerFactory getWorkerFactory();
+    field public static final int MIN_SCHEDULER_LIMIT = 20; // 0x14
+  }
+
+  public static final class Configuration.Builder {
+    ctor public Configuration.Builder();
+    method public androidx.work.Configuration build();
+    method public androidx.work.Configuration.Builder setDefaultProcessName(String);
+    method public androidx.work.Configuration.Builder setExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setInitializationExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setInputMergerFactory(androidx.work.InputMergerFactory);
+    method public androidx.work.Configuration.Builder setJobSchedulerJobIdRange(int, int);
+    method public androidx.work.Configuration.Builder setMaxSchedulerLimit(int);
+    method public androidx.work.Configuration.Builder setMinimumLoggingLevel(int);
+    method public androidx.work.Configuration.Builder setRunnableScheduler(androidx.work.RunnableScheduler);
+    method public androidx.work.Configuration.Builder setSchedulingExceptionHandler(androidx.core.util.Consumer<java.lang.Throwable!>);
+    method public androidx.work.Configuration.Builder setTaskExecutor(java.util.concurrent.Executor);
+    method public androidx.work.Configuration.Builder setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public static interface Configuration.Provider {
+    method public androidx.work.Configuration getWorkManagerConfiguration();
+  }
+
+  public final class Constraints {
+    ctor public Constraints(optional @androidx.room.ColumnInfo(name="required_network_type") androidx.work.NetworkType requiredNetworkType, optional @androidx.room.ColumnInfo(name="requires_charging") boolean requiresCharging, optional @androidx.room.ColumnInfo(name="requires_device_idle") boolean requiresDeviceIdle, optional @androidx.room.ColumnInfo(name="requires_battery_not_low") boolean requiresBatteryNotLow, optional @androidx.room.ColumnInfo(name="requires_storage_not_low") boolean requiresStorageNotLow, optional @androidx.room.ColumnInfo(name="trigger_content_update_delay") long contentTriggerUpdateDelayMillis, optional @androidx.room.ColumnInfo(name="trigger_max_content_delay") long contentTriggerMaxDelayMillis, optional @androidx.room.ColumnInfo(name="content_uri_triggers") java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers);
+    ctor public Constraints(androidx.work.Constraints other);
+    method public long getContentTriggerMaxDelayMillis();
+    method public long getContentTriggerUpdateDelayMillis();
+    method public java.util.Set<androidx.work.Constraints.ContentUriTrigger> getContentUriTriggers();
+    method public androidx.work.NetworkType getRequiredNetworkType();
+    method public boolean requiresBatteryNotLow();
+    method public boolean requiresCharging();
+    method @RequiresApi(23) public boolean requiresDeviceIdle();
+    method public boolean requiresStorageNotLow();
+    property public final long contentTriggerMaxDelayMillis;
+    property public final long contentTriggerUpdateDelayMillis;
+    property public final java.util.Set<androidx.work.Constraints.ContentUriTrigger> contentUriTriggers;
+    property public final androidx.work.NetworkType requiredNetworkType;
+    field public static final androidx.work.Constraints.Companion Companion;
+    field public static final androidx.work.Constraints NONE;
+  }
+
+  public static final class Constraints.Builder {
+    ctor public Constraints.Builder();
+    method @RequiresApi(24) public androidx.work.Constraints.Builder addContentUriTrigger(android.net.Uri uri, boolean triggerForDescendants);
+    method public androidx.work.Constraints build();
+    method public androidx.work.Constraints.Builder setRequiredNetworkType(androidx.work.NetworkType networkType);
+    method public androidx.work.Constraints.Builder setRequiresBatteryNotLow(boolean requiresBatteryNotLow);
+    method public androidx.work.Constraints.Builder setRequiresCharging(boolean requiresCharging);
+    method @RequiresApi(23) public androidx.work.Constraints.Builder setRequiresDeviceIdle(boolean requiresDeviceIdle);
+    method public androidx.work.Constraints.Builder setRequiresStorageNotLow(boolean requiresStorageNotLow);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentMaxDelay(java.time.Duration duration);
+    method @RequiresApi(24) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public androidx.work.Constraints.Builder setTriggerContentUpdateDelay(java.time.Duration duration);
+  }
+
+  public static final class Constraints.Companion {
+  }
+
+  public static final class Constraints.ContentUriTrigger {
+    ctor public Constraints.ContentUriTrigger(android.net.Uri uri, boolean isTriggeredForDescendants);
+    method public android.net.Uri getUri();
+    method public boolean isTriggeredForDescendants();
+    property public final boolean isTriggeredForDescendants;
+    property public final android.net.Uri uri;
+  }
+
+  public final class Data {
+    ctor public Data(androidx.work.Data);
+    method @androidx.room.TypeConverter public static androidx.work.Data fromByteArray(byte[]);
+    method public boolean getBoolean(String, boolean);
+    method public boolean[]? getBooleanArray(String);
+    method public byte getByte(String, byte);
+    method public byte[]? getByteArray(String);
+    method public double getDouble(String, double);
+    method public double[]? getDoubleArray(String);
+    method public float getFloat(String, float);
+    method public float[]? getFloatArray(String);
+    method public int getInt(String, int);
+    method public int[]? getIntArray(String);
+    method public java.util.Map<java.lang.String!,java.lang.Object!> getKeyValueMap();
+    method public long getLong(String, long);
+    method public long[]? getLongArray(String);
+    method public String? getString(String);
+    method public String![]? getStringArray(String);
+    method public <T> boolean hasKeyWithValueOfType(String, Class<T!>);
+    method public byte[] toByteArray();
+    field public static final androidx.work.Data EMPTY;
+    field public static final int MAX_DATA_BYTES = 10240; // 0x2800
+  }
+
+  public static final class Data.Builder {
+    ctor public Data.Builder();
+    method public androidx.work.Data build();
+    method public androidx.work.Data.Builder putAll(androidx.work.Data);
+    method public androidx.work.Data.Builder putAll(java.util.Map<java.lang.String!,java.lang.Object!>);
+    method public androidx.work.Data.Builder putBoolean(String, boolean);
+    method public androidx.work.Data.Builder putBooleanArray(String, boolean[]);
+    method public androidx.work.Data.Builder putByte(String, byte);
+    method public androidx.work.Data.Builder putByteArray(String, byte[]);
+    method public androidx.work.Data.Builder putDouble(String, double);
+    method public androidx.work.Data.Builder putDoubleArray(String, double[]);
+    method public androidx.work.Data.Builder putFloat(String, float);
+    method public androidx.work.Data.Builder putFloatArray(String, float[]);
+    method public androidx.work.Data.Builder putInt(String, int);
+    method public androidx.work.Data.Builder putIntArray(String, int[]);
+    method public androidx.work.Data.Builder putLong(String, long);
+    method public androidx.work.Data.Builder putLongArray(String, long[]);
+    method public androidx.work.Data.Builder putString(String, String?);
+    method public androidx.work.Data.Builder putStringArray(String, String![]);
+  }
+
+  public class DelegatingWorkerFactory extends androidx.work.WorkerFactory {
+    ctor public DelegatingWorkerFactory();
+    method public final void addFactory(androidx.work.WorkerFactory);
+    method public final androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public enum ExistingPeriodicWorkPolicy {
+    method public static androidx.work.ExistingPeriodicWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingPeriodicWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy CANCEL_AND_REENQUEUE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy KEEP;
+    enum_constant @Deprecated public static final androidx.work.ExistingPeriodicWorkPolicy REPLACE;
+    enum_constant public static final androidx.work.ExistingPeriodicWorkPolicy UPDATE;
+  }
+
+  public enum ExistingWorkPolicy {
+    method public static androidx.work.ExistingWorkPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.ExistingWorkPolicy[] values();
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND;
+    enum_constant public static final androidx.work.ExistingWorkPolicy APPEND_OR_REPLACE;
+    enum_constant public static final androidx.work.ExistingWorkPolicy KEEP;
+    enum_constant public static final androidx.work.ExistingWorkPolicy REPLACE;
+  }
+
+  public final class ForegroundInfo {
+    ctor public ForegroundInfo(int, android.app.Notification);
+    ctor public ForegroundInfo(int, android.app.Notification, int);
+    method public int getForegroundServiceType();
+    method public android.app.Notification getNotification();
+    method public int getNotificationId();
+  }
+
+  public interface ForegroundUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(android.content.Context, java.util.UUID, androidx.work.ForegroundInfo);
+  }
+
+  public abstract class InputMerger {
+    ctor public InputMerger();
+    method public abstract androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public abstract class InputMergerFactory {
+    ctor public InputMergerFactory();
+    method public abstract androidx.work.InputMerger? createInputMerger(String);
+  }
+
+  public abstract class ListenableWorker {
+    ctor public ListenableWorker(android.content.Context, androidx.work.WorkerParameters);
+    method public final android.content.Context getApplicationContext();
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ForegroundInfo!> getForegroundInfoAsync();
+    method public final java.util.UUID getId();
+    method public final androidx.work.Data getInputData();
+    method @RequiresApi(28) public final android.net.Network? getNetwork();
+    method @IntRange(from=0) public final int getRunAttemptCount();
+    method public final java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public final java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public final java.util.List<android.net.Uri!> getTriggeredContentUris();
+    method public final boolean isStopped();
+    method public void onStopped();
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setForegroundAsync(androidx.work.ForegroundInfo);
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setProgressAsync(androidx.work.Data);
+    method @MainThread public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract static class ListenableWorker.Result {
+    method public static androidx.work.ListenableWorker.Result failure();
+    method public static androidx.work.ListenableWorker.Result failure(androidx.work.Data);
+    method public abstract androidx.work.Data getOutputData();
+    method public static androidx.work.ListenableWorker.Result retry();
+    method public static androidx.work.ListenableWorker.Result success();
+    method public static androidx.work.ListenableWorker.Result success(androidx.work.Data);
+  }
+
+  public enum NetworkType {
+    method public static androidx.work.NetworkType valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.NetworkType[] values();
+    enum_constant public static final androidx.work.NetworkType CONNECTED;
+    enum_constant public static final androidx.work.NetworkType METERED;
+    enum_constant public static final androidx.work.NetworkType NOT_REQUIRED;
+    enum_constant public static final androidx.work.NetworkType NOT_ROAMING;
+    enum_constant @RequiresApi(30) public static final androidx.work.NetworkType TEMPORARILY_UNMETERED;
+    enum_constant public static final androidx.work.NetworkType UNMETERED;
+  }
+
+  public final class OneTimeWorkRequest extends androidx.work.WorkRequest {
+    method public static androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public static java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+    field public static final androidx.work.OneTimeWorkRequest.Companion Companion;
+  }
+
+  public static final class OneTimeWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.OneTimeWorkRequest.Builder,androidx.work.OneTimeWorkRequest> {
+    ctor public OneTimeWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public androidx.work.OneTimeWorkRequest.Builder setInputMerger(Class<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public static final class OneTimeWorkRequest.Companion {
+    method public androidx.work.OneTimeWorkRequest from(Class<? extends androidx.work.ListenableWorker> workerClass);
+    method public java.util.List<androidx.work.OneTimeWorkRequest> from(java.util.List<? extends java.lang.Class<? extends androidx.work.ListenableWorker>> workerClasses);
+  }
+
+  public final class OneTimeWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.OneTimeWorkRequest.Builder! OneTimeWorkRequestBuilder();
+    method public static inline androidx.work.OneTimeWorkRequest.Builder setInputMerger(androidx.work.OneTimeWorkRequest.Builder, kotlin.reflect.KClass<? extends androidx.work.InputMerger> inputMerger);
+  }
+
+  public interface Operation {
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.Operation.State.SUCCESS!> getResult();
+    method public androidx.lifecycle.LiveData<androidx.work.Operation.State!> getState();
+  }
+
+  public abstract static class Operation.State {
+  }
+
+  public static final class Operation.State.FAILURE extends androidx.work.Operation.State {
+    ctor public Operation.State.FAILURE(Throwable);
+    method public Throwable getThrowable();
+  }
+
+  public static final class Operation.State.IN_PROGRESS extends androidx.work.Operation.State {
+  }
+
+  public static final class Operation.State.SUCCESS extends androidx.work.Operation.State {
+  }
+
+  public enum OutOfQuotaPolicy {
+    method public static androidx.work.OutOfQuotaPolicy valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.work.OutOfQuotaPolicy[] values();
+    enum_constant public static final androidx.work.OutOfQuotaPolicy DROP_WORK_REQUEST;
+    enum_constant public static final androidx.work.OutOfQuotaPolicy RUN_AS_NON_EXPEDITED_WORK_REQUEST;
+  }
+
+  public final class OverwritingInputMerger extends androidx.work.InputMerger {
+    ctor public OverwritingInputMerger();
+    method public androidx.work.Data merge(java.util.List<androidx.work.Data!>);
+  }
+
+  public final class PeriodicWorkRequest extends androidx.work.WorkRequest {
+    field public static final androidx.work.PeriodicWorkRequest.Companion Companion;
+    field public static final long MIN_PERIODIC_FLEX_MILLIS = 300000L; // 0x493e0L
+    field public static final long MIN_PERIODIC_INTERVAL_MILLIS = 900000L; // 0xdbba0L
+  }
+
+  public static final class PeriodicWorkRequest.Builder extends androidx.work.WorkRequest.Builder<androidx.work.PeriodicWorkRequest.Builder,androidx.work.PeriodicWorkRequest> {
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval);
+    ctor public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexInterval, java.util.concurrent.TimeUnit flexIntervalTimeUnit);
+    ctor @RequiresApi(26) public PeriodicWorkRequest.Builder(Class<? extends androidx.work.ListenableWorker> workerClass, java.time.Duration repeatInterval, java.time.Duration flexInterval);
+  }
+
+  public static final class PeriodicWorkRequest.Companion {
+  }
+
+  public final class PeriodicWorkRequestKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval);
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(long repeatInterval, java.util.concurrent.TimeUnit repeatIntervalTimeUnit, long flexTimeInterval, java.util.concurrent.TimeUnit flexTimeIntervalUnit);
+    method @RequiresApi(26) public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.PeriodicWorkRequest.Builder! PeriodicWorkRequestBuilder(java.time.Duration repeatInterval, java.time.Duration flexTimeInterval);
+  }
+
+  public interface ProgressUpdater {
+    method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> updateProgress(android.content.Context, java.util.UUID, androidx.work.Data);
+  }
+
+  public interface RunnableScheduler {
+    method public void cancel(Runnable);
+    method public void scheduleWithDelay(@IntRange(from=0) long, Runnable);
+  }
+
+  public abstract class WorkContinuation {
+    ctor public WorkContinuation();
+    method public static androidx.work.WorkContinuation combine(java.util.List<androidx.work.WorkContinuation!>);
+    method public abstract androidx.work.Operation enqueue();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos();
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData();
+    method public final androidx.work.WorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public final class WorkInfo {
+    method public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getOutputData();
+    method public androidx.work.Data getProgress();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public androidx.work.WorkInfo.State getState();
+    method public java.util.Set<java.lang.String!> getTags();
+  }
+
+  public enum WorkInfo.State {
+    method public boolean isFinished();
+    enum_constant public static final androidx.work.WorkInfo.State BLOCKED;
+    enum_constant public static final androidx.work.WorkInfo.State CANCELLED;
+    enum_constant public static final androidx.work.WorkInfo.State ENQUEUED;
+    enum_constant public static final androidx.work.WorkInfo.State FAILED;
+    enum_constant public static final androidx.work.WorkInfo.State RUNNING;
+    enum_constant public static final androidx.work.WorkInfo.State SUCCEEDED;
+  }
+
+  public abstract class WorkManager {
+    method public final androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.WorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.WorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Operation cancelAllWork();
+    method public abstract androidx.work.Operation cancelAllWorkByTag(String);
+    method public abstract androidx.work.Operation cancelUniqueWork(String);
+    method public abstract androidx.work.Operation cancelWorkById(java.util.UUID);
+    method public abstract android.app.PendingIntent createCancelPendingIntent(java.util.UUID);
+    method public final androidx.work.Operation enqueue(androidx.work.WorkRequest);
+    method public abstract androidx.work.Operation enqueue(java.util.List<? extends androidx.work.WorkRequest>);
+    method public abstract androidx.work.Operation enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.Operation enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract androidx.work.Configuration getConfiguration();
+    method @Deprecated public static androidx.work.WorkManager getInstance();
+    method public static androidx.work.WorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Long!> getLastCancelAllTimeMillis();
+    method public abstract androidx.lifecycle.LiveData<java.lang.Long!> getLastCancelAllTimeMillisLiveData();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkInfo!> getWorkInfoById(java.util.UUID);
+    method public abstract androidx.lifecycle.LiveData<androidx.work.WorkInfo!> getWorkInfoByIdLiveData(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTag(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosByTagLiveData(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWork(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosForUniqueWorkLiveData(String);
+    method public abstract androidx.lifecycle.LiveData<java.util.List<androidx.work.WorkInfo!>!> getWorkInfosLiveData(androidx.work.WorkQuery);
+    method public static void initialize(android.content.Context, androidx.work.Configuration);
+    method public static boolean isInitialized();
+    method public abstract androidx.work.Operation pruneWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<androidx.work.WorkManager.UpdateResult!> updateWork(androidx.work.WorkRequest);
+  }
+
+  public enum WorkManager.UpdateResult {
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_FOR_NEXT_RUN;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult APPLIED_IMMEDIATELY;
+    enum_constant public static final androidx.work.WorkManager.UpdateResult NOT_APPLIED;
+  }
+
+  public final class WorkManagerInitializer implements androidx.startup.Initializer<androidx.work.WorkManager> {
+    ctor public WorkManagerInitializer();
+    method public androidx.work.WorkManager create(android.content.Context);
+    method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>!> dependencies();
+  }
+
+  public final class WorkQuery {
+    method public static androidx.work.WorkQuery fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery fromIds(java.util.UUID!...);
+    method public static androidx.work.WorkQuery fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery fromStates(androidx.work.WorkInfo.State!...);
+    method public static androidx.work.WorkQuery fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery fromTags(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.lang.String!...);
+    method public static androidx.work.WorkQuery fromUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public java.util.List<java.util.UUID!> getIds();
+    method public java.util.List<androidx.work.WorkInfo.State!> getStates();
+    method public java.util.List<java.lang.String!> getTags();
+    method public java.util.List<java.lang.String!> getUniqueWorkNames();
+  }
+
+  public static final class WorkQuery.Builder {
+    method public androidx.work.WorkQuery.Builder addIds(java.util.List<java.util.UUID!>);
+    method public androidx.work.WorkQuery.Builder addStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public androidx.work.WorkQuery.Builder addTags(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery.Builder addUniqueWorkNames(java.util.List<java.lang.String!>);
+    method public androidx.work.WorkQuery build();
+    method public static androidx.work.WorkQuery.Builder fromIds(java.util.List<java.util.UUID!>);
+    method public static androidx.work.WorkQuery.Builder fromStates(java.util.List<androidx.work.WorkInfo.State!>);
+    method public static androidx.work.WorkQuery.Builder fromTags(java.util.List<java.lang.String!>);
+    method public static androidx.work.WorkQuery.Builder fromUniqueWorkNames(java.util.List<java.lang.String!>);
+  }
+
+  public abstract class WorkRequest {
+    method public java.util.UUID getId();
+    property public java.util.UUID id;
+    field public static final androidx.work.WorkRequest.Companion Companion;
+    field public static final long DEFAULT_BACKOFF_DELAY_MILLIS = 30000L; // 0x7530L
+    field public static final long MAX_BACKOFF_MILLIS = 18000000L; // 0x112a880L
+    field public static final long MIN_BACKOFF_MILLIS = 10000L; // 0x2710L
+  }
+
+  public abstract static class WorkRequest.Builder<B extends androidx.work.WorkRequest.Builder<B, ?>, W extends androidx.work.WorkRequest> {
+    method public final B addTag(String tag);
+    method public final W build();
+    method public final B keepResultsForAtLeast(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B keepResultsForAtLeast(java.time.Duration duration);
+    method public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, long backoffDelay, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public final B setBackoffCriteria(androidx.work.BackoffPolicy backoffPolicy, java.time.Duration duration);
+    method public final B setConstraints(androidx.work.Constraints constraints);
+    method public B setExpedited(androidx.work.OutOfQuotaPolicy policy);
+    method public final B setId(java.util.UUID id);
+    method public B setInitialDelay(long duration, java.util.concurrent.TimeUnit timeUnit);
+    method @RequiresApi(26) public B setInitialDelay(java.time.Duration duration);
+    method public final B setInputData(androidx.work.Data inputData);
+  }
+
+  public static final class WorkRequest.Companion {
+  }
+
+  public abstract class Worker extends androidx.work.ListenableWorker {
+    ctor public Worker(android.content.Context, androidx.work.WorkerParameters);
+    method @WorkerThread public abstract androidx.work.ListenableWorker.Result doWork();
+    method @WorkerThread public androidx.work.ForegroundInfo getForegroundInfo();
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+  public abstract class WorkerFactory {
+    ctor public WorkerFactory();
+    method public abstract androidx.work.ListenableWorker? createWorker(android.content.Context, String, androidx.work.WorkerParameters);
+  }
+
+  public final class WorkerParameters {
+    method @IntRange(from=0) public int getGeneration();
+    method public java.util.UUID getId();
+    method public androidx.work.Data getInputData();
+    method @RequiresApi(28) public android.net.Network? getNetwork();
+    method @IntRange(from=0) public int getRunAttemptCount();
+    method public java.util.Set<java.lang.String!> getTags();
+    method @RequiresApi(24) public java.util.List<java.lang.String!> getTriggeredContentAuthorities();
+    method @RequiresApi(24) public java.util.List<android.net.Uri!> getTriggeredContentUris();
+  }
+
+}
+
+package androidx.work.multiprocess {
+
+  public abstract class RemoteWorkContinuation {
+    method public static androidx.work.multiprocess.RemoteWorkContinuation combine(java.util.List<androidx.work.multiprocess.RemoteWorkContinuation!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue();
+    method public final androidx.work.multiprocess.RemoteWorkContinuation then(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation then(java.util.List<androidx.work.OneTimeWorkRequest!>);
+  }
+
+  public abstract class RemoteWorkManager {
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public final androidx.work.multiprocess.RemoteWorkContinuation beginWith(androidx.work.OneTimeWorkRequest);
+    method public abstract androidx.work.multiprocess.RemoteWorkContinuation beginWith(java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWork();
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelAllWorkByTag(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelUniqueWork(String);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelWorkById(java.util.UUID);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(androidx.work.WorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueue(java.util.List<androidx.work.WorkRequest!>);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniquePeriodicWork(String, androidx.work.ExistingPeriodicWorkPolicy, androidx.work.PeriodicWorkRequest);
+    method public final com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, androidx.work.OneTimeWorkRequest);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enqueueUniqueWork(String, androidx.work.ExistingWorkPolicy, java.util.List<androidx.work.OneTimeWorkRequest!>);
+    method public static androidx.work.multiprocess.RemoteWorkManager getInstance(android.content.Context);
+    method public abstract com.google.common.util.concurrent.ListenableFuture<java.util.List<androidx.work.WorkInfo!>!> getWorkInfos(androidx.work.WorkQuery);
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/2.8.0-beta03.txt b/work/work-rxjava2/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava2/api/res-2.8.0-beta03.txt b/work/work-rxjava2/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava2/api/res-2.8.0-beta03.txt
diff --git a/work/work-rxjava2/api/restricted_2.8.0-beta03.txt b/work/work-rxjava2/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..1cca40e
--- /dev/null
+++ b/work/work-rxjava2/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,16 @@
+// Signature format: 4.0
+package androidx.work {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.Scheduler getBackgroundScheduler();
+    method public io.reactivex.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.Completable setForeground(androidx.work.ForegroundInfo);
+    method @Deprecated public final io.reactivex.Single<java.lang.Void!> setProgress(androidx.work.Data);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/2.8.0-beta03.txt b/work/work-rxjava3/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-rxjava3/api/res-2.8.0-beta03.txt b/work/work-rxjava3/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-rxjava3/api/res-2.8.0-beta03.txt
diff --git a/work/work-rxjava3/api/restricted_2.8.0-beta03.txt b/work/work-rxjava3/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..0983052
--- /dev/null
+++ b/work/work-rxjava3/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.work.rxjava3 {
+
+  public abstract class RxWorker extends androidx.work.ListenableWorker {
+    ctor public RxWorker(android.content.Context, androidx.work.WorkerParameters);
+    method @MainThread public abstract io.reactivex.rxjava3.core.Single<androidx.work.ListenableWorker.Result!> createWork();
+    method protected io.reactivex.rxjava3.core.Scheduler getBackgroundScheduler();
+    method public io.reactivex.rxjava3.core.Single<androidx.work.ForegroundInfo!> getForegroundInfo();
+    method public final io.reactivex.rxjava3.core.Completable setCompletableProgress(androidx.work.Data);
+    method public final io.reactivex.rxjava3.core.Completable setForeground(androidx.work.ForegroundInfo);
+    method public final com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result!> startWork();
+  }
+
+}
+
diff --git a/work/work-testing/api/2.8.0-beta03.txt b/work/work-testing/api/2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt b/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/public_plus_experimental_2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+
diff --git a/work/work-testing/api/res-2.8.0-beta03.txt b/work/work-testing/api/res-2.8.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/work/work-testing/api/res-2.8.0-beta03.txt
diff --git a/work/work-testing/api/restricted_2.8.0-beta03.txt b/work/work-testing/api/restricted_2.8.0-beta03.txt
new file mode 100644
index 0000000..f3f3fe2
--- /dev/null
+++ b/work/work-testing/api/restricted_2.8.0-beta03.txt
@@ -0,0 +1,52 @@
+// Signature format: 4.0
+package androidx.work.testing {
+
+  public class SynchronousExecutor implements java.util.concurrent.Executor {
+    ctor public SynchronousExecutor();
+    method public void execute(Runnable);
+  }
+
+  public interface TestDriver {
+    method public void setAllConstraintsMet(java.util.UUID);
+    method public void setInitialDelayMet(java.util.UUID);
+    method public void setPeriodDelayMet(java.util.UUID);
+  }
+
+  public class TestListenableWorkerBuilder<W extends androidx.work.ListenableWorker> {
+    method public W build();
+    method public static androidx.work.testing.TestListenableWorkerBuilder<? extends androidx.work.ListenableWorker> from(android.content.Context, androidx.work.WorkRequest);
+    method public static <W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W!> from(android.content.Context, Class<W!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setForegroundUpdater(androidx.work.ForegroundUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setId(java.util.UUID);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setInputData(androidx.work.Data);
+    method @RequiresApi(28) public androidx.work.testing.TestListenableWorkerBuilder<W!> setNetwork(android.net.Network);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setProgressUpdater(androidx.work.ProgressUpdater);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setRunAttemptCount(int);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setTags(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentAuthorities(java.util.List<java.lang.String!>);
+    method @RequiresApi(24) public androidx.work.testing.TestListenableWorkerBuilder<W!> setTriggeredContentUris(java.util.List<android.net.Uri!>);
+    method public androidx.work.testing.TestListenableWorkerBuilder<W!> setWorkerFactory(androidx.work.WorkerFactory);
+  }
+
+  public final class TestListenableWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.ListenableWorker> androidx.work.testing.TestListenableWorkerBuilder<W>! TestListenableWorkerBuilder(android.content.Context context, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public class TestWorkerBuilder<W extends androidx.work.Worker> extends androidx.work.testing.TestListenableWorkerBuilder<W> {
+    method public static androidx.work.testing.TestWorkerBuilder<? extends androidx.work.Worker> from(android.content.Context, androidx.work.WorkRequest, java.util.concurrent.Executor);
+    method public static <W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W!> from(android.content.Context, Class<W!>, java.util.concurrent.Executor);
+  }
+
+  public final class TestWorkerBuilderKt {
+    method public static inline <reified W extends androidx.work.Worker> androidx.work.testing.TestWorkerBuilder<W>! TestWorkerBuilder(android.content.Context context, java.util.concurrent.Executor executor, optional androidx.work.Data inputData, optional java.util.List<? extends java.lang.String> tags, optional int runAttemptCount, optional java.util.List<? extends android.net.Uri> triggeredContentUris, optional java.util.List<? extends java.lang.String> triggeredContentAuthorities);
+  }
+
+  public final class WorkManagerTestInitHelper {
+    method @Deprecated public static androidx.work.testing.TestDriver? getTestDriver();
+    method public static androidx.work.testing.TestDriver? getTestDriver(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context);
+    method public static void initializeTestWorkManager(android.content.Context, androidx.work.Configuration);
+  }
+
+}
+