Merge "Fix ImageCapture NPE crash in 1.3.0 alpha" 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/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/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-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/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/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 1b8fa29..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
@@ -122,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-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/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
index cf9a3e3..0da5134 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/ProfileInstallBroadcastTest.kt
@@ -27,6 +27,7 @@
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class ProfileInstallBroadcastTest {
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
     @Test
     fun installProfile() {
         assertNull(ProfileInstallBroadcast.installProfile(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/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/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/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-core/src/main/java/androidx/camera/core/Preview.java b/camera/camera-core/src/main/java/androidx/camera/core/Preview.java
index f266b2d..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
@@ -323,7 +323,7 @@
                 /*hasEmbeddedTransform=*/true,
                 requireNonNull(getCropRect(resolution)),
                 getRelativeRotation(camera),
-                /*mirroring=*/true,
+                /*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}.
      *
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/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/processing/SurfaceOutputImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/SurfaceOutputImpl.java
index 04956c5..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
@@ -235,7 +235,7 @@
     @AnyThread
     @Override
     public void updateTransformMatrix(@NonNull float[] output, @NonNull float[] input) {
-        System.arraycopy(input, 0, output, 0, 16);
+        System.arraycopy(mGlTransform, 0, output, 0, 16);
     }
 
     /**
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 9920500..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
@@ -44,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
@@ -51,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
@@ -58,8 +61,6 @@
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
-import kotlin.jvm.Throws
-import org.junit.Assert
 
 private val TEST_CAMERA_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
 
@@ -72,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(
@@ -111,6 +119,9 @@
             this?.removeUseCases(useCases)
         }
         cameraUseCaseAdapter = null
+        if (::previewToDetach.isInitialized) {
+            previewToDetach.onDetached()
+        }
         CameraXUtil.shutdown().get()
     }
 
@@ -252,14 +263,22 @@
     }
 
     @Test
-    fun createPreviewWithProcessor_mirroringIsTrue() {
+    fun backCameraWithProcessor_notMirrored() {
         // Arrange.
         val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
-
         // Act: create pipeline
-        val preview = createPreview(processor)
+        val preview = createPreview(processor, backCamera)
+        // Assert
+        assertThat(preview.getCameraSurface().mirroring).isFalse()
+    }
 
-        // Assert: preview is mirrored by default.
+    @Test
+    fun frontCameraWithProcessor_mirrored() {
+        // Arrange.
+        val processor = FakeSurfaceProcessorInternal(mainThreadExecutor())
+        // Act: create pipeline
+        val preview = createPreview(processor, frontCamera)
+        // Assert
         assertThat(preview.getCameraSurface().mirroring).isTrue()
     }
 
@@ -281,9 +300,6 @@
         shadowOf(getMainLooper()).idle()
         // Assert: the rotation of the SettableFuture is updated based on ROTATION_90.
         assertThat(preview.getCameraSurface().rotationDegrees).isEqualTo(180)
-
-        // Clean up
-        preview.onDetached()
     }
 
     private fun Preview.getCameraSurface(): SettableSurface {
@@ -540,21 +556,24 @@
         return Pair(surfaceRequest!!, transformationInfo!!)
     }
 
-    private fun createPreview(surfaceProcessor: SurfaceProcessorInternal? = null): Preview {
-        val preview = Preview.Builder()
+    private fun createPreview(
+        surfaceProcessor: SurfaceProcessorInternal? = null,
+        camera: FakeCamera = backCamera
+    ): Preview {
+        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/processing/SurfaceOutputImplTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceOutputImplTest.kt
index 6f44748..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
@@ -100,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()
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
index d037ab4..db01315 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -47,6 +47,7 @@
 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
@@ -341,6 +342,7 @@
         file.delete()
     }
 
+    @FlakyTest(bugId = 259294631)
     @Test
     fun canRecordToFile_rightAfterPreviousRecordingStopped() {
         // Arrange.
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 b3c9695..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
@@ -133,7 +133,7 @@
     }
 
     @Test
-    fun enableEffect_effectIsEnabled() {
+    fun enableEffect_previewEffectIsEnabled() {
         // Arrange: launch app and verify effect is inactive.
         fragment.assertPreviewIsStreaming()
         val processor =
@@ -150,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 {
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 3881ff3..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
@@ -20,8 +20,8 @@
 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;
@@ -150,6 +150,7 @@
 
     @VisibleForTesting
     ToneMappingPreviewEffect mToneMappingPreviewEffect;
+    ToneMappingImageEffect mToneMappingImageEffect;
 
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
@@ -221,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();
@@ -372,7 +374,8 @@
 
     private void onEffectsToggled() {
         if (mEffectToggle.isChecked()) {
-            mCameraController.setEffects(singletonList(mToneMappingPreviewEffect));
+            mCameraController.setEffects(
+                    asList(mToneMappingPreviewEffect, mToneMappingImageEffect));
         } else {
             mCameraController.setEffects(emptyList());
         }
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/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-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/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/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index bda6e92..5320f47 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 {
@@ -371,8 +371,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);
   }
 
 }
@@ -1185,7 +1185,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/build.gradle b/compose/foundation/foundation/build.gradle
index 78d781e..dd28bf30 100644
--- a/compose/foundation/foundation/build.gradle
+++ b/compose/foundation/foundation/build.gradle
@@ -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/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/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/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/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/text/BasicTextMinLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
deleted file mode 100644
index c8a5fd5..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/BasicTextMinLinesTest.kt
+++ /dev/null
@@ -1,111 +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.compose.foundation.text
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.sp
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-@MediumTest
-@RunWith(Parameterized::class)
-class BasicTextMinLinesTest(private val useAnnotatedString: Boolean) {
-    @get:Rule
-    val rule = createComposeRule()
-
-    companion object {
-        @JvmStatic
-        @Parameterized.Parameters(name = "useAnnotatedString={0}")
-        fun parameters() = arrayOf(true, false)
-    }
-
-    private val density = Density(1f)
-    private val fontSize = 20
-
-    @Test
-    fun defaultMinLines_withEmptyText() {
-        displayText("", 1) { height ->
-            assertThat(height).isEqualTo(fontSize)
-        }
-    }
-
-    @Test
-    fun minLines_greater_thanEmptyText() {
-        displayText("", 5) { height ->
-            assertThat(height).isEqualTo(fontSize * 5)
-        }
-    }
-
-    @Test
-    fun minLines_smaller_thanTextLines() {
-        displayText("Line1\nLine2", 1) { height ->
-            assertThat(height).isEqualTo(fontSize * 2)
-        }
-    }
-
-    @Test
-    fun minLines_greater_thanTextLines() {
-        displayText("Line1\nLine2", 5) { height ->
-            assertThat(height).isEqualTo(fontSize * 5)
-        }
-    }
-
-    private fun displayText(text: String, minLines: Int, verify: (Int) -> Unit) {
-        var height by Delegates.notNull<Int>()
-        val modifier = Modifier.fillMaxWidth().onSizeChanged { height = it.height }
-        val style = TextStyle(
-            fontSize = fontSize.sp,
-            fontFamily = TEST_FONT_FAMILY,
-            lineHeight = fontSize.sp
-        )
-
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides density) {
-                if (useAnnotatedString) {
-                    BasicText(
-                        text = AnnotatedString(text),
-                        modifier = modifier,
-                        style = style,
-                        minLines = minLines
-                    )
-                } else {
-                    BasicText(
-                        text = text,
-                        modifier = modifier,
-                        style = style,
-                        minLines = minLines
-                    )
-                }
-            }
-        }
-
-        rule.runOnIdle { verify(height) }
-    }
-}
\ No newline at end of file
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/HeightInLinesModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
index 270cde8..c278e7b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/HeightInLinesModifierTest.kt
@@ -18,15 +18,15 @@
 
 import android.content.Context
 import android.graphics.Typeface
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.requiredWidth
 import androidx.compose.foundation.text.CoreTextField
 import androidx.compose.foundation.text.TEST_FONT
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
 import androidx.compose.foundation.text.heightInLines
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.InspectableValue
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalFontFamilyResolver
@@ -50,7 +50,8 @@
 import androidx.test.filters.MediumTest
 import androidx.test.platform.app.InstrumentationRegistry
 import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.UnconfinedTestDispatcher
@@ -63,8 +64,6 @@
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class HeightInLinesModifierTest {
-    private val fontSize = 20
-    private val defaultTextStyle = TextStyle(fontSize = 20.sp, fontFamily = TEST_FONT_FAMILY)
 
     private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
         "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
@@ -89,23 +88,66 @@
 
     @Test
     fun minLines_shortInputText() {
-        setTextFieldWithMinMaxLines(
-            TextFieldValue("abc"),
-            minLines = 5
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(1)
-            assertThat(height).isGreaterThan(fontSize * 5)
+        var subjectLayout: TextLayoutResult? = null
+        var subjectHeight: Int? = null
+        var twoLineHeight: Int? = null
+        val positionedLatch = CountDownLatch(1)
+        val twoLinePositionedLatch = CountDownLatch(1)
+
+        rule.setContent {
+            HeightObservingText(
+                onGlobalHeightPositioned = {
+                    subjectHeight = it
+                    positionedLatch.countDown()
+                },
+                onTextLayoutResult = {
+                    subjectLayout = it
+                },
+                textFieldValue = TextFieldValue("abc"),
+                minLines = 2
+            )
+            HeightObservingText(
+                onGlobalHeightPositioned = {
+                    twoLineHeight = it
+                    twoLinePositionedLatch.countDown()
+                },
+                onTextLayoutResult = {},
+                textFieldValue = TextFieldValue("1\n2"),
+                minLines = 2
+            )
+        }
+        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+        assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            assertThat(subjectLayout).isNotNull()
+            assertThat(subjectLayout!!.lineCount).isEqualTo(1)
+            assertThat(subjectHeight!!).isEqualTo(twoLineHeight)
         }
     }
 
     @Test
     fun maxLines_shortInputText() {
-        setTextFieldWithMinMaxLines(
+        val (textLayoutResult, height) = setTextFieldWithMaxLines(
             TextFieldValue("abc"),
             maxLines = 5
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(1)
-            assertThat(height).isGreaterThan(fontSize)
+        )
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+            assertThat(textLayoutResult.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test
+    fun maxLines_notApplied_infiniteMaxLines() {
+        val (textLayoutResult, height) =
+            setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.size.height).isEqualTo(height)
         }
     }
 
@@ -148,12 +190,16 @@
 
     @Test
     fun minLines_longInputText() {
-       setTextFieldWithMinMaxLines(
+        val (textLayoutResult, height) = setTextFieldWithMaxLines(
             TextFieldValue(longText),
             minLines = 2
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isGreaterThan(2)
-            assertThat(height).isGreaterThan(fontSize * 2)
+        )
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            // should be in the 20s, but use this to create invariant for the next assertion
+            assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+            assertThat(textLayoutResult.size.height).isEqualTo(height)
         }
     }
 
@@ -162,21 +208,33 @@
         var subjectLayout: TextLayoutResult? = null
         var subjectHeight: Int? = null
         var twoLineHeight: Int? = null
+        val positionedLatch = CountDownLatch(1)
+        val twoLinePositionedLatch = CountDownLatch(1)
 
         rule.setContent {
             HeightObservingText(
-                onHeightChanged = { subjectHeight = it },
-                onTextLayoutResult = { subjectLayout = it },
+                onGlobalHeightPositioned = {
+                    subjectHeight = it
+                    positionedLatch.countDown()
+                },
+                onTextLayoutResult = {
+                    subjectLayout = it
+                },
                 textFieldValue = TextFieldValue(longText),
                 maxLines = 2
             )
             HeightObservingText(
-                onHeightChanged = { twoLineHeight = it },
+                onGlobalHeightPositioned = {
+                    twoLineHeight = it
+                    twoLinePositionedLatch.countDown()
+                },
                 onTextLayoutResult = {},
                 textFieldValue = TextFieldValue("1\n2"),
                 maxLines = 2
             )
         }
+        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+        assertThat(twoLinePositionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
 
         rule.runOnIdle {
             assertThat(subjectLayout).isNotNull()
@@ -217,12 +275,13 @@
                 LocalDensity provides Density(1.0f, 1f)
             ) {
                 HeightObservingText(
-                    onHeightChanged = {
+                    onGlobalHeightPositioned = {
                         heights.add(it)
                     },
+                    onTextLayoutResult = {},
                     textFieldValue = TextFieldValue(longText),
                     maxLines = 10,
-                    textStyle = defaultTextStyle.copy(
+                    textStyle = TextStyle.Default.copy(
                         fontFamily = fontFamily,
                         fontSize = 80.sp
                     )
@@ -254,55 +313,61 @@
         )
     }
 
-    private fun setTextFieldWithMinMaxLines(
+    private fun setTextFieldWithMaxLines(
         textFieldValue: TextFieldValue,
         minLines: Int = 1,
-        maxLines: Int = Int.MAX_VALUE,
-        verify: (Int, TextLayoutResult) -> Unit
-    ) {
-        var height by Delegates.notNull<Int>()
-        lateinit var textLayoutResult: TextLayoutResult
+        maxLines: Int = Int.MAX_VALUE
+    ): Pair<TextLayoutResult?, Int?> {
+        var textLayoutResult: TextLayoutResult? = null
+        var height: Int? = null
+        val positionedLatch = CountDownLatch(1)
+
         rule.setContent {
             HeightObservingText(
-                onHeightChanged = { height = it },
+                onGlobalHeightPositioned = {
+                    height = it
+                    positionedLatch.countDown()
+                },
+                onTextLayoutResult = {
+                    textLayoutResult = it
+                },
                 textFieldValue = textFieldValue,
-                onTextLayoutResult = { textLayoutResult = it },
                 minLines = minLines,
                 maxLines = maxLines
             )
         }
+        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
 
-        rule.runOnIdle {
-            verify(height, textLayoutResult)
-        }
+        return Pair(textLayoutResult, height)
     }
 
     @Composable
     private fun HeightObservingText(
-        onHeightChanged: (Int) -> Unit,
+        onGlobalHeightPositioned: (Int) -> Unit,
+        onTextLayoutResult: (TextLayoutResult) -> Unit,
         textFieldValue: TextFieldValue,
-        onTextLayoutResult: (TextLayoutResult) -> Unit = {},
         minLines: Int = 1,
         maxLines: Int = Int.MAX_VALUE,
-        textStyle: TextStyle = defaultTextStyle
+        textStyle: TextStyle = TextStyle.Default
     ) {
-        CoreTextField(
-            value = textFieldValue,
-            onValueChange = {},
-            textStyle = textStyle,
-            onTextLayout = onTextLayoutResult,
-            modifier = Modifier
-                .onSizeChanged {
-                    onHeightChanged(it.height)
-                }
-                .requiredWidth(100.dp)
-                // we test modifier here so propagating min and max lines here instead of passing
-                // them to the CoreTextField directly
-                .heightInLines(
-                    textStyle = textStyle,
-                    minLines = minLines,
-                    maxLines = maxLines
-                )
-        )
+        Box(
+            Modifier.onGloballyPositioned {
+                onGlobalHeightPositioned(it.size.height)
+            }
+        ) {
+            CoreTextField(
+                value = textFieldValue,
+                onValueChange = {},
+                textStyle = textStyle,
+                modifier = Modifier
+                    .requiredWidth(100.dp)
+                    .heightInLines(
+                        textStyle = textStyle,
+                        minLines = minLines,
+                        maxLines = maxLines
+                    ),
+                onTextLayout = onTextLayoutResult
+            )
+        }
     }
 }
\ No newline at end of file
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/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
deleted file mode 100644
index 9fc479c..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldMinMaxLinesTest.kt
+++ /dev/null
@@ -1,124 +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.compose.foundation.textfield
-
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.text.CoreTextField
-import androidx.compose.foundation.text.TEST_FONT_FAMILY
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
-import kotlin.properties.Delegates
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@MediumTest
-@RunWith(AndroidJUnit4::class)
-class TextFieldMinMaxLinesTest {
-    private val fontSize = 20
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test
-    fun minLines_smaller_thanInput() {
-        displayTextField(
-            text = "abc\nabc\nabc",
-            minLines = 1
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(3)
-            assertThat(height).isEqualTo(fontSize * 3)
-        }
-    }
-
-    @Test
-    fun minLines_greater_thanInput() {
-        displayTextField(
-            text = "abc",
-            minLines = 3
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(1)
-            assertThat(height).isEqualTo(fontSize * 3)
-        }
-    }
-
-    @Test
-    fun maxLines_smaller_thanInput() {
-        displayTextField(
-            text = "abc\nabc\nabc",
-            maxLines = 1
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(3)
-            assertThat(height).isEqualTo(fontSize)
-        }
-    }
-
-    @Test
-    fun maxLines_greater_thanInput() {
-        displayTextField(
-            text = "abc",
-            maxLines = 3
-        ) { height, textLayoutResult ->
-            assertThat(textLayoutResult.lineCount).isEqualTo(1)
-            assertThat(height).isEqualTo(fontSize)
-        }
-    }
-
-    private fun displayTextField(
-        text: String,
-        minLines: Int = 1,
-        maxLines: Int = Int.MAX_VALUE,
-        verify: (Int, TextLayoutResult) -> Unit
-    ) {
-        var height by Delegates.notNull<Int>()
-        lateinit var textLayoutResult: TextLayoutResult
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides Density(1f)) {
-                CoreTextField(
-                    value = TextFieldValue(text),
-                    onValueChange = {},
-                    onTextLayout = { textLayoutResult = it },
-                    modifier = Modifier
-                        .onSizeChanged { height = it.height }
-                        .fillMaxWidth(),
-                    minLines = minLines,
-                    maxLines = maxLines,
-                    textStyle = TextStyle(
-                        fontSize = fontSize.sp,
-                        fontFamily = TEST_FONT_FAMILY,
-                        lineHeight = fontSize.sp
-                    )
-                )
-            }
-        }
-
-        rule.runOnIdle {
-            verify(height, textLayoutResult)
-        }
-    }
-}
\ No newline at end of file
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/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/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/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/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index b8ded54..d499986 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -220,7 +220,6 @@
                     softWrap = softWrap,
                     fontFamilyResolver = fontFamilyResolver,
                     overflow = overflow,
-                    minLines = minLines,
                     maxLines = maxLines,
                     placeholders = placeholders
                 ),
@@ -239,7 +238,6 @@
                 softWrap = softWrap,
                 fontFamilyResolver = fontFamilyResolver,
                 overflow = overflow,
-                minLines = minLines,
                 maxLines = maxLines,
                 placeholders = placeholders,
             )
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/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/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/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-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
index 4ace0ae..463830d 100644
--- a/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
+++ b/compose/runtime/runtime-lint/src/test/java/androidx/compose/runtime/lint/ComposableLambdaParameterDetectorTest.kt
@@ -59,7 +59,6 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -97,7 +96,6 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -127,7 +125,6 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -169,7 +166,6 @@
             ),
             Stubs.Composable
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
@@ -235,7 +231,6 @@
             Stubs.Composable
         )
             .skipTestModes(TestMode.TYPE_ALIAS)
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293458
             .run()
             .expect(
                 """
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index e75d073..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 {
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/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-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
index 0c5b55e..f3aba8d 100644
--- a/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
+++ b/compose/ui/ui-lint/src/test/java/androidx/compose/ui/lint/ModifierParameterDetectorTest.kt
@@ -20,7 +20,6 @@
 
 import androidx.compose.lint.test.Stubs
 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
@@ -63,7 +62,6 @@
             Stubs.Composable,
             Stubs.Modifier
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -105,7 +103,6 @@
             Stubs.Composable,
             Stubs.Modifier
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -149,7 +146,6 @@
             Stubs.Composable,
             Stubs.Modifier
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -191,7 +187,6 @@
             Stubs.Composable,
             Stubs.Modifier
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
@@ -227,7 +222,6 @@
             Stubs.Composable,
             Stubs.Modifier
         )
-            .skipTestModes(TestMode.SUPPRESSIBLE) // b/257293766
             .run()
             .expect(
                 """
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/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-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/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 bd274e6..4944ff2 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -72,7 +72,7 @@
         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")
@@ -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,7 +167,7 @@
                 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")
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/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/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/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/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 e4f5e82..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
@@ -38,7 +38,7 @@
  * The input data is provided by calling [addPosition]. Adding data is cheap.
  *
  * 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,
+ * 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
@@ -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/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/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/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/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/OnBackPressedCallbackTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
index e9428d1..abe5b23 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/OnBackPressedCallbackTest.kt
@@ -44,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
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/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/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/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
new file mode 100644
index 0000000..3b3a016
--- /dev/null
+++ b/health/health-services-client/src/main/aidl/androidx/health/services/client/impl/request/UpdateExerciseTypeConfigRequest.aidl
@@ -0,0 +1,20 @@
+/*
+ * 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;
+
+/** @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/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/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/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/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
index 03390ee..17d262b 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/xpoet_ext.kt
@@ -28,11 +28,8 @@
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.asClassName
 import androidx.room.compiler.codegen.asMutableClassName
-import androidx.room.compiler.codegen.toJavaPoet
-import androidx.room.ext.CommonTypeNames.STRING
 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
@@ -104,8 +101,7 @@
     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")
 }
 
@@ -143,19 +139,18 @@
 object CollectionTypeNames {
     val ARRAY_MAP = XClassName.get(COLLECTION_PACKAGE, "ArrayMap")
     val LONG_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "LongSparseArray")
-    val INT_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
-}
-
-object KotlinCollectionTypeNames {
-    val MUTABLE_LIST = List::class.asMutableClassName()
+    val INT_SPARSE_ARRAY = XClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
 }
 
 object CommonTypeNames {
     val LIST = List::class.asClassName()
+    val MUTABLE_LIST = List::class.asMutableClassName()
     val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
     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")
@@ -261,6 +256,10 @@
     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 =
@@ -291,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"
@@ -368,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 == STRING.toJavaPoet()) 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.
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 4f18b5d..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
@@ -73,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
@@ -601,21 +603,24 @@
         } else if (typeMirror.isTypeOf(java.util.Map::class) ||
             typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet() ||
             typeMirror.rawType.typeName == LONG_SPARSE_ARRAY.toJavaPoet() ||
-            typeMirror.rawType.typeName == INT_SPARSE_ARRAY
+            typeMirror.rawType.typeName == INT_SPARSE_ARRAY.toJavaPoet()
         ) {
-            val keyTypeArg = when (typeMirror.rawType.typeName) {
-                LONG_SPARSE_ARRAY.toJavaPoet() -> context.processingEnv.requireType(TypeName.LONG)
-                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 = when (typeMirror.rawType.typeName) {
-                LONG_SPARSE_ARRAY.toJavaPoet() -> LONG_SPARSE_ARRAY.toJavaPoet()
-                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()
@@ -636,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.toJavaPoet(),
-                        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,
@@ -706,8 +716,7 @@
                     keyRowAdapter = checkTypeOrNull(keyRowAdapter) ?: return null,
                     valueRowAdapter = checkTypeOrNull(valueRowAdapter) ?: return null,
                     valueCollectionType = null,
-                    isArrayMap = typeMirror.rawType.typeName == ARRAY_MAP.toJavaPoet(),
-                    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 9da6a49..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,17 +17,14 @@
 package androidx.room.solver.query.result
 
 import androidx.room.AmbiguousColumnResolver
-import androidx.room.compiler.codegen.toJavaPoet
+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
@@ -47,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
@@ -57,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(
-                    type = CommonTypeNames.STRING.toJavaPoet(),
+                    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 3f978e7..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 }",
-                CommonTypeNames.STRING.toJavaPoet(),
-                CodeBlock.join(entity.columnNames.map { CodeBlock.of(S, it) }, ",$W")
+            val entityColumnNamesParam = ArrayLiteral(
+                scope.language,
+                CommonTypeNames.STRING,
+                *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 9b05f4a..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,16 +16,16 @@
 
 package androidx.room.solver.query.result
 
-import androidx.room.compiler.codegen.toJavaPoet
+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,
@@ -34,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.toJavaPoet() 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.toJavaPoet() 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
@@ -109,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
                 )
@@ -129,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
@@ -154,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/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/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/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/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/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/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/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/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/DaoKotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
index 8c1d50c..3525dfcd 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/DaoKotlinCodeGenTest.kt
@@ -61,7 +61,9 @@
                 @PrimaryKey
                 var pk: Int
             ) {
-                var variable: Long = 0
+                var variablePrimitive: Long = 0
+                var variableString: String = ""
+                var variableNullableString: String? = null
             }
             """.trimIndent()
         )
@@ -1164,4 +1166,152 @@
             expectedFilePath = getTestGoldenPath(testName)
         )
     }
+
+    @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
+            }
+
+            @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/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/pojoRowAdapter_variableProperty.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/pojoRowAdapter_variableProperty.kt
index b34fd6a..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
@@ -27,11 +27,18 @@
         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)
+                }
             }
         }
     }
@@ -54,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/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-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/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/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 6a1dc5d..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"));
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 94ef7cf..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
@@ -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 ba9e851..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()) {
@@ -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 23add70..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));
 
@@ -204,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);
@@ -254,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 5ddfce9..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();
         }
     };
 
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 e330872..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));
         }
@@ -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 451f493..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;
     }
 
     /**
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 e652c7e..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));
     }
 
@@ -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;
     }
 
     /**
@@ -1071,4 +1022,11 @@
         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/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/public_plus_experimental_current.txt b/tv/tv-foundation/api/public_plus_experimental_current.txt
index ebce6ac..fe3724e 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 {
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/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/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/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/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 1180309..4970d54 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 {
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/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-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 34de840..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.
      */
@@ -1317,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
@@ -1709,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)
         }
 
@@ -1737,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)
@@ -1961,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.
@@ -1999,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),
@@ -2008,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(),
@@ -2035,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,
@@ -2048,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,
@@ -2065,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,
@@ -2073,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,
@@ -2081,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,
@@ -2101,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,
@@ -2121,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,
@@ -2139,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,
@@ -2155,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,
@@ -2169,7 +2154,7 @@
                 FIELD_CONTENT_DESCRIPTION,
                 FIELD_DATA_SOURCE,
                 FIELD_PERSISTENCE_POLICY,
-                FIELD_DISPLAY_POLICY
+                FIELD_DISPLAY_POLICY,
             ),
         )
 
@@ -2181,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")
             }
         }
 
@@ -2234,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/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/WatchFaceService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 9a10e9e3..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
@@ -1225,7 +1225,7 @@
                  * there's no point drawing.
                  */
                 if (watchFaceImpl?.renderer?.shouldAnimate() != false) {
-                    draw()
+                    draw(watchFaceImpl)
                 }
             }
         }
@@ -1415,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 {
@@ -2454,7 +2455,7 @@
             }
         }
 
-        internal fun draw() {
+        internal fun draw(watchFaceImpl: WatchFaceImpl?) {
             try {
                 if (TRACE_DRAW) {
                     Trace.beginSection("onDraw")
@@ -2462,8 +2463,6 @@
                 if (LOG_VERBOSE) {
                     Log.v(TAG, "drawing frame")
                 }
-
-                val watchFaceImpl: WatchFaceImpl? = getWatchFaceImplOrNull()
                 watchFaceImpl?.onDraw()
             } finally {
                 if (TRACE_DRAW) {