Merge changes Ibcbdc9c4,Id3ce3d36 into androidx-main

* changes:
  Code cleanup for Camera2ExtensionsTestUtil with CameraIdExtensionModePair for type safety
  Extend ExtensionsDisabledQuirk to all Motoroal Devices with Extensions v1.1.0 and older
diff --git a/annotation/annotation/build.gradle b/annotation/annotation/build.gradle
index 7f3042f..00c5298 100644
--- a/annotation/annotation/build.gradle
+++ b/annotation/annotation/build.gradle
@@ -70,8 +70,7 @@
 
 androidx {
     name = "Android Support Library Annotations"
-    type = LibraryType.KMP_LIBRARY
-    publish = Publish.SNAPSHOT_AND_RELEASE
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenVersion = LibraryVersions.ANNOTATION
     mavenGroup = LibraryGroups.ANNOTATION
     inceptionYear = "2013"
diff --git a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
index 7d3ae75..daba672 100644
--- a/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
+++ b/appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java
@@ -3931,6 +3931,7 @@
             }
         }
 
+        @DoNotInline
         static Context createConfigurationContext(@NonNull Context context,
                 @NonNull Configuration overrideConfiguration) {
             return context.createConfigurationContext(overrideConfiguration);
@@ -3961,6 +3962,7 @@
     static class Api21Impl {
         private Api21Impl() { }
 
+        @DoNotInline
         static boolean isPowerSaveMode(PowerManager powerManager) {
             return powerManager.isPowerSaveMode();
         }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
index e3f8b92..df8c392 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSuggestionSpecToProtoConverter.java
@@ -21,6 +21,7 @@
 import androidx.appsearch.app.SearchSuggestionSpec;
 import androidx.core.util.Preconditions;
 
+import com.google.android.icing.proto.SuggestionScoringSpecProto;
 import com.google.android.icing.proto.SuggestionSpecProto;
 import com.google.android.icing.proto.TermMatchType;
 
@@ -90,10 +91,26 @@
                 .setNumToReturn(mSearchSuggestionSpec.getMaximumResultCount());
 
         // TODO(b/227356108) expose setTermMatch in SearchSuggestionSpec.
-        protoBuilder.setScoringSpec(SuggestionSpecProto.SuggestionScoringSpecProto.newBuilder()
+        protoBuilder.setScoringSpec(SuggestionScoringSpecProto.newBuilder()
                 .setScoringMatchType(TermMatchType.Code.EXACT_ONLY)
+                .setRankBy(toProtoRankingStrategy(mSearchSuggestionSpec.getRankingStrategy()))
                 .build());
 
         return protoBuilder.build();
     }
+
+    private static SuggestionScoringSpecProto.SuggestionRankingStrategy.Code toProtoRankingStrategy(
+            @SearchSuggestionSpec.SuggestionRankingStrategy int rankingStrategyCode) {
+        switch (rankingStrategyCode) {
+            case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_NONE:
+                return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.NONE;
+            case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT:
+                return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.DOCUMENT_COUNT;
+            case SearchSuggestionSpec.SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY:
+                return SuggestionScoringSpecProto.SuggestionRankingStrategy.Code.TERM_FREQUENCY;
+            default:
+                throw new IllegalArgumentException("Invalid suggestion ranking strategy: "
+                        + rankingStrategyCode);
+        }
+    }
 }
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index d4beb42..8580d6f 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -472,6 +472,8 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index d4beb42..8580d6f 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -472,6 +472,8 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index d4beb42..8580d6f 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -472,6 +472,8 @@
     method public androidx.appsearch.app.SearchSpec.Builder addFilterSchemas(java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, java.util.Collection<java.lang.String!>);
     method public androidx.appsearch.app.SearchSpec.Builder addProjectionPaths(String, java.util.Collection<androidx.appsearch.app.PropertyPath!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionPathsForDocumentClass(Class<?>, java.util.Collection<androidx.appsearch.app.PropertyPath!>) throws androidx.appsearch.exceptions.AppSearchException;
+    method public androidx.appsearch.app.SearchSpec.Builder addProjectionsForDocumentClass(Class<?>, java.util.Collection<java.lang.String!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec build();
     method public androidx.appsearch.app.SearchSpec.Builder setMaxSnippetSize(@IntRange(from=0, to=0x2710) int);
     method public androidx.appsearch.app.SearchSpec.Builder setOrder(int);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
index 38fc851..b3cc00b 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionInternalTestBase.java
@@ -239,6 +239,72 @@
     }
 
     @Test
+    public void testSearchSuggestion_differentRankingStrategy() throws Exception {
+        // Schema registration
+        AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
+                        new StringPropertyConfig.Builder("body")
+                                .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                                .setTokenizerType(StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
+                                .setIndexingType(StringPropertyConfig.INDEXING_TYPE_PREFIXES)
+                                .build())
+                .build();
+        mDb1.setSchemaAsync(new SetSchemaRequest.Builder().addSchemas(schema).build()).get();
+
+        // Index documents
+        // term1 appears 3 times in all 3 docs.
+        // term2 appears 4 times in 2 docs.
+        // term3 appears 5 times in 1 doc.
+        GenericDocument doc1 = new GenericDocument.Builder<>("namespace", "id1", "Type")
+                .setPropertyString("body", "term1 term3 term3 term3 term3 term3")
+                .build();
+        GenericDocument doc2 = new GenericDocument.Builder<>("namespace", "id2", "Type")
+                .setPropertyString("body", "term1 term2 term2 term2")
+                .build();
+        GenericDocument doc3 = new GenericDocument.Builder<>("namespace", "id3", "Type")
+                .setPropertyString("body", "term1 term2")
+                .build();
+
+        checkIsBatchResultSuccess(mDb1.putAsync(
+                new PutDocumentsRequest.Builder().addGenericDocuments(doc1, doc2, doc3)
+                        .build()));
+
+        SearchSuggestionResult result1 =
+                new SearchSuggestionResult.Builder().setSuggestedResult("term1").build();
+        SearchSuggestionResult result2 =
+                new SearchSuggestionResult.Builder().setSuggestedResult("term2").build();
+        SearchSuggestionResult result3 =
+                new SearchSuggestionResult.Builder().setSuggestedResult("term3").build();
+
+
+        // rank by NONE, the order should be arbitrary but all terms appear.
+        List<SearchSuggestionResult> suggestions = mDb1.searchSuggestionAsync(
+                /*suggestionQueryExpression=*/"t",
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                        .setRankingStrategy(SearchSuggestionSpec
+                                .SUGGESTION_RANKING_STRATEGY_NONE)
+                        .build()).get();
+        assertThat(suggestions).containsExactly(result2, result1, result3);
+
+        // rank by document count, the order should be term1:3 > term2:2 > term3:1
+        suggestions = mDb1.searchSuggestionAsync(
+                /*suggestionQueryExpression=*/"t",
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                        .setRankingStrategy(SearchSuggestionSpec
+                                .SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT)
+                        .build()).get();
+        assertThat(suggestions).containsExactly(result1, result2, result3).inOrder();
+
+        // rank by term frequency, the order should be term3:5 > term2:4 > term1:3
+        suggestions = mDb1.searchSuggestionAsync(
+                /*suggestionQueryExpression=*/"t",
+                new SearchSuggestionSpec.Builder(/*totalResultCount=*/10)
+                        .setRankingStrategy(SearchSuggestionSpec
+                                .SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY)
+                        .build()).get();
+        assertThat(suggestions).containsExactly(result3, result2, result1).inOrder();
+    }
+
+    @Test
     public void testSearchSuggestion_removeDocument() throws Exception {
         // Schema registration
         AppSearchSchema schema = new AppSearchSchema.Builder("Type").addProperty(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
index 6443c3d..ddb7093 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AppSearchSessionPlatformInternalTest.java
@@ -68,6 +68,12 @@
 
     @Override
     @Test
+    public void testSearchSuggestion_differentRankingStrategy() throws Exception {
+        // TODO(b/227356108) enable the test when suggestion is ready in platform.
+    }
+
+    @Override
+    @Test
     public void testSearchSuggestion_removeDocument() throws Exception {
         // TODO(b/227356108) enable the test when suggestion is ready in platform.
     }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
index c62ba4f..2816781 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/cts/app/SearchSpecCtsTest.java
@@ -22,6 +22,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import androidx.appsearch.annotation.Document;
+import androidx.appsearch.app.PropertyPath;
 import androidx.appsearch.app.SearchSpec;
 
 import com.google.common.collect.ImmutableList;
@@ -130,5 +131,26 @@
 
         assertThat(searchSpec.getFilterSchemas()).containsExactly("King");
     }
+
+    @Test
+    public void testProjectionsForDocumentClass() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addProjectionPathsForDocumentClass(King.class, ImmutableList.of(
+                        new PropertyPath("field1"), new PropertyPath("field2.subfield2")))
+                .build();
+
+        assertThat(searchSpec.getProjections().get("King"))
+                .containsExactly("field1", "field2.subfield2");
+
+        searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .addProjectionsForDocumentClass(King.class,
+                        ImmutableList.of("field3", "field4.subfield3"))
+                .build();
+
+        assertThat(searchSpec.getProjections().get("King"))
+                .containsExactly("field3", "field4.subfield3");
+    }
 // @exportToFramework:endStrip()
 }
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 d149fe5..07e709a 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -712,6 +712,54 @@
             return addProjection(schema, propertyPathsArrayList);
         }
 
+// @exportToFramework:startStrip()
+        /**
+         * Adds property paths for the Document class to be used for projection. If property
+         * paths are added for a document class, then only the properties referred to will be
+         * retrieved for results of that type. If a property path that is specified isn't present
+         * in a result, it will be ignored for that result. Property paths cannot be null.
+         *
+         * @see #addProjection
+         *
+         * @param documentClass a class, annotated with @Document, corresponding to the schema to
+         *                      add projections to.
+         * @param propertyPaths the projections to add.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
+        @NonNull
+        public SearchSpec.Builder addProjectionsForDocumentClass(
+                @NonNull Class<?> documentClass, @NonNull Collection<String> propertyPaths)
+                throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            DocumentClassFactoryRegistry registry = DocumentClassFactoryRegistry.getInstance();
+            DocumentClassFactory<?> factory = registry.getOrCreateFactory(documentClass);
+            return addProjection(factory.getSchemaName(), propertyPaths);
+        }
+
+        /**
+         * Adds property paths for the specified Document class to be used for projection.
+         * @see #addProjectionPaths
+         *
+         * @param documentClass a class, annotated with @Document, corresponding to the schema to
+         *                      add projections to.
+         * @param propertyPaths the projections to add.
+         */
+        @SuppressLint("MissingGetterMatchingBuilder")  // Projections available from getProjections
+        @NonNull
+        public SearchSpec.Builder addProjectionPathsForDocumentClass(
+                @NonNull Class<?> documentClass, @NonNull Collection<PropertyPath> propertyPaths)
+                throws AppSearchException {
+            Preconditions.checkNotNull(documentClass);
+            resetIfBuilt();
+            ArrayList<String> propertyPathsArrayList = new ArrayList<>(propertyPaths.size());
+            for (PropertyPath propertyPath : propertyPaths) {
+                propertyPathsArrayList.add(propertyPath.toString());
+            }
+            return addProjectionsForDocumentClass(documentClass, propertyPathsArrayList);
+        }
+// @exportToFramework:endStrip()
+
         /**
          * Set the maximum number of results to return for each group, where groups are defined
          * by grouping type.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
index 865cd89..d18f9cf 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSuggestionSpec.java
@@ -17,11 +17,14 @@
 package androidx.appsearch.app;
 import android.os.Bundle;
 
+import androidx.annotation.IntDef;
 import androidx.annotation.IntRange;
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.core.util.Preconditions;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -39,7 +42,7 @@
 public class SearchSuggestionSpec {
     static final String NAMESPACE_FIELD = "namespace";
     static final String MAXIMUM_RESULT_COUNT_FIELD = "maximumResultCount";
-
+    static final String RANKING_STRATEGY_FIELD = "rankingStrategy";
     private final Bundle mBundle;
     private final int mMaximumResultCount;
 
@@ -54,6 +57,49 @@
     }
 
     /**
+     * Ranking Strategy for {@link SearchSuggestionResult}.
+     *
+     * @hide
+     */
+    @IntDef(value = {
+            SUGGESTION_RANKING_STRATEGY_NONE,
+            SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT,
+            SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY,
+    })
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SuggestionRankingStrategy {
+    }
+
+    /**
+     * Ranked by the document count that contains the term.
+     *
+     * <p>Suppose the following document is in the index.
+     * <pre>Doc1 contains: term1 term2 term2 term2</pre>
+     * <pre>Doc2 contains: term1</pre>
+     *
+     * <p>Then, suppose that a search suggestion for "t" is issued with the DOCUMENT_COUNT, the
+     * returned {@link SearchSuggestionResult}s will be: term1, term2. The term1 will have higher
+     * score and appear in the results first.
+     */
+    public static final int SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT = 0;
+    /**
+     * Ranked by the term appear frequency.
+     *
+     * <p>Suppose the following document is in the index.
+     * <pre>Doc1 contains: term1 term2 term2 term2</pre>
+     * <pre>Doc2 contains: term1</pre>
+     *
+     * <p>Then, suppose that a search suggestion for "t" is issued with the TERM_FREQUENCY,
+     * the returned {@link SearchSuggestionResult}s will be: term2, term1. The term2 will have
+     * higher score and appear in the results first.
+     */
+    public static final int SUGGESTION_RANKING_STRATEGY_TERM_FREQUENCY = 1;
+
+    /** No Ranking, results are returned in arbitrary order. */
+    public static final int SUGGESTION_RANKING_STRATEGY_NONE = 2;
+
+    /**
      * Returns the {@link Bundle} populated by this builder.
      *
      * @hide
@@ -85,10 +131,18 @@
         return Collections.unmodifiableList(namespaces);
     }
 
+    /** Returns the ranking strategy. */
+    public @SuggestionRankingStrategy int getRankingStrategy() {
+        return mBundle.getInt(RANKING_STRATEGY_FIELD);
+    }
+
+
     /** Builder for {@link SearchSuggestionSpec objects}. */
     public static final class Builder {
         private ArrayList<String> mNamespaces = new ArrayList<>();
         private final int mTotalResultCount;
+        private @SuggestionRankingStrategy int mRankingStrategy =
+                SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT;
         private boolean mBuilt = false;
 
         /**
@@ -129,12 +183,29 @@
             return this;
         }
 
+        /**
+         * Sets ranking strategy for suggestion results.
+         *
+         * <p>The default value {@link #SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT} will be used if
+         * this method is never called.
+         */
+        @NonNull
+        public Builder setRankingStrategy(@SuggestionRankingStrategy int rankingStrategy) {
+            Preconditions.checkArgumentInRange(rankingStrategy,
+                    SUGGESTION_RANKING_STRATEGY_DOCUMENT_COUNT, SUGGESTION_RANKING_STRATEGY_NONE,
+                    "Suggestion ranking strategy");
+            resetIfBuilt();
+            mRankingStrategy = rankingStrategy;
+            return this;
+        }
+
         /** Constructs a new {@link SearchSpec} from the contents of this builder. */
         @NonNull
         public SearchSuggestionSpec build() {
             Bundle bundle = new Bundle();
             bundle.putStringArrayList(NAMESPACE_FIELD, mNamespaces);
             bundle.putInt(MAXIMUM_RESULT_COUNT_FIELD, mTotalResultCount);
+            bundle.putInt(RANKING_STRATEGY_FIELD, mRankingStrategy);
             mBuilt = true;
             return new SearchSuggestionSpec(bundle);
         }
diff --git a/benchmark/benchmark-common/api/public_plus_experimental_current.txt b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
index 24e4123..54c1187 100644
--- a/benchmark/benchmark-common/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-common/api/public_plus_experimental_current.txt
@@ -55,9 +55,30 @@
 
 package androidx.benchmark.perfetto {
 
+  @kotlin.RequiresOptIn @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.FUNCTION}) public @interface ExperimentalPerfettoCaptureApi {
+  }
+
   public final class PerfettoConfigKt {
   }
 
+  @RequiresApi(21) @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTrace {
+    ctor public PerfettoTrace(String path);
+    method public String getPath();
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public static void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    property public final String path;
+    field public static final androidx.benchmark.perfetto.PerfettoTrace.Companion Companion;
+  }
+
+  public static final class PerfettoTrace.Companion {
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, optional String? userspaceTracingPackage, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, optional java.util.List<java.lang.String> appTagPackages, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+    method public void record(String fileLabel, kotlin.jvm.functions.Function0<kotlin.Unit> block);
+  }
+
   public final class UiStateKt {
   }
 
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
index 952d3c4..02322c9 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/OutputsTest.kt
@@ -109,7 +109,7 @@
             file.writeText(file.name) // use name, as it's fairly unique
             Assert.assertEquals(
                 file.name,
-                Shell.executeCommand("cat ${file.absolutePath}")
+                Shell.executeScriptCaptureStdout("cat ${file.absolutePath}")
             )
         } finally {
             file.delete()
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 32953bb..bd01556 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellBehaviorTest.kt
@@ -17,14 +17,15 @@
 package androidx.benchmark
 
 import android.os.Build
+import android.os.Process
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
-import org.junit.Test
-import org.junit.runner.RunWith
 import kotlin.test.assertEquals
 import kotlin.test.assertNotNull
 import kotlin.test.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
 
 /**
  * This class collects tests of strange shell behavior, for the purpose of documenting
@@ -34,23 +35,34 @@
 @SdkSuppress(minSdkVersion = 21)
 @RunWith(AndroidJUnit4::class)
 class ShellBehaviorTest {
+    /**
+     * Test validates consistent behavior of pgrep, for usage in discovering processes without
+     * needing to check stderr
+     */
     @Test
     fun pgrepLF() {
         // Should only be one process - this one!
-        val pgrepString = Shell.executeCommand("pgrep -l -f ${Packages.TEST}").trim()
+        val pgrepOutput = Shell.executeScriptCaptureStdoutStderr("pgrep -l -f ${Packages.TEST}")
 
         if (Build.VERSION.SDK_INT >= 23) {
-            assertTrue(pgrepString.endsWith(" ${Packages.TEST}"))
+            // API 23 has trailing whitespace after the package name for some reason :shrug:
+            val regex = "^\\d+ ${Packages.TEST.replace(".", "\\.")}\\s*$".toRegex()
+            assertTrue(
+                // For some reason, `stdout.contains(regex)` doesn't work :shrug:
+                pgrepOutput.stdout.lines().any { it.matches(regex) },
+                "expected $regex to be contained in output:\n${pgrepOutput.stdout}"
+            )
         } else {
-            // command doesn't exist (and we don't try and read stderr here)
-            assertEquals("", pgrepString)
+            // command doesn't exist
+            assertEquals("", pgrepOutput.stdout)
+            assertTrue(pgrepOutput.stderr.isNotBlank())
         }
     }
 
     @Test
     fun pidof() {
         // Should only be one process - this one!
-        val pidofString = Shell.executeCommand("pidof ${Packages.TEST}").trim()
+        val pidofString = Shell.executeScriptCaptureStdout("pidof ${Packages.TEST}").trim()
 
         when {
             Build.VERSION.SDK_INT < 23 -> {
@@ -69,14 +81,16 @@
 
     @Test
     fun psDashA() {
-        val output = Shell.executeCommand("ps -A").trim()
+        val output = Shell.executeScriptCaptureStdout("ps -A").trim()
         when {
             Build.VERSION.SDK_INT <= 23 -> {
-                // doesn't correctly handle -A
-                assertTrue(
-                    output.matches(Regex("USER.+PID.+PPID.+VSIZE.+RSS.+WCHAN.+PC.+NAME")),
-                    "expected no processes from 'ps -A', saw $output"
-                )
+                // doesn't correctly handle -A, sometimes sees nothing, sometimes only this process
+                val processes = output.lines()
+                assertTrue(processes.size <= 2)
+                assertTrue(processes.first().matches(psLabelRowRegex))
+                if (processes.size > 1) {
+                    assertTrue(processes.last().endsWith(Packages.TEST))
+                }
             }
             Build.VERSION.SDK_INT in 24..25 -> {
                 // still doesn't support, but useful error at least
@@ -84,10 +98,37 @@
             }
             else -> {
                 // ps -A should work - expect several processes including this one
-                val processes = output.split("\n")
+                val processes = output.lines()
                 assertTrue(processes.size > 5)
+                assertTrue(processes.first().matches(psLabelRowRegex))
                 assertTrue(processes.any { it.endsWith(Packages.TEST) })
             }
         }
     }
+
+    /**
+     * Test validates consistent behavior of ps, for usage in checking process is alive without
+     * needing to check stderr
+     */
+    @Test
+    fun ps() {
+        val output = Shell.executeScriptCaptureStdout("ps ${Process.myPid()}").trim()
+        // ps should work - expect several processes including this one
+        val lines = output.lines()
+        assertEquals(2, lines.size)
+        assertTrue(lines.first().matches(psLabelRowRegex))
+        assertTrue(lines.last().endsWith(Packages.TEST))
+    }
+
+    companion object {
+        /**
+         * Regex for matching ps output label row
+         *
+         * Note that `ps` output changes over time, e.g.:
+         *
+         * * API 23 - `USER\s+PID\s+PPID\s+VSIZE\s+RSS\s+WCHAN\s+PC\s+NAME`
+         * * API 33 - `USER\s+PID\s+PPID\s+VSZ\s+RSS\s+WCHAN\s+ADDR\s+S\s+NAME\s`
+         */
+        val psLabelRowRegex = Regex("USER\\s+PID.+NAME\\s*")
+    }
 }
\ No newline at end of file
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 4139a44..c23a119 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/ShellTest.kt
@@ -108,20 +108,37 @@
 
     @SdkSuppress(minSdkVersion = 21)
     @Test
-    fun executeScript_trivial() {
-        Assert.assertEquals("foo\n", Shell.executeScript("echo foo"))
+    fun executeScriptCaptureStdout_trivial() {
+        Assert.assertEquals("foo\n", Shell.executeScriptCaptureStdout("echo foo"))
     }
 
     @SdkSuppress(minSdkVersion = 21)
     @Test
-    fun executeScriptWithStderr_trivial() {
-        Assert.assertEquals(Shell.Output("foo\n", ""), Shell.executeScriptWithStderr("echo foo"))
+    fun executeScriptCaptureStdoutStderr_trivial() {
+        Assert.assertEquals(
+            Shell.Output("foo\n", ""),
+            Shell.executeScriptCaptureStdoutStderr("echo foo")
+        )
     }
 
     @SdkSuppress(minSdkVersion = 21)
     @Test
-    fun executeScriptWithStderr_invalidCommand() {
-        val shellOutput = Shell.executeScriptWithStderr("invalidCommand")
+    fun executeScriptCaptureStdoutStderr_stderrFirstLine() {
+        Assert.assertEquals(
+            Shell.Output("bar\n", "foo\n"),
+            Shell.executeScriptCaptureStdoutStderr(
+                """
+                echo foo 1>&2 # stderr on 1st line, previously unsupported
+                echo bar
+                """.trimIndent()
+            )
+        )
+    }
+
+    @SdkSuppress(minSdkVersion = 21)
+    @Test
+    fun executeScriptCaptureStdoutStderr_invalidCommand() {
+        val shellOutput = Shell.executeScriptCaptureStdoutStderr("invalidCommand")
 
         Assert.assertEquals("", shellOutput.stdout)
 
@@ -138,38 +155,47 @@
 
     @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
     @Test
-    fun executeScript_pipe_xargs() {
+    fun executeScriptCaptureStdout_pipe_xargs() {
         // validate piping works with xargs
-        Assert.assertEquals("foo\n", Shell.executeScript("echo foo | xargs echo $1"))
+        Assert.assertEquals("foo\n", Shell.executeScriptCaptureStdout("echo foo | xargs echo $1"))
     }
 
     @SdkSuppress(minSdkVersion = 29) // `$(</dev/stdin)` doesn't work before 29
     @Test
-    fun executeScript_pipe_echo() {
+    fun executeScriptCaptureStdout_pipe_echo() {
         // validate piping works
-        Assert.assertEquals("foo\n", Shell.executeScript("echo foo | echo $(</dev/stdin)"))
+        Assert.assertEquals(
+            "foo\n",
+            Shell.executeScriptCaptureStdout("echo foo | echo $(</dev/stdin)")
+        )
     }
 
     @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
     @Test
-    fun executeScript_stdinArg_xargs() {
+    fun executeScriptCaptureStdout_stdinArg_xargs() {
         // validate stdin to first command in script
-        Assert.assertEquals("foo\n", Shell.executeScript("xargs echo $1", stdin = "foo"))
+        Assert.assertEquals(
+            "foo\n",
+            Shell.executeScriptCaptureStdout("xargs echo $1", stdin = "foo")
+        )
     }
 
     @SdkSuppress(minSdkVersion = 29) // `$(</dev/stdin)` doesn't work before 29
     @Test
-    fun executeScript_stdinArg_echo() {
+    fun executeScriptCaptureStdout_stdinArg_echo() {
         // validate stdin to first command in script
-        Assert.assertEquals("foo\n", Shell.executeScript("echo $(</dev/stdin)", stdin = "foo"))
+        Assert.assertEquals(
+            "foo\n",
+            Shell.executeScriptCaptureStdout("echo $(</dev/stdin)", stdin = "foo")
+        )
     }
 
     @SdkSuppress(minSdkVersion = 21)
     @Test
-    fun executeScript_multilineRedirect() {
+    fun executeScriptCaptureStdout_multilineRedirect() {
         Assert.assertEquals(
             "foo\n",
-            Shell.executeScript(
+            Shell.executeScriptCaptureStdout(
                 """
                     echo foo > /data/local/tmp/foofile
                     cat /data/local/tmp/foofile
@@ -180,10 +206,10 @@
 
     @SdkSuppress(minSdkVersion = 26) // xargs only available 26+
     @Test
-    fun executeScript_multilineRedirectStdin_xargs() {
+    fun executeScriptCaptureStdout_multilineRedirectStdin_xargs() {
         Assert.assertEquals(
             "foo\n",
-            Shell.executeScript(
+            Shell.executeScriptCaptureStdout(
                 """
                     xargs echo $1 > /data/local/tmp/foofile
                     cat /data/local/tmp/foofile
@@ -195,10 +221,10 @@
 
     @SdkSuppress(minSdkVersion = 29) // `$(</dev/stdin)` doesn't work before 29
     @Test
-    fun executeScript_multilineRedirectStdin_echo() {
+    fun executeScriptCaptureStdout_multilineRedirectStdin_echo() {
         Assert.assertEquals(
             "foo\n",
-            Shell.executeScript(
+            Shell.executeScriptCaptureStdout(
                 """
                     echo $(</dev/stdin) > /data/local/tmp/foofile
                     cat /data/local/tmp/foofile
@@ -218,10 +244,10 @@
         try {
             Assert.assertEquals(
                 "foo\n",
-                Shell.executeCommand(path)
+                Shell.executeScriptCaptureStdout(path)
             )
         } finally {
-            Shell.executeCommand("rm $path")
+            Shell.executeScriptCaptureStdout("rm $path")
         }
     }
 
@@ -348,15 +374,29 @@
     @Test
     fun checkRootStatus() {
         if (Shell.isSessionRooted()) {
-            assertContains(Shell.executeCommand("id"), "uid=0(root)")
+            assertContains(Shell.executeScriptCaptureStdout("id"), "uid=0(root)")
         } else {
             assertFalse(
-                Shell.executeCommand("id").contains("uid=0(root)"),
+                Shell.executeScriptCaptureStdout("id").contains("uid=0(root)"),
                 "Shell.isSessionRooted() is false so user should not be root"
             )
         }
     }
 
+    @SdkSuppress(minSdkVersion = 21)
+    @Test
+    fun shellReuse() {
+        val script = Shell.createShellScript("xargs echo $1", stdin = "foo")
+
+        repeat(2) {
+            // validates that the stdin can be reused across multiple invocations
+            with(script.start()) {
+                assertEquals(listOf("foo"), stdOutLineSequence().toList())
+            }
+        }
+        script.cleanUp()
+    }
+
     @RequiresApi(21)
     private fun pidof(packageName: String): Int? {
         return Shell.getPidsForProcess(packageName).firstOrNull()
@@ -374,7 +414,7 @@
          */
         @RequiresApi(23)
         fun getBackgroundSpinningProcess(): Shell.ProcessPid {
-            val pid = Shell.executeScript(
+            val pid = Shell.executeScriptCaptureStdout(
                 """
                     $BACKGROUND_SPINNING_PROCESS_NAME > /dev/null 2> /dev/null &
                     echo $!
diff --git a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/perfetto/AtraceTagTest.kt b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/perfetto/AtraceTagTest.kt
index 88f8578..c5f13d6 100644
--- a/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/perfetto/AtraceTagTest.kt
+++ b/benchmark/benchmark-common/src/androidTest/java/androidx/benchmark/perfetto/AtraceTagTest.kt
@@ -35,12 +35,12 @@
 
     @Test
     fun atraceListCategories_readable() {
-        val results = Shell.executeCommand("atrace --list_categories")
+        val results = Shell.executeScriptCaptureStdout("atrace --list_categories")
         assertNotEquals("", results)
     }
 
     private fun getActualSupportedTags(): Set<String> {
-        val results = Shell.executeCommand("atrace --list_categories")
+        val results = Shell.executeScriptCaptureStdout("atrace --list_categories")
 
         assertNotEquals("", results)
         val actualSupportedTags = results
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
index 8ef2930..9e372d7 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Profiler.kt
@@ -220,16 +220,16 @@
         securityPerfHarden.forceValue()
 
         // for all other properties, simply set the values, as these don't have defaults
-        Shell.executeCommand("setprop debug.perf_event_max_sample_rate 10000")
-        Shell.executeCommand("setprop debug.perf_cpu_time_max_percent 25")
-        Shell.executeCommand("setprop debug.perf_event_mlock_kb 32800")
+        Shell.executeScriptSilent("setprop debug.perf_event_max_sample_rate 10000")
+        Shell.executeScriptSilent("setprop debug.perf_cpu_time_max_percent 25")
+        Shell.executeScriptSilent("setprop debug.perf_event_mlock_kb 32800")
 
         outputRelativePath = traceName(traceUniqueName, "stackSampling")
         session = ProfileSession().also {
             // prepare simpleperf must be done as shell user, so do this here with other shell setup
             // NOTE: this is sticky across reboots, so missing this will cause tests or profiling to
             // fail, but only on devices that have not run this command since flashing (e.g. in CI)
-            Shell.executeCommand(it.findSimpleperf() + " api-prepare")
+            Shell.executeScriptSilent(it.findSimpleperf() + " api-prepare")
             it.startRecording(
                 RecordOptions()
                     .setSampleFrequency(Arguments.profilerSampleFrequency)
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/PropOverride.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/PropOverride.kt
index 7fb05d6..bf57294 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/PropOverride.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/PropOverride.kt
@@ -43,11 +43,11 @@
             return
         }
 
-        val currentPropVal = Shell.executeCommand("getprop $propName").trim()
+        val currentPropVal = Shell.getprop(propName)
         if (currentPropVal != overrideValue) {
             resetValue = currentPropVal
             Log.d(BenchmarkState.TAG, "setting $propName to $overrideValue (was $currentPropVal)")
-            Shell.executeCommand("setprop $propName $overrideValue")
+            Shell.executeScriptCaptureStdout("setprop $propName $overrideValue")
         }
     }
 
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 8a3587c..86d304e 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/Shell.kt
@@ -22,6 +22,7 @@
 import android.os.ParcelFileDescriptor.AutoCloseInputStream
 import android.os.SystemClock
 import android.util.Log
+import androidx.annotation.CheckResult
 import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.test.platform.app.InstrumentationRegistry
@@ -77,23 +78,18 @@
     }
 
     /**
-     * Run a command, and capture stdout
+     * Run a command, and capture stdout, dropping / ignoring stderr
      *
      * Below L, returns null
      */
     fun optionalCommand(command: String): String? {
         return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            executeCommand(command)
+            executeScriptCaptureStdoutStderr(command).stdout
         } else {
             null
         }
     }
 
-    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
-    fun executeCommand(command: String): String {
-        return ShellImpl.executeCommand(command)
-    }
-
     /**
      * Function for reading shell-accessible proc files, like scaling_max_freq, which can't be
      * read directly by the app process.
@@ -113,21 +109,20 @@
 
     @RequiresApi(21)
     fun chmodExecutable(absoluteFilePath: String) {
+        // use unsafe commands, as this is used in Shell.executeScript
         if (Build.VERSION.SDK_INT >= 23) {
-            ShellImpl.executeCommand("chmod +x $absoluteFilePath")
+            ShellImpl.executeCommandUnsafe("chmod +x $absoluteFilePath")
         } 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.executeCommand("chmod 777 $absoluteFilePath")
+            ShellImpl.executeCommandUnsafe("chmod 777 $absoluteFilePath")
         }
     }
 
     @RequiresApi(21)
     fun moveToTmpAndMakeExecutable(src: String, dst: String) {
-        // Note: we don't check for return values from the below, since shell based file
-        // permission errors generally crash our process.
-        ShellImpl.executeCommand("cp $src $dst")
+        ShellImpl.executeCommandUnsafe("cp $src $dst")
         chmodExecutable(dst)
     }
 
@@ -170,11 +165,17 @@
         return ShellImpl.isSessionRooted || ShellImpl.isSuAvailable
     }
 
+    @RequiresApi(21)
+    fun getprop(propertyName: String): String {
+        return executeScriptCaptureStdout("getprop $propertyName").trim()
+    }
+
     /**
-     * Convenience wrapper around [UiAutomation.executeShellCommand()] which enables redirects,
-     * piping, and all other shell script functionality.
+     * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
+     * scripting functionality like piping and redirects, and which throws if stdout or stder was
+     * produced.
      *
-     * Unlike [UiDevice.executeShellCommand()], this method supports arbitrary multi-line shell
+     * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell
      * expressions, as it creates and executes a shell script in `/data/local/tmp/`.
      *
      * Note that shell scripting capabilities differ based on device version. To see which utilities
@@ -187,12 +188,34 @@
      * @return Stdout string
      */
     @RequiresApi(21)
-    fun executeScript(script: String, stdin: String? = null): String {
-        return ShellImpl
-            .createShellScript(script, stdin, false)
-            .start()
-            .getOutputAndClose()
-            .stdout
+    fun executeScriptSilent(script: String, stdin: String? = null) {
+        val output = executeScriptCaptureStdoutStderr(script, stdin)
+        check(output.isBlank()) { "Expected no stdout/stderr from $script, saw $output" }
+    }
+
+    /**
+     * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
+     * scripting functionality like piping and redirects, and which captures stdout and throws if
+     * stderr was produced.
+     *
+     * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell
+     * expressions, as it creates and executes a shell script in `/data/local/tmp/`.
+     *
+     * Note that shell scripting capabilities differ based on device version. To see which utilities
+     * are available on which platform versions,see
+     * [Android's shell and utilities](https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md#)
+     *
+     * @param script Script content to run
+     * @param stdin String to pass in as stdin to first command in script
+     *
+     * @return Stdout string
+     */
+    @RequiresApi(21)
+    @CheckResult
+    fun executeScriptCaptureStdout(script: String, stdin: String? = null): String {
+        val output = executeScriptCaptureStdoutStderr(script, stdin)
+        check(output.stderr.isBlank()) { "Expected no stderr from $script, saw ${output.stderr}" }
+        return output.stdout
     }
 
     data class Output(val stdout: String, val stderr: String) {
@@ -209,10 +232,10 @@
     }
 
     /**
-     * Convenience wrapper around [UiAutomation.executeShellCommand()] which enables redirects,
-     * piping, and all other shell script functionality, and which captures stderr of last command.
+     * Convenience wrapper around [android.app.UiAutomation.executeShellCommand] which adds
+     * scripting functionality like piping and redirects, and which captures both stdout and stderr.
      *
-     * Unlike [UiDevice.executeShellCommand()], this method supports arbitrary multi-line shell
+     * Unlike `executeShellCommand()`, this method supports arbitrary multi-line shell
      * expressions, as it creates and executes a shell script in `/data/local/tmp/`.
      *
      * Note that shell scripting capabilities differ based on device version. To see which utilities
@@ -222,21 +245,24 @@
      * @param script Script content to run
      * @param stdin String to pass in as stdin to first command in script
      *
-     * @return ShellOutput, including stdout of full script, and stderr of last command.
+     * @return Output object containing stdout and stderr of full script, and stderr of last command
      */
     @RequiresApi(21)
-    fun executeScriptWithStderr(
+    @CheckResult
+    fun executeScriptCaptureStdoutStderr(
         script: String,
         stdin: String? = null
     ): Output {
-        return ShellImpl
-            .createShellScript(script = script, stdin = stdin, includeStderr = true)
-            .start()
-            .getOutputAndClose()
+        return trace("executeScript $script".take(127)) {
+            ShellImpl
+                .createShellScript(script = script, stdin = stdin)
+                .start()
+                .getOutputAndClose()
+        }
     }
 
     /**
-     * Creates a executable shell script that can be started. Similar to [executeScriptWithStderr]
+     * Creates a executable shell script that can be started. Similar to [executeScriptCaptureStdoutStderr]
      * but allows deferring and caching script execution.
      *
      * @param script Script content to run
@@ -247,14 +273,12 @@
     @RequiresApi(21)
     fun createShellScript(
         script: String,
-        stdin: String? = null,
-        includeStderr: Boolean = true
+        stdin: String? = null
     ): ShellScript {
         return ShellImpl
             .createShellScript(
                 script = script,
-                stdin = stdin,
-                includeStderr = includeStderr
+                stdin = stdin
             )
     }
 
@@ -283,7 +307,7 @@
         // Can't use ps -A pre API 26, arg isn't supported.
         // Grep device side, since ps output by itself gets truncated
         // NOTE: `ps | grep` is slow (multiple seconds), so avoid whenever possible!
-        return executeScript("ps | grep $processName")
+        return executeScriptCaptureStdout("ps | grep $processName")
             .split(Regex("\r?\n"))
             .map { it.trim() }
             .filter { psLineContainsProcess(psOutputLine = it, processName = processName) }
@@ -306,7 +330,9 @@
      */
     @RequiresApi(23)
     private fun pgrepLF(pattern: String): List<Pair<Int, String>> {
-        return executeCommand("pgrep -l -f $pattern")
+        // Note: we use the unsafe variant for performance, since this is a
+        // common operation, and pgrep is stable after API 23 see [ShellBehaviorTest#pgrep]
+        return ShellImpl.executeCommandUnsafe("pgrep -l -f $pattern")
             .split(Regex("\r?\n"))
             .filter { it.isNotEmpty() }
             .map {
@@ -336,7 +362,7 @@
         // NOTE: Can't use ps -A pre API 26, arg isn't supported, but would need
         // to pass it on 26 to see all processes.
         // NOTE: `ps | grep` is slow (multiple seconds), so avoid whenever possible!
-        return executeScript("ps | grep $packageName")
+        return executeScriptCaptureStdout("ps | grep $packageName")
             .split(Regex("\r?\n"))
             .map {
                 // get process name from end
@@ -355,7 +381,9 @@
      */
     @RequiresApi(21)
     fun isProcessAlive(pid: Int, processName: String): Boolean {
-        return executeCommand("ps $pid")
+        // unsafe, since this behavior is well tested, and performance here is important
+        // See [ShellBehaviorTest#ps]
+        return ShellImpl.executeCommandUnsafe("ps $pid")
             .split(Regex("\r?\n"))
             .any { psLineContainsProcess(psOutputLine = it, processName = processName) }
     }
@@ -388,7 +416,9 @@
         vararg processes: ProcessPid
     ) {
         processes.forEach {
-            val stopOutput = executeCommand("kill -TERM ${it.pid}")
+            // NOTE: we don't fail on stdout/stderr, since killing processes can be racy, and
+            // killing one can kill others. Instead, validation of process death happens below.
+            val stopOutput = executeScriptCaptureStdoutStderr("kill -TERM ${it.pid}")
             Log.d(BenchmarkState.TAG, "kill -TERM command output - $stopOutput")
         }
 
@@ -408,7 +438,7 @@
 
     @RequiresApi(21)
     fun pathExists(absoluteFilePath: String): Boolean {
-        return executeCommand("ls $absoluteFilePath").trim() == absoluteFilePath
+        return ShellImpl.executeCommandUnsafe("ls $absoluteFilePath").trim() == absoluteFilePath
     }
 }
 
@@ -436,23 +466,26 @@
     init {
         // These variables are used in executeCommand and executeScript, so we keep them as var
         // instead of val and use a separate initializer
-        isSessionRooted = executeCommand("id").contains("uid=0(root)")
+        isSessionRooted = executeCommandUnsafe("id").contains("uid=0(root)")
+        // use a script below, since `su` command failure is unrecoverable on some API levels
         isSuAvailable = createShellScript(
-            "su root id",
-            null,
-            false
+            script = "su root id",
+            stdin = null
         ).start().getOutputAndClose().stdout.contains("uid=0(root)")
     }
 
     /**
      * Reimplementation of UiAutomator's Device.executeShellCommand,
      * to avoid the UiAutomator dependency, and add tracing
+     *
+     * NOTE: this does not capture stderr, and is thus unsafe. Only use this when the more complex
+     * Shell.executeScript APIs aren't appropriate (such as in their implementation)
      */
-    fun executeCommand(cmd: String): String = trace("executeCommand $cmd".take(127)) {
-        return@trace executeCommandNonBlocking(cmd).fullyReadInputStream()
+    fun executeCommandUnsafe(cmd: String): String = trace("executeCommand $cmd".take(127)) {
+        return@trace executeCommandNonBlockingUnsafe(cmd).fullyReadInputStream()
     }
 
-    fun executeCommandNonBlocking(cmd: String): ParcelFileDescriptor =
+    fun executeCommandNonBlockingUnsafe(cmd: String): ParcelFileDescriptor =
         trace("executeCommandNonBlocking $cmd".take(127)) {
             return@trace uiAutomation.executeShellCommand(
                 if (!isSessionRooted && isSuAvailable) {
@@ -465,57 +498,32 @@
 
     fun createShellScript(
         script: String,
-        stdin: String?,
-        includeStderr: Boolean
-    ): ShellScript = trace("createShellScript $script".take(127)) {
+        stdin: String?
+    ): ShellScript = trace("createShellScript") {
 
         // dirUsableByAppAndShell is writable, but we can't execute there (as of Q),
         // so we copy to /data/local/tmp
         val externalDir = Outputs.dirUsableByAppAndShell
-        val writableScriptFile = File.createTempFile("temporaryScript", ".sh", externalDir)
-        val runnableScriptPath = "/data/local/tmp/" + writableScriptFile.name
+        val scriptContentFile = File.createTempFile("temporaryScript", null, externalDir)
 
         // only create/read/delete stdin/stderr files if they are needed
         val stdinFile = stdin?.run {
             File.createTempFile("temporaryStdin", null, externalDir)
         }
-        val stderrPath = if (includeStderr) {
-            // we use a modified runnableScriptPath (as opposed to externalDir) because some shell
-            // commands fail to redirect stderr to externalDir (notably, `am start`).
-            // This also means we need to `cat` the file to read it, and `rm` to remove it.
-            runnableScriptPath + "_stderr"
-        } else {
-            null
-        }
-
-        var shellScript: ShellScript? = null
+        // we use a path on /data/local/tmp (as opposed to externalDir) because some shell
+        // commands fail to redirect stderr to externalDir (notably, `am start`).
+        // This also means we need to `cat` the file to read it, and `rm` to remove it.
+        val stderrPath = "/data/local/tmp/" + scriptContentFile.name + "_stderr"
 
         try {
-            var scriptText: String = script
-            if (stdinFile != null) {
-                stdinFile.writeText(stdin)
-                scriptText = "cat ${stdinFile.absolutePath} | $scriptText"
-            }
-            if (stderrPath != null) {
-                scriptText = "$scriptText 2> $stderrPath"
-            }
-            writableScriptFile.writeText(scriptText)
-
-            // Note: we don't check for return values from the below, since shell based file
-            // permission errors generally crash our process.
-            executeCommand("cp ${writableScriptFile.absolutePath} $runnableScriptPath")
-            Shell.chmodExecutable(runnableScriptPath)
-
-            shellScript = ShellScript(
+            stdinFile?.writeText(stdin)
+            scriptContentFile.writeText(script)
+            return@trace ShellScript(
                 stdinFile = stdinFile,
-                writableScriptFile = writableScriptFile,
-                stderrPath = stderrPath,
-                runnableScriptPath = runnableScriptPath
+                scriptContentFile = scriptContentFile,
+                stderrPath = stderrPath
             )
-
-            return@trace shellScript
         } catch (e: Exception) {
-            shellScript?.cleanUp()
             throw Exception("Can't create shell script", e)
         }
     }
@@ -525,24 +533,29 @@
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 class ShellScript internal constructor(
     private val stdinFile: File?,
-    private val writableScriptFile: File,
-    private val stderrPath: String?,
-    private val runnableScriptPath: String
+    private val scriptContentFile: File,
+    private val stderrPath: String
 ) {
-
     private var cleanedUp: Boolean = false
 
     /**
      * Starts the shell script previously created.
      *
-     * @param params a vararg string of parameters to be passed to the script.
-     *
      * @return a [StartedShellScript] that contains streams to read output streams.
      */
-    fun start(vararg params: String): StartedShellScript = trace("ShellScript#start") {
-        val cmd = "$runnableScriptPath ${params.joinToString(" ")}"
-        val stdoutDescriptor = ShellImpl.executeCommandNonBlocking(cmd)
-        val stderrDescriptorFn = stderrPath?.run { { ShellImpl.executeCommand("cat $stderrPath") } }
+    fun start(): StartedShellScript = trace("ShellScript#start") {
+        val stdoutDescriptor = ShellImpl.executeCommandNonBlockingUnsafe(
+            scriptWrapperCommand(
+                scriptContentPath = scriptContentFile.absolutePath,
+                stderrPath = stderrPath,
+                stdinPath = stdinFile?.absolutePath
+            )
+        )
+        val stderrDescriptorFn = stderrPath.run {
+            {
+                ShellImpl.executeCommandUnsafe("cat $stderrPath")
+            }
+        }
 
         return@trace StartedShellScript(
             stdoutDescriptor = stdoutDescriptor,
@@ -552,28 +565,64 @@
     }
 
     /**
-     * Manually clean up the shell script from the temp folder.
+     * Manually clean up the shell script temporary files from the temp folder.
      */
     fun cleanUp() = trace("ShellScript#cleanUp") {
         if (cleanedUp) {
             return@trace
         }
-        stdinFile?.delete()
-        writableScriptFile.delete()
-        if (stderrPath != null) {
-            ShellImpl.executeCommand("rm $stderrPath $runnableScriptPath")
-        } else {
-            ShellImpl.executeCommand("rm $runnableScriptPath")
-        }
+
+        // NOTE: while we could theoretically remove some of these files from the script, this isn't
+        // safe when the script is called multiple times, expecting the intermediates to remain.
+        // We need a rm to clean up the stderr file anyway (b/c it's not ready until stdout is
+        // complete), so we just delete everything here, all at once.
+        ShellImpl.executeCommandUnsafe(
+            "rm -f " + listOfNotNull(
+                stderrPath,
+                scriptContentFile.absolutePath,
+                stdinFile?.absolutePath
+            ).joinToString(" ")
+        )
         cleanedUp = true
     }
+
+    companion object {
+        /**
+         * Usage args: ```path/to/shellWrapper.sh <scriptFile> <stderrFile> [inputFile]```
+         */
+        private val scriptWrapperPath = Shell.createRunnableExecutable(
+            "shellWrapper.sh",
+            """
+                ### shell script which passes in stdin as needed, and captures stderr in a file
+                # $1 == script content (not executable)
+                # $2 == stderr
+                # $3 == stdin (optional)
+                if [[ $3 -eq "0" ]]; then
+                    /system/bin/sh $1 2> $2
+                else
+                    cat $3 | /system/bin/sh $1 2> $2
+                fi
+            """.trimIndent().byteInputStream()
+        )
+
+        fun scriptWrapperCommand(
+            scriptContentPath: String,
+            stderrPath: String,
+            stdinPath: String?
+        ): String = listOfNotNull(
+            scriptWrapperPath,
+            scriptContentPath,
+            stderrPath,
+            stdinPath
+        ).joinToString(" ")
+    }
 }
 
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
 class StartedShellScript internal constructor(
     private val stdoutDescriptor: ParcelFileDescriptor,
-    private val stderrDescriptorFn: (() -> (String))?,
+    private val stderrDescriptorFn: (() -> (String)),
     private val cleanUpBlock: () -> Unit
 ) : Closeable {
 
@@ -594,7 +643,7 @@
     fun getOutputAndClose(): Shell.Output {
         val output = Shell.Output(
             stdout = stdoutDescriptor.fullyReadInputStream(),
-            stderr = stderrDescriptorFn?.invoke() ?: ""
+            stderr = stderrDescriptorFn.invoke()
         )
         close()
         return output
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
similarity index 67%
copy from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
copy to benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
index 07fb357..28d3e1f 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/ExperimentalPerfettoCaptureApi.kt
@@ -14,11 +14,12 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.benchmark.perfetto
 
-@RequiresOptIn(
-    level = RequiresOptIn.Level.WARNING,
-    message = "This API is experimental and is likely to change in the future."
-)
-@Target(AnnotationTarget.FUNCTION)
-annotation class ExperimentalMultiProcessDataStore
\ No newline at end of file
+/**
+ * Annotation indicating experimental API for capturing Perfetto traces.
+ */
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
+public annotation class ExperimentalPerfettoCaptureApi
\ No newline at end of file
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
index 04756ad..163a308 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCapture.kt
@@ -116,7 +116,7 @@
                     }
                 }.toMap()
             },
-            executeShellCommand = Shell::executeCommand
+            executeShellCommand = Shell::executeScriptCaptureStdout
         )
 
         // negotiate enabling tracing in the app
@@ -130,8 +130,8 @@
                     baseApk,
                     Outputs.dirUsableByAppAndShell
                 ) { tmpFile, dstFile ->
-                    executeShellCommand("mkdir -p ${dstFile.parentFile!!.path}", Regex("^$"))
-                    executeShellCommand("mv ${tmpFile.path} ${dstFile.path}", Regex("^$"))
+                    Shell.executeScriptSilent("mkdir -p ${dstFile.parentFile!!.path}")
+                    Shell.executeScriptSilent("mv ${tmpFile.path} ${dstFile.path}")
                 }
                 handshake.enableTracing(libraryProvider)
             } // provide binaries and retry
@@ -168,13 +168,4 @@
             else -> throw RuntimeException("Unrecognized exit code: ${response.exitCode}.")
         }
     }
-
-    private fun executeShellCommand(command: String, expectedResponse: Regex) {
-        val response = Shell.executeCommand(command)
-        if (!response.matches(expectedResponse)) throw IllegalStateException(
-            "Command response not matching expected." +
-                " Command: $command." +
-                " Expected response: ${expectedResponse.pattern}."
-        )
-    }
 }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
index 166b3e9..58a1723 100644
--- a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoCaptureWrapper.kt
@@ -79,33 +79,45 @@
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
+    private fun stop(traceLabel: String): String {
+        return Outputs.writeFile(
+            fileName = "${traceLabel}_${dateToFileName()}.perfetto-trace",
+            reportKey = "perfetto_trace_$traceLabel"
+        ) {
+            capture!!.stop(it.absolutePath)
+        }
+    }
+
     fun record(
-        benchmarkName: String,
+        fileLabel: String,
         appTagPackages: List<String>,
         userspaceTracingPackage: String?,
-        iteration: Int? = null,
+        traceCallback: ((String) -> Unit)? = null,
         block: () -> Unit
     ): String? {
         // skip if Perfetto not supported, or on Cuttlefish (where tracing doesn't work)
         if (Build.VERSION.SDK_INT < 21 || !isAbiSupported()) {
             block()
-            return null // tracing not supported
+            return null
         }
 
         // Prior to Android 11 (R), a shell property must be set to enable perfetto tracing, see
         // https://perfetto.dev/docs/quickstart/android-tracing#starting-the-tracing-services
-        val propOverride = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
+        val propOverride = if (Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
             PropOverride(TRACE_ENABLE_PROP, "1")
         } else null
+
+        val path: String
         try {
             propOverride?.forceValue()
             start(appTagPackages, userspaceTracingPackage)
-            val path: String
             try {
                 block()
             } finally {
                 // finally here to ensure trace is fully recorded if block throws
-                path = stop(benchmarkName, iteration)
+                path = stop(fileLabel)
+                traceCallback?.invoke(path)
             }
             return path
         } finally {
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 d919572..4d149d2 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
@@ -96,8 +96,8 @@
             val actualConfigPath = if (unbundled) {
                 val path = "$UNBUNDLED_PERFETTO_ROOT_DIR/config.pb"
                 // Move the config to a directory that unbundled perfetto has permissions for.
-                Shell.executeCommand("rm $path")
-                Shell.executeCommand("mv $configFilePath $path")
+                Shell.executeScriptSilent("rm -f $path")
+                Shell.executeScriptSilent("mv $configFilePath $path")
                 path
             } else {
                 configFilePath
@@ -105,13 +105,13 @@
 
             val outputPath = getPerfettoTmpOutputFilePath()
             // Remove already existing temporary output trace file if any.
-            val output = Shell.executeCommand("rm $outputPath")
-            Log.i(LOG_TAG, "Perfetto output file cleanup - $output")
+            Shell.executeScriptSilent("rm -f $outputPath")
 
             // Perfetto
             val perfettoCmd = perfettoCommand(actualConfigPath, isTextProtoConfig)
             Log.i(LOG_TAG, "Starting perfetto tracing with cmd: $perfettoCmd")
-            val perfettoCmdOutput = Shell.executeScript("$perfettoCmd; echo EXITCODE=$?").trim()
+            val perfettoCmdOutput =
+                Shell.executeScriptCaptureStdout("$perfettoCmd; echo EXITCODE=$?").trim()
 
             val expectedSuffix = "\nEXITCODE=0"
             if (!perfettoCmdOutput.endsWith(expectedSuffix)) {
@@ -162,7 +162,7 @@
         val pollTracingOnMs = 100L
 
         repeat(pollTracingOnMaxCount) {
-            when (val output = Shell.executeCommand("cat $path").trim()) {
+            when (val output = Shell.executeScriptCaptureStdout("cat $path").trim()) {
                 "0" -> {
                     userspaceTrace("wait for trace to start (tracing_on == 1)") {
                         SystemClock.sleep(pollTracingOnMs)
@@ -315,8 +315,8 @@
         // destinationFile
         try {
             val moveResult =
-                Shell.executeCommand("mv $sourceFile $destinationFile")
-            if (moveResult.isNotEmpty()) {
+                Shell.executeScriptCaptureStdoutStderr("mv $sourceFile $destinationFile")
+            if (!moveResult.isBlank()) {
                 Log.e(
                     LOG_TAG,
                     """
@@ -423,15 +423,22 @@
                 )
             }
 
-            // Have seen cases where unbundled Perfetto crashes, and leaves ftrace enabled,
+            // Have seen cases where bundled Perfetto crashes, and leaves ftrace enabled,
             // e.g. b/205763418. --cleanup-after-crash will reset that state, so it doesn't leak
             // between tests. If this sort of crash happens on higher API levels, may need to do
-            // this there as well. Can't use /system/bin/traced_probes, as that requires root, and
-            // unbundled tracebox otherwise not used/installed on higher APIs, outside of tests.
+            // this there as well. Can't use bundled /system/bin/traced_probes, as that requires
+            // root, and unbundled tracebox otherwise not used/installed on higher APIs, outside
+            // of tests.
             if (Build.VERSION.SDK_INT < LOWEST_BUNDLED_VERSION_SUPPORTED) {
-                Shell.executeCommand(
+                val output = Shell.executeScriptCaptureStdoutStderr(
                     "$unbundledPerfettoShellPath traced_probes --cleanup-after-crash"
                 )
+                check(
+                    output.stderr.isBlank() ||
+                        output.stderr.contains("Hard resetting ftrace state")
+                ) {
+                    "Unexpected output from --cleanup-after-crash: $output"
+                }
             }
         }
     }
diff --git a/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
new file mode 100644
index 0000000..33bdde3
--- /dev/null
+++ b/benchmark/benchmark-common/src/main/java/androidx/benchmark/perfetto/PerfettoTrace.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.perfetto
+
+import androidx.annotation.RequiresApi
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.File
+
+@RequiresApi(21)
+@ExperimentalPerfettoCaptureApi
+class PerfettoTrace(
+    /**
+     * Absolute file path of the trace.
+     *
+     * Note that the trace is not guaranteed to be placed into an app-accessible directory, and
+     * may require shell commands to access.
+     */
+    val path: String
+) {
+    companion object {
+        /**
+         * Record a Perfetto System Trace for the specified [block].
+         *
+         * If the block throws, the trace is still captured and passed to [traceCallback].
+         */
+        @JvmStatic
+        @JvmOverloads
+        fun record(
+            /**
+             * Output trace file names are labelled `<fileLabel>_<timestamp>.perfetto_trace`
+             *
+             * This timestamp is used for uniqueness when trace files are pulled automatically to
+             * Studio.
+             */
+            fileLabel: String,
+            /**
+             * Target process to trace with app tag (enables android.os.Trace / androidx.Trace).
+             *
+             * By default, traces this process.
+             */
+            appTagPackages: List<String> = listOf(
+                InstrumentationRegistry.getInstrumentation().targetContext.packageName
+            ),
+            /**
+             * Process to trace with userspace tracing, i.e. `androidx.tracing:tracing-perfetto`,
+             * ignored below API 30.
+             *
+             * This tracing is lower overhead than standard `android.os.Trace` tracepoints, but is
+             * currently experimental.
+             */
+            userspaceTracingPackage: String? = null,
+            /**
+             * Callback for trace capture.
+             *
+             * This callback allows you to process the trace even if the block throws, e.g. during
+             * a test failure.
+             */
+            traceCallback: ((PerfettoTrace) -> Unit)? = null,
+            /**
+             * Block to be traced.
+             */
+            block: () -> Unit
+        ) {
+            PerfettoCaptureWrapper().record(
+                fileLabel = fileLabel,
+                appTagPackages = appTagPackages,
+                userspaceTracingPackage = userspaceTracingPackage,
+                traceCallback = { path ->
+                    // emphasize the first package in the package list, or target package otherwise
+                    val highlightPackage = appTagPackages.firstOrNull()
+                        ?: InstrumentationRegistry.getInstrumentation().targetContext.packageName
+                    File(path).appendUiState(
+                        UiState(
+                            timelineStart = null,
+                            timelineEnd = null,
+                            highlightPackage = highlightPackage
+                        )
+                    )
+                    traceCallback?.invoke(PerfettoTrace(path))
+                },
+                block = block
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
index 13f1350..e98a49b 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt
@@ -16,6 +16,7 @@
 
 package androidx.benchmark.darwin.gradle
 
+import java.io.File
 import java.util.concurrent.atomic.AtomicBoolean
 import org.gradle.api.Plugin
 import org.gradle.api.Project
@@ -54,6 +55,17 @@
         extension: DarwinBenchmarkPluginExtension,
         project: Project
     ) {
+        // You can override the xcodeGenDownloadUri by specifying something like:
+        // androidx.benchmark.darwin.xcodeGenDownloadUri=https://github.com/yonaskolb/XcodeGen/releases/download/2.32.0/xcodegen.zip
+        val xcodeGenUri = when (val uri = project.findProperty(XCODEGEN_DOWNLOAD_URI)) {
+            null -> File(
+                project.rootProject.projectDir, // frameworks/support
+                "../../prebuilts/androidx/external/xcodegen"
+            ).absoluteFile.toURI().toString()
+
+            else -> uri.toString()
+        }
+
         val xcodeProjectPath = extension.xcodeProjectName.flatMap { name ->
             project.layout.buildDirectory.dir("$name.xcodeproj")
         }
@@ -62,9 +74,17 @@
             project.layout.buildDirectory.dir("$name.xcresult")
         }
 
+        val fetchXCodeGenTask = project.tasks.register(
+            FETCH_XCODEGEN_TASK, FetchXCodeGenTask::class.java
+        ) {
+            it.xcodeGenUri.set(xcodeGenUri)
+            it.downloadPath.set(project.layout.buildDirectory.dir("xcodegen"))
+        }
+
         val generateXCodeProjectTask = project.tasks.register(
             GENERATE_XCODE_PROJECT_TASK, GenerateXCodeProjectTask::class.java
         ) {
+            it.xcodeGenPath.set(fetchXCodeGenTask.map { task -> task.xcodeGenBinary() })
             it.yamlFile.set(extension.xcodeGenConfigFile)
             it.projectName.set(extension.xcodeProjectName)
             it.xcProjectPath.set(xcodeProjectPath)
@@ -98,6 +118,11 @@
     }
 
     private companion object {
+        // Gradle Properties
+        const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri"
+
+        // Tasks
+        const val FETCH_XCODEGEN_TASK = "fetchXCodeGen"
         const val GENERATE_XCODE_PROJECT_TASK = "generateXCodeProject"
         const val RUN_DARWIN_BENCHMARKS_TASK = "runDarwinBenchmarks"
         const val DARWIN_BENCHMARK_RESULTS_TASK = "darwinBenchmarkResults"
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt
new file mode 100644
index 0000000..6b8d010
--- /dev/null
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/FetchXCodeGenTask.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.darwin.gradle
+
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.net.URI
+import java.util.zip.ZipInputStream
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFile
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.TaskAction
+
+/**
+ * Fetches the `xcodegen` binary which is used to build the XCode project.
+ */
+@CacheableTask
+abstract class FetchXCodeGenTask : DefaultTask() {
+    @get:Input
+    abstract val xcodeGenUri: Property<String>
+
+    @get:OutputDirectory
+    abstract val downloadPath: DirectoryProperty
+
+    @TaskAction
+    fun fetchXcodeGenTask() {
+        val uri = URI(xcodeGenUri.get())
+        when (uri.scheme) {
+            "https" -> {
+                downloadAndExtractXcodeGen(uri)
+            }
+
+            "file" -> {
+                copyXcodeGen(File(uri))
+            }
+
+            else -> throw GradleException("Unsupported scheme")
+        }
+    }
+
+    private fun downloadAndExtractXcodeGen(uri: URI) {
+        val downloadRoot = downloadPath.get().asFile
+        // Setup download location
+        downloadRoot.deleteRecursively()
+        downloadRoot.mkdirs()
+        val url = uri.toURL()
+        val inputStream = url.openStream()
+        // Download
+        val zipFile = File(downloadRoot, "xcodegen.zip")
+        inputStream.use {
+            val outputStream = zipFile.outputStream()
+            outputStream.use {
+                inputStream.copyTo(it)
+            }
+        }
+        // Setup Output Location
+        // This must end with a `bin` for the xcodegen path discovery to work
+        val xcodeGenBinaryPath = File(downloadRoot, "bin")
+        xcodeGenBinaryPath.mkdirs()
+        // Extract
+        val zipStream = ZipInputStream(FileInputStream(zipFile))
+        zipStream.use {
+            var entry = zipStream.nextEntry
+            while (entry != null) {
+                if (entry.name.endsWith("/bin/xcodegen")) {
+                    val output = FileOutputStream(File(xcodeGenBinaryPath, "xcodegen"))
+                    output.use {
+                        zipStream.copyTo(output)
+                    }
+                    break
+                } else {
+                    zipStream.closeEntry()
+                    entry = zipStream.nextEntry
+                }
+            }
+        }
+        markExecutable()
+    }
+
+    private fun copyXcodeGen(source: File) {
+        val downloadRoot = downloadPath.get().asFile
+        source.copyRecursively(downloadRoot, overwrite = true)
+        markExecutable()
+    }
+
+    private fun findXcodeGen(): File {
+        val downloadRoot = downloadPath.get().asFile
+        return downloadRoot.walkTopDown()
+            .asSequence()
+            .toList()
+            .find { file -> file.absolutePath.endsWith("/bin/xcodegen") }
+            ?: throw GradleException("Cannot find `xcodegen` binary in $downloadRoot")
+    }
+
+    private fun markExecutable() {
+        val xcodeGenBinary = findXcodeGen()
+        require(xcodeGenBinary.setExecutable(true)) {
+            "Cannot mark $xcodeGenBinary executable."
+        }
+    }
+
+    fun xcodeGenBinary(): RegularFile {
+        return RegularFile {
+            findXcodeGen()
+        }
+    }
+}
diff --git a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
index c64e895..065e7ff 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
+++ b/benchmark/benchmark-darwin-gradle-plugin/src/main/kotlin/androidx/benchmark/darwin/gradle/GenerateXCodeProjectTask.kt
@@ -39,6 +39,10 @@
 
     @get:InputFile
     @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val xcodeGenPath: RegularFileProperty
+
+    @get:InputFile
+    @get:PathSensitive(PathSensitivity.RELATIVE)
     abstract val yamlFile: RegularFileProperty
 
     @get:Input
@@ -52,6 +56,7 @@
 
     @TaskAction
     fun buildXCodeProject() {
+        val xcodeGen = xcodeGenPath.get().asFile
         val outputFile = xcProjectPath.get().asFile
         if (outputFile.exists()) {
             require(outputFile.deleteRecursively()) {
@@ -59,7 +64,7 @@
             }
         }
         val args = listOf(
-            "xcodegen",
+            xcodeGen.absolutePath,
             "--spec",
             yamlFile.get().asFile.absolutePath,
             "--project",
diff --git a/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt b/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
index 873f105..b376c11 100644
--- a/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-junit4/api/public_plus_experimental_current.txt
@@ -19,5 +19,16 @@
     method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
   }
 
+  @androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi public final class PerfettoTraceRule implements org.junit.rules.TestRule {
+    ctor public PerfettoTraceRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing, optional kotlin.jvm.functions.Function1<? super androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback);
+    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
+    method public boolean getEnableAppTagTracing();
+    method public boolean getEnableUserspaceTracing();
+    method public kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? getTraceCallback();
+    property public final boolean enableAppTagTracing;
+    property public final boolean enableUserspaceTracing;
+    property public final kotlin.jvm.functions.Function1<androidx.benchmark.perfetto.PerfettoTrace,kotlin.Unit>? traceCallback;
+  }
+
 }
 
diff --git a/benchmark/benchmark-junit4/api/restricted_current.ignore b/benchmark/benchmark-junit4/api/restricted_current.ignore
index 7b0440e..b2f8308 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.ignore
+++ b/benchmark/benchmark-junit4/api/restricted_current.ignore
@@ -1,3 +1,3 @@
 // Baseline format: 1.0
-RemovedMethod: androidx.benchmark.junit4.PerfettoRule#PerfettoRule():
-    Removed constructor androidx.benchmark.junit4.PerfettoRule()
+RemovedClass: androidx.benchmark.junit4.PerfettoRule:
+    Removed class androidx.benchmark.junit4.PerfettoRule
diff --git a/benchmark/benchmark-junit4/api/restricted_current.txt b/benchmark/benchmark-junit4/api/restricted_current.txt
index e6624c1..c2d8056 100644
--- a/benchmark/benchmark-junit4/api/restricted_current.txt
+++ b/benchmark/benchmark-junit4/api/restricted_current.txt
@@ -20,14 +20,5 @@
     method public static inline void measureRepeated(androidx.benchmark.junit4.BenchmarkRule, kotlin.jvm.functions.Function1<? super androidx.benchmark.junit4.BenchmarkRule.Scope,kotlin.Unit> block);
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PerfettoRule implements org.junit.rules.TestRule {
-    ctor public PerfettoRule(optional boolean enableAppTagTracing, optional boolean enableUserspaceTracing);
-    method public org.junit.runners.model.Statement apply(org.junit.runners.model.Statement base, org.junit.runner.Description description);
-    method public boolean getEnableAppTagTracing();
-    method public boolean getEnableUserspaceTracing();
-    property public final boolean enableAppTagTracing;
-    property public final boolean enableUserspaceTracing;
-  }
-
 }
 
diff --git a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt b/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt
deleted file mode 100644
index 37934ad..0000000
--- a/benchmark/benchmark-junit4/src/androidTest/java/androidx/benchmark/junit4/PerfettoRuleTest.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.junit4
-
-import android.os.Build
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SmallTest
-import androidx.testutils.verifyWithPolling
-import androidx.tracing.Trace
-import androidx.tracing.trace
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest // recording is expensive
-@RunWith(AndroidJUnit4::class)
-class PerfettoRuleTest {
-    @get:Rule
-    val perfettoRule = PerfettoRule()
-
-    @Ignore("b/217256936")
-    @Test
-    fun tracingEnabled() {
-        trace("PerfettoCaptureTest") {
-            val traceShouldBeEnabled = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
-            verifyTraceEnable(traceShouldBeEnabled)
-        }
-
-        // NOTE: ideally, we'd validate the output file, but it's difficult to assert the
-        // behavior of the rule, since we can't really assert the result of a rule, which
-        // occurs after both @Test and @After
-    }
-}
-
-@SmallTest // not recording is cheap
-@RunWith(AndroidJUnit4::class)
-class PerfettoRuleControlTest {
-    @Test
-    fun tracingNotEnabled() {
-        verifyTraceEnable(false)
-    }
-}
-
-private fun verifyTraceEnable(enabled: Boolean) {
-    // We poll here, since we may need to wait for enable flags to propagate to apps
-    verifyWithPolling(
-        "Timeout waiting for Trace.isEnabled == $enabled",
-        periodMs = 50,
-        timeoutMs = 5000
-    ) {
-        Trace.isEnabled() == enabled
-    }
-}
\ No newline at end of file
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 9a33fe9..676ce86 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
@@ -212,7 +212,7 @@
             var userspaceTrace: perfetto.protos.Trace? = null
 
             val tracePath = PerfettoCaptureWrapper().record(
-                benchmarkName = uniqueName,
+                fileLabel = uniqueName,
                 appTagPackages = packages,
                 userspaceTracingPackage = null
             ) {
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
deleted file mode 100644
index 97fd9d6..0000000
--- a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoRule.kt
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.benchmark.junit4
-
-import androidx.annotation.RestrictTo
-import androidx.benchmark.perfetto.PerfettoCaptureWrapper
-import androidx.test.platform.app.InstrumentationRegistry
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Add this rule to record a Perfetto trace for each test on Q+ devices.
- *
- * Relies on either AGP's additionalTestOutputDir copying, or (in Jetpack CI),
- * `additionalTestOutputFile_***` copying.
- *
- * When invoked locally with Gradle, file will be copied to host path like the following:
- *
- * ```
- * out/androidx/benchmark/benchmark-macro/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/<deviceName>/androidx.mypackage.TestClass_testMethod.perfetto-trace
- * ```
- *
- * Note: if run from Studio, the file must be `adb pull`-ed manually, e.g.:
- * ```
- * > adb pull /storage/emulated/0/Android/data/androidx.mypackage.test/files/test_data/androidx.mypackage.TestClass_testMethod.trace
- * ```
- *
- * You can check logcat for messages tagged "PerfettoCapture:" for the path of each perfetto trace.
- * ```
- * > adb pull /storage/emulated/0/Android/data/mypackage.test/files/PerfettoCaptureTest.trace
- * ```
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public class PerfettoRule(
-    /**
-     * Pass false to disable android.os.Trace API tracing in this process
-     *
-     * Defaults to true.
-     */
-    val enableAppTagTracing: Boolean = true,
-    /**
-     * Pass true to enable userspace tracing (androidx.tracing.tracing-perfetto APIs)
-     *
-     * Defaults to false.
-     */
-    val enableUserspaceTracing: Boolean = false
-) : TestRule {
-    override fun apply(
-        base: Statement,
-        description: Description
-    ): Statement = object : Statement() {
-        override fun evaluate() {
-            val thisPackage = InstrumentationRegistry.getInstrumentation().context.packageName
-            PerfettoCaptureWrapper().record(
-                benchmarkName = "${description.className}_${description.methodName}",
-                appTagPackages = if (enableAppTagTracing) listOf(thisPackage) else emptyList(),
-                userspaceTracingPackage = if (enableUserspaceTracing) thisPackage else null
-            ) {
-                base.evaluate()
-            }
-        }
-    }
-}
diff --git a/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
new file mode 100644
index 0000000..a39994d
--- /dev/null
+++ b/benchmark/benchmark-junit4/src/main/java/androidx/benchmark/junit4/PerfettoTraceRule.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.junit4
+
+import android.os.Build
+import androidx.benchmark.InstrumentationResults
+import androidx.benchmark.Outputs
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Add this rule to record a Perfetto trace for each test on Android Lollipop (API 21)+ devices.
+ *
+ * Captured traces can be observed through any of:
+ * * Android Studio trace linking under `Benchmark` in test output tab
+ * * The optional `traceCallback` parameter
+ * * Android Gradle defining and pulling the file via additionalTestOutputDir.
+ *
+ * When invoked via Gradle, files will be copied to host path like the following:
+ * ```
+ * out/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected/<deviceName>/androidx.mypackage.TestClass_testMethod.perfetto-trace
+ * ```
+ *
+ * You can additionally check logcat for messages tagged "PerfettoCapture:" for the path of each
+ * perfetto trace.
+ * ```
+ * > adb pull /storage/emulated/0/Android/data/mypackage.test/files/PerfettoCaptureTest.trace
+ * ```
+ */
+@ExperimentalPerfettoCaptureApi
+class PerfettoTraceRule(
+    /**
+     * Pass false to disable android.os.Trace API tracing in this process
+     *
+     * Defaults to true.
+     */
+    val enableAppTagTracing: Boolean = true,
+    /**
+     * Pass true to enable userspace tracing (androidx.tracing.tracing-perfetto APIs)
+     *
+     * Defaults to false.
+     */
+    val enableUserspaceTracing: Boolean = false,
+
+    /**
+     * Callback for each captured trace.
+     */
+    val traceCallback: ((PerfettoTrace) -> Unit)? = null
+) : TestRule {
+    override fun apply(
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        base: Statement,
+        @Suppress("InvalidNullabilityOverride") // JUnit missing annotations
+        description: Description
+    ): Statement = object : Statement() {
+        override fun evaluate() {
+            val thisPackage = InstrumentationRegistry.getInstrumentation().context.packageName
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                val label = "${description.className}_${description.methodName}"
+                PerfettoTrace.record(
+                    fileLabel = label,
+                    appTagPackages = if (enableAppTagTracing) listOf(thisPackage) else emptyList(),
+                    userspaceTracingPackage = if (enableUserspaceTracing) thisPackage else null,
+                    traceCallback = {
+                        val relativePath = Outputs.relativePathFor(it.path)
+                            .replace("(", "\\(")
+                            .replace(")", "\\)")
+                        InstrumentationResults.instrumentationReport {
+                            ideSummaryRecord(
+                                // Can't link, simply print path
+                                summaryV1 = "Trace written to device at ${it.path}",
+                                // Link the trace within Studio
+                                summaryV2 = "[$label Trace](file://$relativePath)"
+                            )
+                        }
+                        traceCallback?.invoke(it)
+                    }
+                ) {
+                    base.evaluate()
+                }
+            } else {
+                base.evaluate()
+            }
+        }
+    }
+}
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index 69c6e99..301070a 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -99,6 +99,58 @@
   public final class MetricResultExtensionsKt {
   }
 
+  @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategory {
+    method public static androidx.benchmark.macro.PowerCategory valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.benchmark.macro.PowerCategory[] values();
+    enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
+    enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
+  }
+
+  @androidx.benchmark.macro.ExperimentalMetricApi public enum PowerCategoryDisplayLevel {
+    method public static androidx.benchmark.macro.PowerCategoryDisplayLevel valueOf(String name) throws java.lang.IllegalArgumentException;
+    method public static androidx.benchmark.macro.PowerCategoryDisplayLevel[] values();
+    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
+    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
+  }
+
+  @RequiresApi(29) @androidx.benchmark.macro.ExperimentalMetricApi public final class PowerMetric extends androidx.benchmark.macro.Metric {
+    ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
+    method public static androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
+    method public static androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+    method public static androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+    field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
+  }
+
+  public static final class PowerMetric.Companion {
+    method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
+    method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+    method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
+  }
+
+  public abstract static sealed class PowerMetric.Type {
+    method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
+    method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
+    property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
+  }
+
+  public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
+    ctor public PowerMetric.Type.Battery();
+  }
+
+  public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
+    ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
+  }
+
+  public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
+    ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
+  }
+
   public enum StartupMode {
     method public static androidx.benchmark.macro.StartupMode valueOf(String name) throws java.lang.IllegalArgumentException;
     method public static androidx.benchmark.macro.StartupMode[] values();
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index 9f3161b..aa128ff 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -97,55 +97,6 @@
   public final class MetricResultExtensionsKt {
   }
 
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public enum PowerCategory {
-    method public static androidx.benchmark.macro.PowerCategory valueOf(String name) throws java.lang.IllegalArgumentException;
-    method public static androidx.benchmark.macro.PowerCategory[] values();
-    enum_constant public static final androidx.benchmark.macro.PowerCategory CPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory DISPLAY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPS;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory GPU;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MACHINE_LEARNING;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory MEMORY;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory NETWORK;
-    enum_constant public static final androidx.benchmark.macro.PowerCategory UNCATEGORIZED;
-  }
-
-  @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public enum PowerCategoryDisplayLevel {
-    method public static androidx.benchmark.macro.PowerCategoryDisplayLevel valueOf(String name) throws java.lang.IllegalArgumentException;
-    method public static androidx.benchmark.macro.PowerCategoryDisplayLevel[] values();
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel BREAKDOWN;
-    enum_constant public static final androidx.benchmark.macro.PowerCategoryDisplayLevel TOTAL;
-  }
-
-  @RequiresApi(29) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PowerMetric extends androidx.benchmark.macro.Metric {
-    ctor public PowerMetric(androidx.benchmark.macro.PowerMetric.Type type);
-    field public static final androidx.benchmark.macro.PowerMetric.Companion Companion;
-  }
-
-  public static final class PowerMetric.Companion {
-    method public androidx.benchmark.macro.PowerMetric.Type.Battery Battery();
-    method public androidx.benchmark.macro.PowerMetric.Type.Energy Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-    method public androidx.benchmark.macro.PowerMetric.Type.Power Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> categories);
-  }
-
-  public abstract static sealed class PowerMetric.Type {
-    method public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> getCategories();
-    method public final void setCategories(java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel>);
-    property public final java.util.Map<androidx.benchmark.macro.PowerCategory,androidx.benchmark.macro.PowerCategoryDisplayLevel> categories;
-  }
-
-  public static final class PowerMetric.Type.Battery extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Battery();
-  }
-
-  public static final class PowerMetric.Type.Energy extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Energy(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> energyCategories);
-  }
-
-  public static final class PowerMetric.Type.Power extends androidx.benchmark.macro.PowerMetric.Type {
-    ctor public PowerMetric.Type.Power(optional java.util.Map<androidx.benchmark.macro.PowerCategory,? extends androidx.benchmark.macro.PowerCategoryDisplayLevel> powerCategories);
-  }
-
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class PowerRail {
     method public boolean hasMetrics(optional boolean throwOnMissingMetrics);
     field public static final androidx.benchmark.macro.PowerRail INSTANCE;
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 2ca3ad2..ccf5a4c 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -65,6 +65,7 @@
     implementation(libs.testUiautomator)
     implementation(libs.wireRuntime)
 
+    androidTestImplementation(project(":benchmark:benchmark-junit4"))
     androidTestImplementation(project(":internal-testutils-ktx"))
     androidTestImplementation("androidx.activity:activity-ktx:1.3.1")
     androidTestImplementation(project(":tracing:tracing-perfetto-common"))
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
index e1f888a..f3b0e7e 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
@@ -21,6 +21,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import kotlin.test.assertFailsWith
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -28,7 +29,6 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
-import kotlin.test.assertFailsWith
 
 @Suppress("DEPRECATION")
 @RunWith(AndroidJUnit4::class)
@@ -37,8 +37,8 @@
     private val vmRunningInterpretedOnly: Boolean
 
     init {
-        val getProp = Shell.executeCommand("getprop dalvik.vm.extra-opts")
-        vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
+        vmRunningInterpretedOnly = Shell.getprop("dalvik.vm.extra-opts")
+            .contains("-Xusejit:false")
     }
 
     @SdkSuppress(minSdkVersion = 24)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index 3fc5027..aeec388 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -218,7 +218,9 @@
     private fun validateShaderCache(empty: Boolean, packageName: String) {
         val path = MacrobenchmarkScope.getShaderCachePath(packageName)
         println("validating shader path $path")
-        val fileCount = Shell.executeScript("find $path -type f | wc -l").trim().toInt()
+        val fileCount = Shell.executeScriptCaptureStdout("find $path -type f | wc -l")
+            .trim()
+            .toInt()
         if (empty) {
             assertEquals(0, fileCount)
         } else {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
new file mode 100644
index 0000000..4b2a453
--- /dev/null
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PerfettoTraceRuleTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.macro.perfetto.PerfettoTraceProcessor
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
+import androidx.benchmark.perfetto.PerfettoHelper
+import androidx.benchmark.perfetto.PerfettoTrace
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import androidx.tracing.trace
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.runner.RunWith
+import org.junit.runners.model.Statement
+
+/**
+ * NOTE: This test is in `benchmark-macro` since validating trace content requires
+ * `benchmark-macro`, which has a minAPI of 23, and benchmark-junit4 can't work with that.
+ *
+ * This should be moved to `benchmark-junit` once trace validation can be done in its tests.
+ */
+@LargeTest // recording is expensive
+@OptIn(ExperimentalPerfettoCaptureApi::class)
+@RunWith(AndroidJUnit4::class)
+class PerfettoTraceRuleTest {
+    companion object {
+        const val UNIQUE_SLICE_NAME = "PerfettoRuleTestUnique"
+    }
+    var trace: PerfettoTrace? = null
+
+    // wrap the perfetto rule with another which consumes + validates the trace
+    @get:Rule
+    val rule: RuleChain = RuleChain.outerRule { base, _ ->
+        object : Statement() {
+            override fun evaluate() {
+                base.evaluate()
+                if (PerfettoHelper.isAbiSupported()) {
+                    assertNotNull(trace)
+                    val sliceNameInstances = PerfettoTraceProcessor.runServer(trace!!.path) {
+                        querySlices(UNIQUE_SLICE_NAME)
+                    }.map { slice -> slice.name }
+                    assertEquals(listOf(UNIQUE_SLICE_NAME), sliceNameInstances)
+                }
+            }
+        }
+    }.around(
+        PerfettoTraceRule {
+            trace = it
+        }
+    )
+
+    @Test
+    fun simple() {
+        trace(UNIQUE_SLICE_NAME) {}
+    }
+
+    @Test(expected = IllegalStateException::class)
+    fun exception() {
+        // trace works even if test throws
+        trace(UNIQUE_SLICE_NAME) {}
+        throw IllegalStateException()
+    }
+}
\ No newline at end of file
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
index d741589..329b736 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/PowerMetricTest.kt
@@ -24,6 +24,7 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 
+@OptIn(ExperimentalMetricApi::class)
 class PowerMetricTest {
     private val captureInfo = Metric.CaptureInfo(
         31,
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
index b2ca1b9..b3561e8 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/StartupTimingMetricTest.kt
@@ -281,7 +281,7 @@
     val metric = StartupTimingMetric()
     metric.configure(packageName)
     val tracePath = PerfettoCaptureWrapper().record(
-        benchmarkName = packageName,
+        fileLabel = packageName,
         // note - packageName may be this package, so we convert to set then list to make unique
         // and on API 23 and below, we use reflection to trace instead within this process
         appTagPackages = if (Build.VERSION.SDK_INT >= 24 && packageName != Packages.TEST) {
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
index ea8984b..eb91d3c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/BatteryDischargeQueryTest.kt
@@ -14,8 +14,11 @@
  * limitations under the License.
  */
 
+@file:OptIn(ExperimentalMetricApi::class)
+
 package androidx.benchmark.macro.perfetto
 
+import androidx.benchmark.macro.ExperimentalMetricApi
 import androidx.benchmark.macro.PowerMetric
 import androidx.benchmark.macro.createTempFileFromAsset
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index b9757a8..64ab132 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -80,7 +80,7 @@
         assertPackageAlive(false)
 
         // clean target process app data to ensure a clean start
-        Shell.executeCommand("pm clear $targetPackage").let { response ->
+        Shell.executeScriptCaptureStdout("pm clear $targetPackage").let { response ->
             assertThat(response).matches("Success\r?\n")
         }
     }
@@ -206,12 +206,8 @@
                     tmpLibFile,
                     Outputs.dirUsableByAppAndShell
                 ) { tmpFile, dstFile ->
-                    Shell.executeCommand("mkdir -p ${dstFile.parentFile!!.path}").also { response ->
-                        assertThat(response).isEmpty()
-                    }
-                    Shell.executeCommand("mv ${tmpFile.path} ${dstFile.path}").also { response ->
-                        assertThat(response).isEmpty()
-                    }
+                    Shell.executeScriptSilent("mkdir -p ${dstFile.parentFile!!.path}")
+                    Shell.executeScriptSilent("mv ${tmpFile.path} ${dstFile.path}")
                 }
             }
         }
@@ -229,7 +225,7 @@
                     }
                 }.toMap()
             },
-            Shell::executeCommand
+            Shell::executeScriptCaptureStdout
         )
 
         /** perform a handshake using [androidx.tracing.perfetto.PerfettoHandshake] */
@@ -280,7 +276,7 @@
         val handshake = PerfettoHandshake(
             "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
             parseJsonMap = { emptyMap() },
-            Shell::executeCommand
+            Shell::executeScriptCaptureStdout
         )
 
         // try
@@ -305,7 +301,7 @@
         val handshake = PerfettoHandshake(
             targetPackage,
             parseJsonMap = { throw IllegalArgumentException(parsingException) },
-            Shell::executeCommand
+            Shell::executeScriptCaptureStdout
         )
 
         handshake.enableTracing(null).also { response ->
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt
index 85fc795..d074851 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessorTest.kt
@@ -41,7 +41,7 @@
     fun shellPath() {
         assumeTrue(isAbiSupported())
         val shellPath = PerfettoTraceProcessor.shellPath
-        val out = Shell.executeCommand("$shellPath --version")
+        val out = Shell.executeScriptCaptureStdout("$shellPath --version")
         assertTrue(
             "expect to get Perfetto version string, saw: $out",
             out.contains("Perfetto v")
@@ -145,7 +145,8 @@
         // This method will return true if the server status endpoint returns 200 (that is also
         // the only status code being returned).
         fun isRunning(): Boolean = try {
-            with(URL("http://localhost:10555/").openConnection() as HttpURLConnection) {
+            val url = URL("http://localhost:${PerfettoTraceProcessor.PORT}/")
+            with(url.openConnection() as HttpURLConnection) {
                 return@with responseCode == 200
             }
         } catch (e: ConnectException) {
@@ -155,7 +156,7 @@
         // Check server is not running
         assertTrue(!isRunning())
 
-        PerfettoTraceProcessor.runServer(httpServerPort = 10555) {
+        PerfettoTraceProcessor.runServer {
 
             // Check server is running
             assertTrue(isRunning())
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
index 74e74ee..a86691c 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PowerQueryTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.benchmark.macro.perfetto
 
+import androidx.benchmark.macro.ExperimentalMetricApi
 import androidx.benchmark.macro.PowerCategory
 import androidx.benchmark.macro.PowerMetric.Companion.MEASURE_BLOCK_SECTION_NAME
 import androidx.benchmark.macro.createTempFileFromAsset
@@ -28,6 +29,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+@OptIn(ExperimentalMetricApi::class)
 @SdkSuppress(minSdkVersion = 29)
 @RunWith(AndroidJUnit4::class)
 @SmallTest
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index 749f23e..89c624e 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -160,20 +160,13 @@
  */
 @RequiresApi(33)
 private fun extractProfile(packageName: String): String {
-    val dumpResult = Shell.executeScriptWithStderr(
+    Shell.executeScriptSilent(
         "pm dump-profiles --dump-classes-and-methods $packageName"
     )
-    check(dumpResult.isBlank()) {
-        "Expected no stdout/stderr from pm dump-profiles," +
-            " saw '${dumpResult.stdout}', '${dumpResult.stderr}'"
-    }
     val fileName = "$packageName-primary.prof.txt"
-    val mvResult = Shell.executeScriptWithStderr(
+    Shell.executeScriptSilent(
         "mv /data/misc/profman/$fileName ${Outputs.dirUsableByAppAndShell}/"
     )
-    check(mvResult.isBlank()) {
-        "Expected no stdout/stderr from mv, saw '${mvResult.stdout}', '${mvResult.stderr}'"
-    }
 
     val rawRuleOutput = File(Outputs.dirUsableByAppAndShell, fileName)
     try {
@@ -194,7 +187,7 @@
     // The path to the primary profile
     val currentProfile = "/data/misc/profiles/cur/0/$packageName/primary.prof"
     Log.d(TAG, "Reference profile location: $referenceProfile")
-    val pathResult = Shell.executeScript("pm path $packageName")
+    val pathResult = Shell.executeScriptCaptureStdout("pm path $packageName")
     // The result looks like: `package: <result>`
     val apkPath = pathResult.substringAfter("package:").trim()
     Log.d(TAG, "APK Path: $apkPath")
@@ -210,7 +203,7 @@
     // The `current` path is eventually merged  into the `ref` path after background dexopt.
     for (currentPath in pathOptions) {
         Log.d(TAG, "Using profile location: $currentPath")
-        val profile = Shell.executeScript(
+        val profile = Shell.executeScriptCaptureStdout(
             "profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath"
         )
         if (profile.isNotBlank()) {
@@ -313,7 +306,7 @@
         // so we just specify emulator and hope there's only one
         "-e "
     } else {
-        val getpropOutput = Shell.executeScriptWithStderr("getprop ro.serialno")
+        val getpropOutput = Shell.executeScriptCaptureStdoutStderr("getprop ro.serialno")
         if (getpropOutput.stdout.isBlank() || getpropOutput.stderr.isNotBlank()) {
             "" // failed to get serial
         } else {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 285827b..b02110c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -28,6 +28,7 @@
 import androidx.benchmark.macro.CompilationMode.Ignore
 import androidx.benchmark.macro.CompilationMode.None
 import androidx.benchmark.macro.CompilationMode.Partial
+import androidx.benchmark.userspaceTrace
 import androidx.profileinstaller.ProfileInstallReceiver
 import org.junit.AssumptionViolatedException
 
@@ -79,6 +80,33 @@
         if (Build.VERSION.SDK_INT >= 24) {
             if (Arguments.enableCompilation) {
                 Log.d(TAG, "Resetting $packageName")
+                // The compilation mode chooses whether a reset is required or not.
+                // Currently the only compilation mode that does not perform a reset is
+                // CompilationMode.Ignore.
+                if (shouldReset()) {
+                    // It's not possible to reset the compilation profile on `user` builds.
+                    // The flag `enablePackageReset` can be set to `true` on `userdebug` builds in
+                    // order to speed-up the profile reset. When set to false, reset is performed
+                    // uninstalling and reinstalling the app.
+                    if (Shell.isSessionRooted()) {
+                        // Package reset enabled
+                        Log.d(TAG, "Re-compiling $packageName")
+                        // cmd package compile --reset returns a "Success" or a "Failure" to stdout.
+                        // Rather than rely on exit codes which are not always correct, we
+                        // specifically look for the work "Success" in stdout to make sure reset
+                        // actually happened.
+                        val output = Shell.executeScriptCaptureStdout(
+                            "cmd package compile --reset $packageName"
+                        )
+                        check(output.trim() == "Success") {
+                            "Unable to recompile $packageName ($output)"
+                        }
+                    } else {
+                        // User builds. Kick off a full uninstall-reinstall
+                        Log.d(TAG, "Reinstalling $packageName")
+                        reinstallPackage(packageName)
+                    }
+                }
                 // Write skip file to stop profile installer from interfering with the benchmark
                 writeProfileInstallerSkipFile(packageName, killProcessBlock = killProcessBlock)
                 compileImpl(packageName, killProcessBlock, warmupBlock)
@@ -88,6 +116,42 @@
         }
     }
 
+    // This is a more expensive when compared to `compile --reset`.
+    private fun reinstallPackage(packageName: String) {
+        userspaceTrace("reinstallPackage") {
+            val packagePath = Shell.executeScriptCaptureStdout("pm path $packageName")
+            // The result looks like: `package: <result>`
+            val apkPath = packagePath.substringAfter("package:").trim()
+            // Copy the APK to /data/local/temp
+            val tempApkPath = "/data/local/tmp/$packageName-${System.currentTimeMillis()}.apk"
+            Log.d(TAG, "Copying APK to $tempApkPath")
+            val result = Shell.executeScriptCaptureStdout(
+                "cp $apkPath $tempApkPath"
+            )
+            try {
+                // Uninstall package
+                // This is what effectively clears the ART profiles
+                Log.d(TAG, "Uninstalling $packageName")
+                var output = Shell.executeScriptCaptureStdout("pm uninstall $packageName")
+                check(output.trim() == "Success") {
+                    "Unable to uninstall $packageName ($result)"
+                }
+                // Install the APK from /data/local/tmp
+                Log.d(TAG, "Installing $packageName")
+                // Provide a `-t` argument to `pm install` to ensure test packages are
+                // correctly installed. (b/231294733)
+                output = Shell.executeScriptCaptureStdout("pm install -t $tempApkPath")
+                check(output.trim() == "Success") {
+                    "Unable to install $packageName ($result)"
+                }
+            } finally {
+                // Cleanup the temporary APK
+                Log.d(TAG, "Deleting $tempApkPath")
+                Shell.executeScriptSilent("rm $tempApkPath")
+            }
+        }
+    }
+
     /**
      * Writes a skip file via a [ProfileInstallReceiver] broadcast, so profile installation
      * does not interfere with benchmarks.
@@ -114,6 +178,9 @@
         warmupBlock: () -> Unit
     )
 
+    @RequiresApi(24)
+    internal abstract fun shouldReset(): Boolean
+
     /**
      * No pre-compilation - a compilation profile reset is performed and the entire app will be
      * allowed to Just-In-Time compile as it runs.
@@ -131,8 +198,10 @@
             killProcessBlock: () -> Unit,
             warmupBlock: () -> Unit
         ) {
-            cmdPackageCompile(packageName, "verify")
+            // nothing to do!
         }
+
+        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -152,6 +221,8 @@
         ) {
             // Do nothing.
         }
+
+        override fun shouldReset(): Boolean = false
     }
 
     /**
@@ -248,7 +319,7 @@
                             TAG,
                             "Unable to saveProfile with profileinstaller ($saveResult), trying kill"
                         )
-                        val response = Shell.executeScriptWithStderr(
+                        val response = Shell.executeScriptCaptureStdoutStderr(
                             "killall -s SIGUSR1 $packageName"
                         )
                         check(response.isBlank()) {
@@ -262,6 +333,8 @@
                 cmdPackageCompile(packageName, "speed-profile")
             }
         }
+
+        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -286,6 +359,8 @@
             }
             // Noop on older versions: apps are fully compiled at install time on API 23 and below
         }
+
+        override fun shouldReset(): Boolean = true
     }
 
     /**
@@ -309,6 +384,8 @@
         ) {
             // Nothing to do - handled externally
         }
+
+        override fun shouldReset(): Boolean = true
     }
 
     companion object {
@@ -340,7 +417,10 @@
 
         @RequiresApi(24)
         internal fun cmdPackageCompile(packageName: String, compileArgument: String) {
-            Shell.executeCommand("cmd package compile -f -m $compileArgument $packageName")
+            val stdout = Shell.executeScriptCaptureStdout(
+                "cmd package compile -f -m $compileArgument $packageName"
+            )
+            check(stdout.trim() == "Success")
         }
     }
 }
@@ -354,7 +434,7 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
 fun CompilationMode.isSupportedWithVmSettings(): Boolean {
-    val getProp = Shell.executeCommand("getprop dalvik.vm.extra-opts")
+    val getProp = Shell.getprop("dalvik.vm.extra-opts")
     val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
 
     // true if requires interpreted, false otherwise
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index ed2982f..689cf89 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -208,9 +208,9 @@
                     setupBlock(scope)
                 }
 
+                val iterString = iteration.toString().padStart(3, '0')
                 val tracePath = perfettoCollector.record(
-                    benchmarkName = uniqueName,
-                    iteration = iteration,
+                    fileLabel = "${uniqueName}_iter$iterString",
 
                     /**
                      * Prior to API 24, every package name was joined into a single setprop which
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
index 62f6d06..db37987 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkScope.kt
@@ -126,7 +126,7 @@
         Log.d(TAG, "Starting activity with command: $cmd")
 
         // executeShellScript used to access stderr, and avoid need to escape special chars like `;`
-        val result = Shell.executeScriptWithStderr(cmd)
+        val result = Shell.executeScriptCaptureStdoutStderr(cmd)
 
         val outputLines = result.stdout
             .split("\n")
@@ -192,7 +192,7 @@
                 // we use framestats here because it gives us not just frame counts, but actual
                 // timestamps for new activity starts. Frame counts would mostly work, but would
                 // have false positives if some window of the app is still animating/rendering.
-                Shell.executeCommand("dumpsys gfxinfo $processName framestats")
+                Shell.executeScriptCaptureStdout("dumpsys gfxinfo $processName framestats")
             }
             FrameStatsResult.parse(frameStatsOutput)
         }
@@ -246,10 +246,7 @@
             if (Shell.isSessionRooted()) {
                 // fall back to root approach
                 val path = getShaderCachePath(packageName)
-                val output = Shell.executeScriptWithStderr("find $path -type f | xargs rm")
-                check(output.isBlank()) {
-                    "Failed to delete shader cache directory $path, output $output"
-                }
+                Shell.executeScriptSilent("find $path -type f | xargs rm")
             } else {
                 throw IllegalStateException(dropError)
             }
@@ -264,14 +261,14 @@
      */
     @RequiresApi(31)
     private fun dropKernelPageCacheSetProp() {
-        val result = Shell.executeScriptWithStderr("setprop perf.drop_caches 3")
+        val result = Shell.executeScriptCaptureStdoutStderr("setprop perf.drop_caches 3")
         check(result.stdout.isEmpty() && result.stderr.isEmpty()) {
             "Failed to trigger drop cache via setprop: $result"
         }
         // Polling duration is very conservative, on Pixel 4L finishes in ~150ms
         repeat(50) {
             Thread.sleep(50)
-            when (val getPropResult = Shell.executeCommand("getprop perf.drop_caches").trim()) {
+            when (val getPropResult = Shell.getprop("perf.drop_caches")) {
                 "0" -> return // completed!
                 "3" -> {} // not completed, continue
                 else -> throw IllegalStateException(
@@ -297,7 +294,7 @@
         if (Build.VERSION.SDK_INT >= 31) {
             dropKernelPageCacheSetProp()
         } else {
-            val result = Shell.executeScript(
+            val result = Shell.executeScriptCaptureStdout(
                 "echo 3 > /proc/sys/vm/drop_caches && echo Success || echo Failure"
             ).trim()
             // Older user builds don't allow drop caches, should investigate workaround
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
index e8fac44..dfd14e1 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Metric.kt
@@ -400,7 +400,7 @@
  * This measurement is not available prior to API 29.
  */
 @RequiresApi(29)
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@ExperimentalMetricApi
 public class PowerMetric(
     private val type: Type
 ) : Metric() {
@@ -408,16 +408,19 @@
     companion object {
         internal const val MEASURE_BLOCK_SECTION_NAME = "measureBlock"
 
+        @JvmStatic
         fun Battery(): Type.Battery {
             return Type.Battery()
         }
 
+        @JvmStatic
         fun Energy(
             categories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()
         ): Type.Energy {
             return Type.Energy(categories)
         }
 
+        @JvmStatic
         fun Power(
             categories: Map<PowerCategory, PowerCategoryDisplayLevel> = emptyMap()
         ): Type.Power {
@@ -456,13 +459,13 @@
 
     internal override fun start() {
         if (type is Type.Battery) {
-            Shell.executeCommand("setprop power.battery_input.suspended true")
+            Shell.executeScriptSilent("setprop power.battery_input.suspended true")
         }
     }
 
     internal override fun stop() {
         if (type is Type.Battery) {
-            Shell.executeCommand("setprop power.battery_input.suspended false")
+            Shell.executeScriptSilent("setprop power.battery_input.suspended false")
         }
     }
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategory.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategory.kt
index f82567b..207953a 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategory.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategory.kt
@@ -16,9 +16,7 @@
 
 package androidx.benchmark.macro
 
-import androidx.annotation.RestrictTo
-
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@ExperimentalMetricApi
 enum class PowerCategory {
     CPU,
     DISPLAY,
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategoryDisplayLevel.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategoryDisplayLevel.kt
index 5655295..03060cf 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategoryDisplayLevel.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerCategoryDisplayLevel.kt
@@ -16,9 +16,7 @@
 
 package androidx.benchmark.macro
 
-import androidx.annotation.RestrictTo
-
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@ExperimentalMetricApi
 enum class PowerCategoryDisplayLevel {
     BREAKDOWN,
     TOTAL
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
index 864fa2a..69f0fb6 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/PowerRail.kt
@@ -43,7 +43,7 @@
      * @Throws UnsupportedOperationException if `hasException == true` and no rail metrics are found.
      */
     fun hasMetrics(throwOnMissingMetrics: Boolean = false): Boolean {
-        val output = Shell.executeCommand(DUMPSYS_POWERSTATS)
+        val output = Shell.executeScriptCaptureStdout(DUMPSYS_POWERSTATS)
         return hasMetrics(output, throwOnMissingMetrics)
     }
 
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
index 1e2eb91..dc8d53f 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/ProfileInstallBroadcast.kt
@@ -36,7 +36,9 @@
         // installed synchronously
         val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
         // Use an explicit broadcast given the app was force-stopped.
-        val result = Shell.executeCommand("am broadcast -a $action $packageName/$receiverName")
+        val result = Shell.executeScriptCaptureStdout(
+            "am broadcast -a $action $packageName/$receiverName"
+        )
             .substringAfter("Broadcast completed: result=")
             .trim()
             .toIntOrNull()
@@ -101,7 +103,7 @@
         val action = "androidx.profileinstaller.action.SKIP_FILE"
         val operationKey = "EXTRA_SKIP_FILE_OPERATION"
         val extras = "$operationKey $operation"
-        val result = Shell.executeCommand(
+        val result = Shell.executeScriptCaptureStdout(
             "am broadcast -a $action -e $extras $packageName/$receiverName"
         )
             .substringAfter("Broadcast completed: result=")
@@ -141,7 +143,9 @@
     fun saveProfile(packageName: String): String? {
         Log.d(TAG, "Profile Installer - Save Profile")
         val action = "androidx.profileinstaller.action.SAVE_PROFILE"
-        val result = Shell.executeCommand("am broadcast -a $action $packageName/$receiverName")
+        val result = Shell.executeScriptCaptureStdout(
+            "am broadcast -a $action $packageName/$receiverName"
+        )
             .substringAfter("Broadcast completed: result=")
             .trim()
             .toIntOrNull()
@@ -180,7 +184,7 @@
         val action = "androidx.profileinstaller.action.BENCHMARK_OPERATION"
         val operationKey = "EXTRA_BENCHMARK_OPERATION"
         val extras = "$operationKey $operation"
-        val result = Shell.executeCommand(
+        val result = Shell.executeScriptCaptureStdout(
             "am broadcast -a $action -e $extras $packageName/$receiverName"
         )
             .substringAfter("Broadcast completed: result=")
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
index 1284651..830a342 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PerfettoTraceProcessor.kt
@@ -30,11 +30,10 @@
  * Enables parsing perfetto traces on-device
  */
 @RestrictTo(LIBRARY_GROUP) // for internal benchmarking only
-class PerfettoTraceProcessor(httpServerPort: Int = DEFAULT_HTTP_SERVER_PORT) {
+class PerfettoTraceProcessor {
 
     companion object {
-        private const val TAG = "PerfettoTraceProcessor"
-        private const val DEFAULT_HTTP_SERVER_PORT = 9001
+        const val PORT = 9001
 
         /**
          * The actual [File] path to the `trace_processor_shell`.
@@ -53,14 +52,13 @@
          */
         fun <T> runServer(
             absoluteTracePath: String? = null,
-            httpServerPort: Int = DEFAULT_HTTP_SERVER_PORT,
             block: PerfettoTraceProcessor.() -> T
         ): T = userspaceTrace("PerfettoTraceProcessor#runServer") {
             var perfettoTraceProcessor: PerfettoTraceProcessor? = null
             try {
 
                 // Initializes the server process
-                perfettoTraceProcessor = PerfettoTraceProcessor(httpServerPort).startServer()
+                perfettoTraceProcessor = PerfettoTraceProcessor().startServer()
 
                 // Loads a trace if required
                 if (absoluteTracePath != null) {
@@ -77,7 +75,7 @@
         }
     }
 
-    private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer(httpServerPort)
+    private val perfettoHttpServer: PerfettoHttpServer = PerfettoHttpServer()
     private var traceLoaded = false
 
     private fun startServer(): PerfettoTraceProcessor =
@@ -166,7 +164,7 @@
      *
      * Note that sliceNames may include wildcard matches, such as `foo%`
      */
-    internal fun querySlices(
+    fun querySlices(
         vararg sliceNames: String
     ): List<Slice> {
         require(perfettoHttpServer.isRunning()) { "Perfetto trace_shell_process is not running." }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
index 089b5a1..0aca0d8 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/PowerQuery.kt
@@ -16,11 +16,13 @@
 
 package androidx.benchmark.macro.perfetto
 
+import androidx.benchmark.macro.ExperimentalMetricApi
 import androidx.benchmark.macro.PowerCategory
 import androidx.benchmark.macro.PowerMetric
 
 // We want to use android_powrails.sql, but cannot as they do not split into sections with slice
 
+@OptIn(ExperimentalMetricApi::class)
 internal object PowerQuery {
     private fun getFullQuery(slice: Slice) = """
         SELECT
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
index 4cc0db3..7ee5044 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/Slice.kt
@@ -16,9 +16,11 @@
 
 package androidx.benchmark.macro.perfetto
 
+import androidx.annotation.RestrictTo
 import androidx.benchmark.macro.perfetto.server.QueryResultIterator
 
-internal data class Slice(
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+data class Slice(
     val name: String,
     val ts: Long,
     val dur: Long
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
index 7c20eec..e268c30 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/perfetto/server/PerfettoHttpServer.kt
@@ -43,7 +43,7 @@
  * is based on the python one of the official repo:
  * https://github.com/google/perfetto/blob/master/python/perfetto/trace_processor/http.py
  */
-internal class PerfettoHttpServer(private val port: Int) {
+internal class PerfettoHttpServer {
 
     companion object {
         private const val HTTP_ADDRESS = "http://localhost"
@@ -59,6 +59,7 @@
         private const val TAG = "PerfettoHttpServer"
         private const val SERVER_START_TIMEOUT_MS = 5000
         private const val READ_TIMEOUT_SECONDS = 300000
+        private const val SERVER_PROCESS_NAME = "trace_processor_shell"
 
         private var shellScript: ShellScript? = null
 
@@ -72,8 +73,8 @@
             if (instance != null) {
                 return@synchronized instance
             }
-            val script =
-                """echo pid:$$ ; exec ${PerfettoTraceProcessor.shellPath} -D --http-port "$@" """
+            val script = "echo pid:$$ ; exec ${PerfettoTraceProcessor.shellPath} -D" +
+                " --http-port \"${PerfettoTraceProcessor.PORT}\" "
             instance = Shell.createShellScript(script)
             shellScript = instance
             instance
@@ -95,7 +96,7 @@
      *
      * @throws IllegalStateException if the server is not running by the end of the timeout.
      */
-    fun startServer() = userspaceTrace("PerfettoHttpServer#startServer port $port") {
+    fun startServer() = userspaceTrace("PerfettoHttpServer#startServer") {
         if (processId != null) {
             Log.w(TAG, "Tried to start a trace shell processor that is already running.")
             return@userspaceTrace
@@ -116,7 +117,7 @@
             )
         }
 
-        val shellScript = getOrCreateShellScript().start(port.toString())
+        val shellScript = getOrCreateShellScript().start()
 
         processId = shellScript
             .stdOutLineSequence()
@@ -130,13 +131,30 @@
             Thread.sleep(5)
             elapsed += 5
             if (elapsed >= SERVER_START_TIMEOUT_MS) {
-                throw IllegalStateException(
-                    """
+
+                // In the event that the instrumentation app cannot connect to the
+                // trace_processor_shell server, trying to read the full stderr may make the
+                // process hang. Here we check if the process is still running to determine if
+                // that's the case and throw the correct exception.
+
+                val processRunning =
+                    processId?.let { Shell.isProcessAlive(it, SERVER_PROCESS_NAME) } ?: false
+
+                if (processRunning) {
+                    throw IllegalStateException(
+                        """
+                        The instrumentation app cannot connect to the trace_processor_shell server.
+                        """.trimIndent()
+                    )
+                } else {
+                    throw IllegalStateException(
+                        """
                         Perfetto trace_processor_shell did not start correctly.
                         Process stderr:
                         ${shellScript.getOutputAndClose().stderr}
-                    """.trimIndent()
-                )
+                        """.trimIndent()
+                    )
+                }
             }
         }
         Log.i(TAG, "Perfetto trace processor shell server started (pid=$processId).")
@@ -145,19 +163,19 @@
     /**
      * Stops the server killing the associated process
      */
-    fun stopServer() = userspaceTrace("PerfettoHttpServer#stopServer port $port") {
+    fun stopServer() = userspaceTrace("PerfettoHttpServer#stopServer") {
         if (processId == null) {
             Log.w(TAG, "Tried to stop trace shell processor http server without starting it.")
             return@userspaceTrace
         }
-        Shell.executeCommand("kill -TERM $processId")
+        Shell.executeScriptSilent("kill -TERM $processId")
         Log.i(TAG, "Perfetto trace processor shell server stopped (pid=$processId).")
     }
 
     /**
      * Returns true whether the server is running, false otherwise.
      */
-    fun isRunning(): Boolean = userspaceTrace("PerfettoHttpServer#isRunning port $port") {
+    fun isRunning(): Boolean = userspaceTrace("PerfettoHttpServer#isRunning") {
         return@userspaceTrace try {
             val statusResult = status()
             return@userspaceTrace statusResult.api_version != null && statusResult.api_version > 0
@@ -243,7 +261,10 @@
         encodeBlock: ((OutputStream) -> Unit)?,
         decodeBlock: ((InputStream) -> T)
     ): T {
-        with(URL("$HTTP_ADDRESS:$port$url").openConnection() as HttpURLConnection) {
+        with(
+            URL("$HTTP_ADDRESS:${PerfettoTraceProcessor.PORT}$url")
+                .openConnection() as HttpURLConnection
+        ) {
             requestMethod = method
             readTimeout = READ_TIMEOUT_SECONDS
             setRequestProperty("Content-Type", contentType)
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
index f2bfd74..cc8012b 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoOverheadBenchmark.kt
@@ -17,8 +17,9 @@
 package androidx.benchmark.benchmark
 
 import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.PerfettoRule
+import androidx.benchmark.junit4.PerfettoTraceRule
 import androidx.benchmark.junit4.measureRepeated
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.tracing.Trace
@@ -33,8 +34,9 @@
     @get:Rule
     val benchmarkRule = BenchmarkRule()
 
+    @OptIn(ExperimentalPerfettoCaptureApi::class)
     @get:Rule
-    val perfettoRule = PerfettoRule()
+    val mPerfettoTraceRule = PerfettoTraceRule()
 
     /**
      * Empty baseline, no tracing. Expect similar results to [TrivialJavaBenchmark.nothing].
diff --git a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
index 7f0a639..18de45a 100644
--- a/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
+++ b/benchmark/benchmark/src/androidTest/java/androidx/benchmark/benchmark/PerfettoSdkOverheadBenchmark.kt
@@ -18,8 +18,9 @@
 
 import android.os.Build
 import androidx.benchmark.junit4.BenchmarkRule
-import androidx.benchmark.junit4.PerfettoRule
+import androidx.benchmark.junit4.PerfettoTraceRule
 import androidx.benchmark.junit4.measureRepeated
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.benchmark.perfetto.PerfettoCapture
 import androidx.benchmark.perfetto.PerfettoHelper.Companion.isAbiSupported
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -49,8 +50,9 @@
     @get:Rule
     val benchmarkRule = BenchmarkRule(packages = listOf(targetPackage))
 
+    @OptIn(ExperimentalPerfettoCaptureApi::class)
     @get:Rule
-    val perfettoRule = PerfettoRule()
+    val mPerfettoTraceRule = PerfettoTraceRule()
 
     private val testData = Array(50_000) { UUID.randomUUID().toString() }
 
diff --git a/benchmark/gradle-plugin/build.gradle b/benchmark/gradle-plugin/build.gradle
index 9bbf2cc..b7db163 100644
--- a/benchmark/gradle-plugin/build.gradle
+++ b/benchmark/gradle-plugin/build.gradle
@@ -27,7 +27,7 @@
 dependencies {
     implementation(findGradleKotlinDsl())
     implementation(gradleApi())
-    implementation("com.android.tools.build:gradle:7.0.0")
+    implementation("com.android.tools.build:gradle:7.3.0")
     implementation(libs.kotlinStdlib)
 
     testImplementation(gradleTestKit())
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
index 71e3524..f35b192 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/CompilationModeTest.kt
@@ -89,8 +89,8 @@
         }
 
     private fun getCompilationMode(): String {
-        val dump =
-            Shell.executeScriptWithStderr("cmd package dump $TARGET_PACKAGE_NAME").stdout.trim()
+        val dump = Shell.executeScriptCaptureStdoutStderr("cmd package dump $TARGET_PACKAGE_NAME")
+            .stdout.trim()
 
         // Find `Dexopt state:` line
         var firstMarkerFound = false
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
index 4f38392..325ddc9 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/PerfettoTraceProcessorBenchmark.kt
@@ -82,7 +82,6 @@
                 // Note that this is an arbitrary number and the default cannot be used because
                 // the macrobenchmark instance of the server is running at the same time.
                 PerfettoTraceProcessor.runServer(
-                    httpServerPort = 10555,
                     absoluteTracePath = traceFile.absolutePath,
                     block = block
                 )
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
index cd0992e..8917e0e 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialPowerBenchmark.kt
@@ -39,7 +39,7 @@
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = 29)
 @OptIn(ExperimentalMetricApi::class)
-class TrivialPowerBenchmark() {
+class TrivialPowerBenchmark {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
diff --git a/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
new file mode 100644
index 0000000..9954170
--- /dev/null
+++ b/buildSrc-tests/src/test/kotlin/androidx/build/LibraryVersionsServiceTest.kt
@@ -0,0 +1,243 @@
+/*
+ * 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.build
+
+import com.google.common.truth.Truth.assertThat
+import org.gradle.testfixtures.ProjectBuilder
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class LibraryVersionsServiceTest {
+    @get:Rule
+    val tempDir = TemporaryFolder()
+
+    @Test
+    fun basic() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.V1" }
+            G2 = { group = "g.g2"}
+        """.trimIndent()
+        )
+        assertThat(
+            service.libraryGroups["G1"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "g.g1", atomicGroupVersion = Version("1.2.3")
+            )
+        )
+        assertThat(
+            service.libraryGroups["G2"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "g.g2", atomicGroupVersion = null
+            )
+        )
+    }
+
+    @Test
+    fun withMultiplatformVersion() {
+        val toml = """
+            [versions]
+            V1 = "1.2.3"
+            V1_KMP = "1.2.3-dev05"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.V1", multiplatformGroupVersion = "versions.V1_KMP"}
+        """.trimIndent()
+        val dontUseKmpVersions = createLibraryVersionsService(toml)
+        assertThat(
+            dontUseKmpVersions.libraryGroups["G1"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "g.g1", atomicGroupVersion = Version("1.2.3")
+            )
+        )
+
+        val useKmpVersions =
+            createLibraryVersionsService(toml, useMultiplatformGroupVersions = true)
+        assertThat(
+            useKmpVersions.libraryGroups["G1"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "g.g1", atomicGroupVersion = Version("1.2.3-dev05")
+            )
+        )
+    }
+
+    @Test
+    fun customComposeVersions() {
+        val toml = """
+            [versions]
+            COMPOSE_V1 = "1.2.3"
+            [groups]
+            COMPOSE = { group = "androidx.compose.suffix", atomicGroupVersion = "versions.COMPOSE_V1" }
+        """.trimIndent()
+        val noCustomVersion = createLibraryVersionsService(toml)
+        assertThat(
+            noCustomVersion.libraryGroups["COMPOSE"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "androidx.compose.suffix", atomicGroupVersion = Version("1.2.3")
+            )
+        )
+        val customComposeVersion = createLibraryVersionsService(
+            toml, composeCustomGroup = "not.androidx.compose", composeCustomVersion = "1.1.1"
+        )
+        assertThat(
+            customComposeVersion.libraryGroups["COMPOSE"]
+        ).isEqualTo(
+            LibraryGroup(
+                group = "not.androidx.compose.suffix", atomicGroupVersion = Version("1.1.1")
+            )
+        )
+    }
+
+    @Test
+    fun missingVersionReference() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.doesNotExist" }
+        """.trimIndent()
+        )
+        val result = runCatching {
+            service.libraryGroups["G1"]
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            "Group entry g.g1 specifies doesNotExist, but such version doesn't exist"
+        )
+    }
+
+    @Test
+    fun malformedVersionReference() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "v1" }
+        """.trimIndent()
+        )
+        val result = runCatching {
+            service.libraryGroups["G1"]
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            "Group entry atomicGroupVersion is expected to start with versions"
+        )
+    }
+
+    @Test
+    fun missingVersionReference_multiplatform() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.V1", multiplatformGroupVersion = "versions.doesNotExist2" }
+        """.trimIndent()
+        )
+        val result = runCatching {
+            service.libraryGroups["G1"]
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            "Group entry g.g1 specifies doesNotExist2, but such version doesn't exist"
+        )
+    }
+
+    @Test
+    fun malformedVersionReference_multiplatform() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", atomicGroupVersion = "versions.V1", multiplatformGroupVersion = "V1" }
+        """.trimIndent()
+        )
+        val result = runCatching {
+            service.libraryGroups["G1"]
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            "Group entry multiplatformGroupVersion is expected to start with versions"
+        )
+    }
+
+    @Test
+    fun atomicMultiplatformGroupWithoutAtomicGroup() {
+        val service = createLibraryVersionsService(
+            """
+            [versions]
+            V1 = "1.2.3"
+            [groups]
+            G1 = { group = "g.g1", multiplatformGroupVersion = "versions.V1" }
+            """.trimIndent()
+        )
+        val result = runCatching {
+            service.libraryGroups["G1"]
+        }
+        assertThat(
+            result.exceptionOrNull()
+        ).hasMessageThat().contains(
+            """
+            Cannot specify multiplatformGroupVersion for G1 without specifying an atomicGroupVersion
+            """.trimIndent()
+        )
+    }
+
+    private fun createLibraryVersionsService(
+        tomlFile: String,
+        composeCustomVersion: String? = null,
+        composeCustomGroup: String? = null,
+        useMultiplatformGroupVersions: Boolean = false
+    ): LibraryVersionsService {
+        val project = ProjectBuilder.builder().withProjectDir(tempDir.newFolder()).build()
+        val serviceProvider = project.gradle.sharedServices.registerIfAbsent(
+            "libraryVersionsService", LibraryVersionsService::class.java
+        ) { spec ->
+            spec.parameters.tomlFile = project.provider {
+                tomlFile
+            }
+            spec.parameters.composeCustomVersion = project.provider {
+                composeCustomVersion
+            }
+            spec.parameters.composeCustomGroup = project.provider {
+                composeCustomGroup
+            }
+            spec.parameters.useMultiplatformGroupVersions = project.provider {
+                useMultiplatformGroupVersions
+            }
+        }
+        return serviceProvider.get()
+    }
+}
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
index 30f599c..4099584 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXExtension.kt
@@ -41,6 +41,9 @@
         val content = project.providers.fileContents(toml)
         val composeCustomVersion = project.providers.environmentVariable("COMPOSE_CUSTOM_VERSION")
         val composeCustomGroup = project.providers.environmentVariable("COMPOSE_CUSTOM_GROUP")
+        val useMultiplatformVersions = project.provider {
+            Multiplatform.isKotlinNativeEnabled(project)
+        }
 
         val serviceProvider = project.gradle.sharedServices.registerIfAbsent(
             "libraryVersionsService",
@@ -49,6 +52,7 @@
             spec.parameters.tomlFile = content.asText
             spec.parameters.composeCustomVersion = composeCustomVersion
             spec.parameters.composeCustomGroup = composeCustomGroup
+            spec.parameters.useMultiplatformGroupVersions = useMultiplatformVersions
         }
         LibraryGroups = serviceProvider.get().libraryGroups
         LibraryVersions = serviceProvider.get().libraryVersions
@@ -209,14 +213,6 @@
     var runApiTasks: RunApiTasks = RunApiTasks.Auto
         get() = if (field == RunApiTasks.Auto && type != LibraryType.UNSET) type.checkApi else field
     var type: LibraryType = LibraryType.UNSET
-        set(value) {
-            // don't disable multiplatform if it's already enabled, because sometimes it's enabled
-            // through flags and we don't want setting `type =` to disable it accidentally.
-            if (value.shouldEnableMultiplatform()) {
-                multiplatform = true
-            }
-            field = value
-        }
     var failOnDeprecationWarnings = true
 
     var legacyDisableKotlinStrictApiMode = false
@@ -225,15 +221,6 @@
 
     var bypassCoordinateValidation = false
 
-    /**
-     * Whether this project uses KMP.
-     */
-    private var multiplatform: Boolean = false
-        set(value) {
-            Multiplatform.setEnabledForProject(project, value)
-            field = value
-        }
-
     fun shouldEnforceKotlinStrictApiMode(): Boolean {
         return !legacyDisableKotlinStrictApiMode &&
             shouldConfigureApiTasks()
@@ -262,5 +249,3 @@
     var name: String? = null
     var url: String? = null
 }
-
-private fun LibraryType.shouldEnableMultiplatform() = this is LibraryType.KmpLibrary
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
index 4e1d108..01286f7 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXGradleProperties.kt
@@ -130,6 +130,12 @@
  */
 const val ALLOW_MISSING_LINT_CHECKS_PROJECT = "androidx.allow.missing.lint"
 
+/**
+ * If set to a uri, this is the location that will be used to download `xcodegen` when running
+ * Darwin benchmarks.
+ */
+const val XCODEGEN_DOWNLOAD_URI = "androidx.benchmark.darwin.xcodeGenDownloadUri"
+
 val ALL_ANDROIDX_PROPERTIES = setOf(
     ALTERNATIVE_PROJECT_URL,
     VERSION_EXTRA_CHECK_ENABLED,
@@ -155,7 +161,8 @@
     PROFILE_YOURKIT_AGENT_PATH,
     KMP_GITHUB_BUILD,
     ENABLED_KMP_TARGET_PLATFORMS,
-    ALLOW_MISSING_LINT_CHECKS_PROJECT
+    ALLOW_MISSING_LINT_CHECKS_PROJECT,
+    XCODEGEN_DOWNLOAD_URI
 )
 
 /**
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index 15d46bf..d82baf1 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -145,6 +145,7 @@
         project.configureProjectStructureValidation(extension)
         project.configureProjectVersionValidation(extension)
         project.registerProjectOrArtifact()
+        project.addCreateLibraryBuildInfoFileTasks(extension)
 
         project.configurations.create("samples")
         project.validateMultiplatformPluginHasNotBeenApplied()
@@ -332,6 +333,7 @@
                 configureAndroidLibraryWithMultiplatformPluginOptions()
             }
             project.configureKmpTests()
+            project.configureSourceJarForMultiplatform()
         }
     }
 
@@ -437,7 +439,6 @@
         project.configurePublicResourcesStub(libraryExtension)
         project.configureSourceJarForAndroid(libraryExtension)
         project.configureVersionFileWriter(libraryExtension, androidXExtension)
-        project.addCreateLibraryBuildInfoFileTasks(androidXExtension)
         project.configureJavaCompilationWarnings(androidXExtension)
 
         project.configureDependencyVerification(androidXExtension) { taskProvider ->
@@ -501,8 +502,6 @@
             }
         }
 
-        project.addCreateLibraryBuildInfoFileTasks(extension)
-
         // Standard lint, docs, and Metalava configuration for AndroidX projects.
         project.configureNonAndroidProjectForLint(extension)
         val apiTaskConfig = if (project.multiplatformExtension != null) {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
index 6e77d40..426bc93 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/LibraryVersionsService.kt
@@ -22,6 +22,7 @@
 import org.gradle.api.services.BuildServiceParameters
 import org.tomlj.Toml
 import org.tomlj.TomlParseResult
+import org.tomlj.TomlTable
 
 /**
  * Loads Library groups and versions from a specified TOML file.
@@ -31,6 +32,7 @@
         var tomlFile: Provider<String>
         var composeCustomVersion: Provider<String>
         var composeCustomGroup: Provider<String>
+        var useMultiplatformGroupVersions: Provider<Boolean>
     }
 
     private val parsedTomlFile: TomlParseResult by lazy {
@@ -59,6 +61,25 @@
     val libraryGroups: Map<String, LibraryGroup> by lazy {
         val groups = parsedTomlFile.getTable("groups")
             ?: throw GradleException("Library versions toml file is missing [groups] table")
+        val useMultiplatformGroupVersion =
+            parameters.useMultiplatformGroupVersions.orElse(false).get()
+
+        fun readGroupVersion(groupDefinition: TomlTable, groupName: String, key: String): Version? {
+            val versionRef = groupDefinition.getString(key) ?: return null
+            if (!versionRef.startsWith(VersionReferencePrefix)) {
+                throw GradleException(
+                    "Group entry $key is expected to start with $VersionReferencePrefix"
+                )
+            }
+            // name without `versions.`
+            val atomicGroupVersionName = versionRef.removePrefix(
+                VersionReferencePrefix
+            )
+            return libraryVersions[atomicGroupVersionName] ?: error(
+                "Group entry $groupName specifies $atomicGroupVersionName, but such version " +
+                    "doesn't exist"
+            )
+        }
         groups.keySet().associateWith { name ->
             val groupDefinition = groups.getTable(name)!!
             val groupName = groupDefinition.getString("group")!!
@@ -68,29 +89,31 @@
                 groupName.replace("androidx.compose", parameters.composeCustomGroup.get())
             } else groupName
 
-            if (groupDefinition.contains(AtomicGroupVersion)) {
-                val atomicGroupVersionReference = groupDefinition.getString(AtomicGroupVersion)!!
-                if (!atomicGroupVersionReference.startsWith(VersionReferencePrefix)) {
-                    throw GradleException(
-                        "Group entry $AtomicGroupVersion is expected to start with " +
-                            VersionReferencePrefix
-                    )
-                }
-                // name without `versions.`
-                val atomicGroupVersionName = atomicGroupVersionReference.removePrefix(
-                    VersionReferencePrefix
-                )
-                check(libraryVersions.containsKey(atomicGroupVersionName)) {
-                    "Group entry $name specifies $atomicGroupVersionName, but such version " +
-                        "doesn't exist"
-                }
-                LibraryGroup(finalGroupName, libraryVersions[atomicGroupVersionName])
-            } else {
-                LibraryGroup(finalGroupName, null)
+            val atomicGroupVersion = readGroupVersion(
+                groupDefinition = groupDefinition,
+                groupName = groupName,
+                key = AtomicGroupVersion
+            )
+            val multiplatformGroupVersion = readGroupVersion(
+                groupDefinition = groupDefinition,
+                groupName = groupName,
+                key = MultiplatformGroupVersion
+            )
+            check(
+                multiplatformGroupVersion == null || atomicGroupVersion != null
+            ) {
+                "Cannot specify $MultiplatformGroupVersion for $name without specifying an " +
+                    AtomicGroupVersion
             }
+            val groupVersion = when {
+                useMultiplatformGroupVersion -> multiplatformGroupVersion ?: atomicGroupVersion
+                else -> atomicGroupVersion
+            }
+            LibraryGroup(finalGroupName, groupVersion)
         }
     }
 }
 
 private const val VersionReferencePrefix = "versions."
-private const val AtomicGroupVersion = "atomicGroupVersion"
\ No newline at end of file
+private const val AtomicGroupVersion = "atomicGroupVersion"
+private const val MultiplatformGroupVersion = "multiplatformGroupVersion"
\ No newline at end of file
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
index 5513c74..bc95bb6e 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/MavenUploadHelper.kt
@@ -16,11 +16,11 @@
 
 package androidx.build
 
-import androidx.build.Multiplatform.Companion.isMultiplatformEnabled
 import com.android.build.gradle.LibraryPlugin
 import com.google.gson.GsonBuilder
 import com.google.gson.JsonObject
 import com.google.gson.stream.JsonWriter
+import org.gradle.api.artifacts.Configuration
 import groovy.util.Node
 import java.io.File
 import java.io.StringReader
@@ -42,6 +42,7 @@
 import org.gradle.api.component.SoftwareComponentFactory
 import org.gradle.api.internal.component.SoftwareComponentInternal
 import org.gradle.api.internal.component.UsageContext
+import org.gradle.api.plugins.JavaPlugin
 import org.gradle.api.provider.Provider
 import org.gradle.api.publish.PublishingExtension
 import org.gradle.api.publish.maven.MavenPom
@@ -53,6 +54,7 @@
 import org.gradle.kotlin.dsl.create
 import org.gradle.kotlin.dsl.findByType
 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
+import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
 import org.xml.sax.InputSource
 import org.xml.sax.XMLReader
@@ -62,137 +64,132 @@
     componentFactory: SoftwareComponentFactory
 ) {
     apply(mapOf("plugin" to "maven-publish"))
-
     var registered = false
-    fun registerOnFirstPublishableArtifact() {
+    fun registerOnFirstPublishableArtifact(component: SoftwareComponent) {
         if (!registered) {
+            configureComponentPublishing(extension, component, componentFactory)
             Release.register(this, extension)
             registered = true
         }
     }
     afterEvaluate {
-        components.all { component ->
-            if (configureJvmComponentPublishing(extension, component))
-                registerOnFirstPublishableArtifact()
+        if (!extension.shouldPublish()) {
+            return@afterEvaluate
         }
-
-        if (project.isMultiplatformPublicationEnabled()) {
-            configureMultiplatformPublication(componentFactory)
-            registerOnFirstPublishableArtifact()
+        components.all { component ->
+            if (isValidReleaseComponent(component)) {
+                registerOnFirstPublishableArtifact(component)
+            }
         }
     }
 }
 
 /**
- * Configure publishing for a JVM-based component.
- *
- * @return true iff a valid publication is created
+ * Configure publishing for a [SoftwareComponent].
  */
-private fun Project.configureJvmComponentPublishing(
+private fun Project.configureComponentPublishing(
     extension: AndroidXExtension,
-    component: SoftwareComponent
-): Boolean {
-    val publishThisComponent =
-        extension.shouldPublish() && component.isAndroidOrJavaReleaseComponent()
-    if (publishThisComponent) {
-        val androidxGroup = validateCoordinatesAndGetGroup(extension)
-        val projectArchiveDir = File(
-            getRepositoryDirectory(),
-            "${androidxGroup.group.replace('.', '/')}/$name"
-        )
-        group = androidxGroup.group
+    component: SoftwareComponent,
+    componentFactory: SoftwareComponentFactory
+) {
+    val androidxGroup = validateCoordinatesAndGetGroup(extension)
+    val projectArchiveDir = File(
+        getRepositoryDirectory(),
+        "${androidxGroup.group.replace('.', '/')}/$name"
+    )
+    group = androidxGroup.group
 
-        /*
-         * Provides a set of maven coordinates (groupId:artifactId) of artifacts in AndroidX
-         * that are Android Libraries.
-         */
-        val androidLibrariesSetProvider: Provider<Set<String>> = provider {
-            val androidxAndroidProjects = mutableSetOf<String>()
-            // Check every project is the project map to see if they are an Android Library
-            val projectModules = project.getProjectsMap()
-            for ((mavenCoordinates, projectPath) in projectModules) {
-                project.findProject(projectPath)?.plugins?.hasPlugin(
-                    LibraryPlugin::class.java
-                )?.let { hasLibraryPlugin ->
-                    if (hasLibraryPlugin) {
-                        androidxAndroidProjects.add(mavenCoordinates)
-                    }
+    /*
+     * Provides a set of maven coordinates (groupId:artifactId) of artifacts in AndroidX
+     * that are Android Libraries.
+     */
+    val androidLibrariesSetProvider: Provider<Set<String>> = provider {
+        val androidxAndroidProjects = mutableSetOf<String>()
+        // Check every project is the project map to see if they are an Android Library
+        val projectModules = project.getProjectsMap()
+        for ((mavenCoordinates, projectPath) in projectModules) {
+            project.findProject(projectPath)?.plugins?.hasPlugin(
+                LibraryPlugin::class.java
+            )?.let { hasLibraryPlugin ->
+                if (hasLibraryPlugin) {
+                    androidxAndroidProjects.add(mavenCoordinates)
                 }
             }
-            androidxAndroidProjects
         }
+        androidxAndroidProjects
+    }
 
-        configure<PublishingExtension> {
-            repositories {
-                it.maven { repo ->
-                    repo.setUrl(getRepositoryDirectory())
-                }
+    configure<PublishingExtension> {
+        repositories {
+            it.maven { repo ->
+                repo.setUrl(getRepositoryDirectory())
             }
-            publications {
-                if (appliesJavaGradlePluginPlugin()) {
-                    // The 'java-gradle-plugin' will also add to the 'pluginMaven' publication
-                    it.create<MavenPublication>("pluginMaven")
-                    tasks.getByName("publishPluginMavenPublicationToMavenRepository").doFirst {
+        }
+        publications {
+            if (appliesJavaGradlePluginPlugin()) {
+                // The 'java-gradle-plugin' will also add to the 'pluginMaven' publication
+                it.create<MavenPublication>("pluginMaven")
+                tasks.getByName("publishPluginMavenPublicationToMavenRepository").doFirst {
+                    removePreviouslyUploadedArchives(projectArchiveDir)
+                }
+            } else {
+                if (project.isMultiplatformPublicationEnabled()) {
+                    configureMultiplatformPublication(componentFactory)
+                } else {
+                    it.create<MavenPublication>("maven") {
+                        from(component)
+                    }
+                    tasks.getByName("publishMavenPublicationToMavenRepository").doFirst {
                         removePreviouslyUploadedArchives(projectArchiveDir)
                     }
-                } else {
-                    if (!project.isMultiplatformPublicationEnabled()) {
-                        it.create<MavenPublication>("maven") {
-                            from(component)
-                        }
-                        tasks.getByName("publishMavenPublicationToMavenRepository").doFirst {
-                            removePreviouslyUploadedArchives(projectArchiveDir)
-                        }
-                    }
-                }
-            }
-            publications.withType(MavenPublication::class.java).all {
-                it.pom { pom ->
-                    addInformativeMetadata(extension, pom)
-                    tweakDependenciesMetadata(androidxGroup, pom, androidLibrariesSetProvider)
                 }
             }
         }
-
-        // Workarounds for https://github.com/gradle/gradle/issues/20011
-        project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
-            task.doLast {
-                val metadataFile = task.outputFile.asFile.get()
-                val metadata = metadataFile.readText()
-                val sortedMetadata = sortGradleMetadataDependencies(metadata)
-
-                if (metadata != sortedMetadata) {
-                    metadataFile.writeText(sortedMetadata)
-                }
-            }
-        }
-        project.tasks.withType(GenerateMavenPom::class.java).configureEach { task ->
-            task.doLast {
-                val pomFile = task.destination
-                val pom = pomFile.readText()
-                val sortedPom = sortPomDependencies(pom)
-
-                if (pom != sortedPom) {
-                    pomFile.writeText(sortedPom)
-                }
-            }
-        }
-
-        // Workaround for https://github.com/gradle/gradle/issues/11717
-        project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
-            task.doLast {
-                val metadata = task.outputFile.asFile.get()
-                val text = metadata.readText()
-                metadata.writeText(
-                    text.replace(
-                        "\"buildId\": .*".toRegex(),
-                        "\"buildId:\": \"${getBuildId()}\""
-                    )
-                )
+        publications.withType(MavenPublication::class.java).all {
+            it.pom { pom ->
+                addInformativeMetadata(extension, pom)
+                tweakDependenciesMetadata(androidxGroup, pom, androidLibrariesSetProvider)
             }
         }
     }
-    return publishThisComponent
+
+    // Workarounds for https://github.com/gradle/gradle/issues/20011
+    project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
+        task.doLast {
+            val metadataFile = task.outputFile.asFile.get()
+            val metadata = metadataFile.readText()
+            val sortedMetadata = sortGradleMetadataDependencies(metadata)
+
+            if (metadata != sortedMetadata) {
+                metadataFile.writeText(sortedMetadata)
+            }
+        }
+    }
+    project.tasks.withType(GenerateMavenPom::class.java).configureEach { task ->
+        task.doLast {
+            val pomFile = task.destination
+            val pom = pomFile.readText()
+            val sortedPom = sortPomDependencies(pom)
+
+            if (pom != sortedPom) {
+                pomFile.writeText(sortedPom)
+            }
+        }
+    }
+
+    // Workaround for https://github.com/gradle/gradle/issues/11717
+    project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
+        task.doLast {
+            val metadata = task.outputFile.asFile.get()
+            val text = metadata.readText()
+            metadata.writeText(
+                text.replace(
+                    "\"buildId\": .*".toRegex(),
+                    "\"buildId:\": \"${getBuildId()}\""
+                )
+            )
+        }
+    }
 }
 
 /**
@@ -304,8 +301,6 @@
 }
 
 private fun Project.isMultiplatformPublicationEnabled(): Boolean {
-    if (!project.isMultiplatformEnabled())
-        return false
     return extensions.findByType<KotlinMultiplatformExtension>() != null
 }
 
@@ -314,7 +309,9 @@
 
     multiplatformExtension.targets.all { target ->
         if (target is KotlinAndroidTarget) {
-            target.publishAllLibraryVariants()
+            target.publishLibraryVariants(
+                Release.DEFAULT_PUBLISH_CONFIG
+            )
         }
     }
 
@@ -329,9 +326,11 @@
 private fun Project.replaceBaseMultiplatformPublication(
     componentFactory: SoftwareComponentFactory
 ) {
-    withSourcesComponent(componentFactory) { sourcesComponent ->
-        val kotlinComponent = components.findByName("kotlin") as SoftwareComponentInternal
-
+    val kotlinComponent = components.findByName("kotlin") as SoftwareComponentInternal
+    withSourcesComponents(
+        componentFactory,
+        setOf("sourcesElements", "androidxSourcesElements")
+    ) { sourcesComponents ->
         configure<PublishingExtension> {
             publications { pubs ->
                 pubs.create<MavenPublication>("androidxKmp") {
@@ -348,7 +347,10 @@
 
                         override fun getUsages(): MutableSet<out UsageContext> {
                             // Include sources artifact we built and root artifacts from kotlin plugin.
-                            return (sourcesComponent.usages + kotlinComponent.usages).toMutableSet()
+                            return (
+                                sourcesComponents.flatMap { it.usages } +
+                                kotlinComponent.usages
+                            ).toMutableSet()
                         }
 
                         override fun getVariants(): MutableSet<out SoftwareComponent> {
@@ -364,30 +366,37 @@
                     it.isAlias = true
                 }
             }
-        }
 
-        disableBaseKmpPublications()
+            disableBaseKmpPublications()
+        }
     }
 }
 
 /**
- * If a source configuration is currently in the project, or eventually gets added, run the given
- * configuration with it.
+ * If source configurations with the given names are currently in the project, or if they
+ * eventually gets added, run the given [action] with those configurations as software components.
  */
-private fun Project.withSourcesComponent(
+private fun Project.withSourcesComponents(
     componentFactory: SoftwareComponentFactory,
-    action: (SoftwareComponentInternal) -> Unit
+    names: Set<String>,
+    action: (List<SoftwareComponentInternal>) -> Unit
 ) {
+    val targetConfigurations = mutableSetOf<Configuration>()
     configurations.configureEach {
-        if (it.attributes.getAttribute(DocsType.DOCS_TYPE_ATTRIBUTE)?.name == DocsType.SOURCES) {
-            // "adhoc" is gradle terminology; it refers to a component with arbitrary included
-            // variants, which is what we want to build.  The name need only be unique within the
-            // project
-            val androidxSourceComponentName = "androidxJvmSources"
-            val component = componentFactory.adhoc(androidxSourceComponentName).apply {
-                addVariantsFromConfiguration(it) {}
-            } as SoftwareComponentInternal
-            action(component)
+        if (
+            it.attributes.getAttribute(DocsType.DOCS_TYPE_ATTRIBUTE)?.name == DocsType.SOURCES &&
+            it.name in names
+        ) {
+            targetConfigurations.add(it)
+            if (targetConfigurations.size == names.size) {
+                action(
+                    targetConfigurations.map { configuration ->
+                        componentFactory.adhoc(configuration.name).apply {
+                            addVariantsFromConfiguration(configuration) {}
+                        } as SoftwareComponentInternal
+                    }
+                )
+            }
         }
     }
 }
@@ -407,8 +416,14 @@
     }
 }
 
-private fun SoftwareComponent.isAndroidOrJavaReleaseComponent() =
-    name == "release" || name == "java"
+private fun Project.isValidReleaseComponent(component: SoftwareComponent) =
+    component.name == releaseComponentName()
+
+private fun Project.releaseComponentName() = when {
+    plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java) -> "kotlin"
+    plugins.hasPlugin(JavaPlugin::class.java) -> "java"
+    else -> "release"
+}
 
 private fun Project.validateCoordinatesAndGetGroup(extension: AndroidXExtension): LibraryGroup {
     val mavenGroup = extension.mavenGroup
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
index 8b2d56d..0ff1cef 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/SourceJarTaskHelper.kt
@@ -16,7 +16,10 @@
 
 package androidx.build
 
+import androidx.build.dokka.kmpDocs.DokkaAnalysisPlatform
 import com.android.build.gradle.LibraryExtension
+import com.google.gson.GsonBuilder
+import java.io.File
 import org.gradle.api.Project
 import org.gradle.api.artifacts.Configuration
 import org.gradle.api.attributes.Bundling
@@ -33,6 +36,15 @@
 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
 import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
 import java.util.Locale
+import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
+import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
+import androidx.build.dokka.kmpDocs.docsPlatform
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
 
 /**
  * Sets up a source jar task for an Android library project.
@@ -108,13 +120,61 @@
     registerSourcesVariant(sourceJar)
 }
 
+fun Project.configureSourceJarForMultiplatform() {
+    val extension = multiplatformExtension ?: throw GradleException(
+        "Unable to find multiplatform extension while configuring multiplatform source JAR"
+    )
+    val metadataFile = buildDir.resolve(
+        PROJECT_STRUCTURE_METADATA_FILEPATH
+    )
+    val multiplatformMetadataTask = tasks.register(
+        "createMultiplatformMetadata",
+        CreateMultiplatformMetadata::class.java
+    ) {
+        it.metadataFile = metadataFile
+        it.sourceSetJson = createSourceSetMetadata(extension)
+    }
+    val sourceJar = tasks.register("multiplatformSourceJar", Jar::class.java) { task ->
+        task.dependsOn(multiplatformMetadataTask)
+        task.archiveClassifier.set("multiplatform-sources")
+
+        // Do not allow source files with duplicate names, information would be lost otherwise.
+        // Different sourceSets in KMP should use different platform infixes, see b/203764756
+        task.duplicatesStrategy = DuplicatesStrategy.FAIL
+        extension.targets.flatMap {
+            it.mainCompilation().allKotlinSourceSets
+        }.toSet().forEach { sourceSet ->
+            task.from(sourceSet.kotlin.srcDirs) { copySpec ->
+                copySpec.into(sourceSet.name)
+            }
+        }
+        task.metaInf.from(metadataFile)
+    }
+    registerMultiplatformSourcesVariant(sourceJar)
+}
+
+internal val Project.multiplatformUsage
+    get() = objects.named<Usage>("androidx-multiplatform-docs")
+
+private fun Project.registerMultiplatformSourcesVariant(sourceJar: TaskProvider<Jar>) {
+    registerSourcesVariant("androidxSourcesElements", sourceJar, multiplatformUsage)
+}
+
 private fun Project.registerSourcesVariant(sourceJar: TaskProvider<Jar>) {
-    configurations.create("sourcesElements") { gradleVariant ->
+    registerSourcesVariant("sourcesElements", sourceJar, objects.named(Usage.JAVA_RUNTIME))
+}
+
+private fun Project.registerSourcesVariant(
+    configurationName: String,
+    sourceJar: TaskProvider<Jar>,
+    usage: Usage
+) {
+    configurations.create(configurationName) { gradleVariant ->
         gradleVariant.isVisible = false
         gradleVariant.isCanBeResolved = false
         gradleVariant.attributes.attribute(
             Usage.USAGE_ATTRIBUTE,
-            objects.named<Usage>(Usage.JAVA_RUNTIME)
+            usage
         )
         gradleVariant.attributes.attribute(
             Category.CATEGORY_ATTRIBUTE,
@@ -146,3 +206,65 @@
         javaComponent.addVariantsFromConfiguration(gradleVariant) { }
     }
 }
+
+/**
+ * Finds the main compilation for a source set, usually called 'main' but for android we need to
+ * search for 'debug' instead.
+ */
+private fun KotlinTarget.mainCompilation() =
+    compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("debug")
+
+/**
+ * Writes a metadata file to the given [metadataFile] location for all multiplatform Kotlin source
+ * sets including their dependencies and analysisPlatform. This is consumed when we are reading
+ * source JARs so that we can pass the correct inputs to Dackka.
+ */
+@CacheableTask
+abstract class CreateMultiplatformMetadata : DefaultTask() {
+    @Input
+    lateinit var sourceSetJson: String
+
+    @OutputFile
+    lateinit var metadataFile: File
+
+    @TaskAction
+    fun execute() {
+        metadataFile.apply {
+            parentFile.mkdirs()
+            createNewFile()
+            writeText(sourceSetJson)
+        }
+    }
+}
+
+fun createSourceSetMetadata(extension: KotlinMultiplatformExtension): String {
+    val commonMain = extension.sourceSets.getByName("commonMain")
+    val sourceSetsByName = mutableMapOf(
+        "commonMain" to mapOf(
+            "name" to commonMain.name,
+            "dependencies" to commonMain.dependsOn.map { it.name },
+            "analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName
+        )
+    )
+    extension.targets.forEach { target ->
+        target.mainCompilation().allKotlinSourceSets.forEach {
+            sourceSetsByName.getOrPut(it.name) {
+                mapOf(
+                    "name" to it.name,
+                    "dependencies" to it.dependsOn.map { it.name },
+                    "analysisPlatform" to target.docsPlatform().jsonName
+                )
+            }
+        }
+    }
+    val sourceSetMetadata = mutableMapOf(
+        "sourceSets" to sourceSetsByName.values
+    )
+    val gson = GsonBuilder().setPrettyPrinting().create()
+    return gson.toJson(sourceSetMetadata)
+}
+
+internal const val PROJECT_STRUCTURE_METADATA_FILENAME = "kotlin-project-structure-metadata.json"
+
+private const val PROJECT_STRUCTURE_METADATA_FILEPATH =
+    "project_structure_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME"
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 c921f7d..e6780db 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dackka/DackkaTask.kt
@@ -16,6 +16,11 @@
 
 package androidx.build.dackka
 
+import androidx.build.docs.ProjectStructureMetadata
+import androidx.build.dokka.kmpDocs.DokkaAnalysisPlatform
+import androidx.build.dokka.kmpDocs.DokkaInputModels
+import androidx.build.dokka.kmpDocs.DokkaUtils
+import com.google.gson.GsonBuilder
 import java.io.File
 import javax.inject.Inject
 import org.gradle.api.DefaultTask
@@ -23,6 +28,7 @@
 import org.gradle.api.file.FileCollection
 import org.gradle.api.file.RegularFile
 import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.model.ObjectFactory
 import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Provider
 import org.gradle.api.provider.SetProperty
@@ -39,13 +45,16 @@
 import org.gradle.workers.WorkAction
 import org.gradle.workers.WorkParameters
 import org.gradle.workers.WorkerExecutor
-import org.json.JSONObject
 
 @CacheableTask
 abstract class DackkaTask @Inject constructor(
-    private val workerExecutor: WorkerExecutor
+    private val workerExecutor: WorkerExecutor,
+    private val objects: ObjectFactory
 ) : DefaultTask() {
 
+    @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
+    lateinit var projectStructureMetadataFile: File
+
     // Classpath containing Dackka
     @get:Classpath
     abstract val dackkaClasspath: ConfigurableFileCollection
@@ -62,9 +71,13 @@
     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
     lateinit var samplesDir: File
 
-    // Directory containing the source code for Dackka to process
+    // Directory containing the JVM source code for Dackka to process
     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
-    lateinit var sourcesDir: File
+    lateinit var jvmSourcesDir: File
+
+    // Directory containing the multiplatform source code for Dackka to process
+    @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
+    lateinit var multiplatformSourcesDir: File
 
     // Directory containing the docs project and package-lists
     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
@@ -98,56 +111,94 @@
     @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
+    lateinit var baseSourceLink: String
+
+    private fun sourceSets(): List<DokkaInputModels.SourceSet> {
+        val externalDocs = externalLinks.map { (name, url) ->
+            DokkaInputModels.GlobalDocsLink(
+                url = url,
+                packageListUrl =
+                    "file://${docsProjectDir.toPath()}/package-lists/$name/package-list"
+            )
+        }
+        val sourceLinks = listOf(
+            DokkaInputModels.SrcLink(
+                // This is part of dokka source links but isn't needed by dackka
+                File("/"),
+                baseSourceLink
+            )
+        )
+        val gson = GsonBuilder().create()
+        val multiplatformSourceSets = projectStructureMetadataFile
+            .takeIf { it.exists() }
+            ?.let { metadataFile ->
+                val metadata = gson.fromJson(
+                    metadataFile.readText(),
+                    ProjectStructureMetadata::class.java
+                )
+                metadata.sourceSets.map { sourceSet ->
+                    val analysisPlatform = DokkaAnalysisPlatform.valueOf(
+                        sourceSet.analysisPlatform.uppercase()
+                    )
+                    val sourceDir = multiplatformSourcesDir.resolve(sourceSet.name)
+                    DokkaInputModels.SourceSet(
+                        id = sourceSetIdForSourceSet(sourceSet.name),
+                        displayName = sourceSet.name,
+                        analysisPlatform = analysisPlatform.jsonName,
+                        sourceRoots = objects.fileCollection().from(sourceDir),
+                        samples = objects.fileCollection(),
+                        includes = objects.fileCollection().from(includesFiles(sourceDir)),
+                        classpath = dependenciesClasspath,
+                        externalDocumentationLinks = externalDocs,
+                        dependentSourceSets = sourceSet.dependencies.map {
+                            sourceSetIdForSourceSet(it)
+                        },
+                        noJdkLink = !analysisPlatform.androidOrJvm(),
+                        noAndroidSdkLink = analysisPlatform != DokkaAnalysisPlatform.ANDROID,
+                        noStdlibLink = false,
+                        sourceLinks = sourceLinks
+                    )
+                }
+        } ?: emptyList()
+        return listOf(
+            DokkaInputModels.SourceSet(
+                id = sourceSetIdForSourceSet("main"),
+                displayName = "main",
+                analysisPlatform = "jvm",
+                sourceRoots = objects.fileCollection().from(jvmSourcesDir),
+                samples = objects.fileCollection().from(samplesDir, frameworkSamplesDir),
+                includes = objects.fileCollection().from(includesFiles(jvmSourcesDir)),
+                classpath = dependenciesClasspath,
+                externalDocumentationLinks = externalDocs,
+                dependentSourceSets = emptyList(),
+                noJdkLink = false,
+                noAndroidSdkLink = false,
+                noStdlibLink = false,
+                sourceLinks = sourceLinks
+            )
+        ) + multiplatformSourceSets
+    }
+
     // Documentation for Dackka command line usage and arguments can be found at
     // https://kotlin.github.io/dokka/1.6.0/user_guide/cli/usage/
     private fun computeArguments(): File {
-
-        // path comes with colons but dokka json expects an ArrayList
-        val classPath = dependenciesClasspath.asPath.split(':').toMutableList()
-
         val linksConfiguration = ""
-        val linksMap = mapOf(
-            "coroutinesCore"
-                to "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
-            "android" to "https://developer.android.com/reference",
-            "guava" to "https://guava.dev/releases/18.0/api/docs/",
-            "kotlin" to "https://kotlinlang.org/api/latest/jvm/stdlib/",
-            "junit" to "https://junit.org/junit4/javadoc/4.12/"
-        )
-        val includes = sourcesDir.walkTopDown()
-            .filter { it.name.endsWith("documentation.md") }.map { it.path }.toHashSet<String>()
-
         val jsonMap = mapOf(
             "moduleName" to "",
             "outputDir" to destinationDir.path,
             "globalLinks" to linksConfiguration,
-            "sourceSets" to listOf(mutableMapOf(
-                "sourceSetID" to mapOf(
-                    "scopeId" to "androidx",
-                    "sourceSetName" to "main"
-                    ),
-                "sourceRoots" to listOf(sourcesDir.path),
-                "samples" to listOf(samplesDir.path, frameworkSamplesDir.path),
-                "classpath" to classPath,
-                "documentedVisibilities" to listOf("PUBLIC", "PROTECTED"),
-                "externalDocumentationLinks" to linksMap.map { (name, url) -> mapOf(
-                    "url" to url,
-                    "packageListUrl" to
-                        "file://${docsProjectDir.toPath()}/package-lists/$name/package-list"
-                    ) },
-                )),
+            "sourceSets" to sourceSets(),
             "offlineMode" to "true",
             "noJdkLink" to "true"
             )
-        @Suppress("UNCHECKED_CAST")
-        if (includes.isNotEmpty())
-            ((jsonMap["sourceSets"]as List<*>).single() as MutableMap<String, Any>)["includes"] =
-                includes
-
-        val json = JSONObject(jsonMap)
+        val gson = DokkaUtils.createGson()
+        val json = gson.toJson(jsonMap)
         val outputFile = File.createTempFile("dackkaArgs", ".json")
         outputFile.deleteOnExit()
-        outputFile.writeText(json.toString(2))
+        outputFile.writeText(json)
         return outputFile
     }
 
@@ -164,6 +215,18 @@
             showLibraryMetadata = showLibraryMetadata,
         )
     }
+
+    companion object {
+        private val externalLinks = mapOf(
+            "coroutinesCore"
+                to "https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core",
+            "android" to "https://developer.android.com/reference",
+            "guava" to "https://guava.dev/releases/18.0/api/docs/",
+            "kotlin" to "https://kotlinlang.org/api/latest/jvm/stdlib/",
+            "junit" to "https://junit.org/junit4/javadoc/4.12/",
+            "okio" to "https://square.github.io/okio/3.x/okio/"
+        )
+    }
 }
 
 interface DackkaParams : WorkParameters {
@@ -232,3 +295,13 @@
         }
     }
 }
+
+private fun includesFiles(sourceRoot: File): List<File> {
+    return sourceRoot.walkTopDown().filter {
+        it.name.endsWith("documentation.md")
+    }.toList()
+}
+
+private fun sourceSetIdForSourceSet(name: String): DokkaInputModels.SourceSetId {
+    return DokkaInputModels.SourceSetId(scopeId = "androidx", sourceSetName = name)
+}
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 6a0cecf..3a6a8be 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/docs/AndroidXDocsImplPlugin.kt
@@ -16,7 +16,9 @@
 
 package androidx.build.docs
 
+import androidx.build.PROJECT_STRUCTURE_METADATA_FILENAME
 import androidx.build.SupportConfig
+import androidx.build.multiplatformUsage
 import androidx.build.dackka.DackkaTask
 import androidx.build.dackka.GenerateMetadataTask
 import androidx.build.dependencies.KOTLIN_VERSION
@@ -29,6 +31,7 @@
 import com.android.build.api.attributes.BuildTypeAttr
 import com.android.build.gradle.LibraryExtension
 import com.android.build.gradle.LibraryPlugin
+import com.google.gson.GsonBuilder
 import java.io.File
 import java.io.FileNotFoundException
 import javax.inject.Inject
@@ -47,11 +50,16 @@
 import org.gradle.api.file.ArchiveOperations
 import org.gradle.api.file.DuplicatesStrategy
 import org.gradle.api.file.FileCollection
+import org.gradle.api.file.FileSystemOperations
 import org.gradle.api.file.RegularFile
 import org.gradle.api.model.ObjectFactory
 import org.gradle.api.plugins.JavaBasePlugin
+import org.gradle.api.provider.ListProperty
 import org.gradle.api.provider.Provider
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.OutputDirectory
 import org.gradle.api.tasks.InputFiles
+import org.gradle.api.tasks.OutputFile
 import org.gradle.api.tasks.Internal
 import org.gradle.api.tasks.PathSensitive
 import org.gradle.api.tasks.PathSensitivity
@@ -72,6 +80,7 @@
 abstract class AndroidXDocsImplPlugin : Plugin<Project> {
     lateinit var docsType: String
     lateinit var docsSourcesConfiguration: Configuration
+    lateinit var multiplatformDocsSourcesConfiguration: Configuration
     lateinit var samplesSourcesConfiguration: Configuration
     lateinit var dependencyClasspath: FileCollection
 
@@ -113,22 +122,40 @@
             samplesSourcesConfiguration
         )
 
-        val unzippedSourcesForDackka = File(project.buildDir, "unzippedSourcesForDackka")
-        val unzipSourcesForDackkaTask = configureDackkaUnzipTask(
+        val unzippedJvmSourcesDirectory = File(project.buildDir, "unzippedJvmSources")
+        val unzippedMultiplatformSourcesDirectory = File(
+            project.buildDir,
+            "unzippedMultiplatformSources"
+        )
+        val mergedProjectMetadata = File(
+            project.buildDir,
+            "project_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME"
+        )
+        val unzipJvmSourcesTask = configureUnzipJvmSourcesTasks(
             project,
-            unzippedSourcesForDackka,
+            unzippedJvmSourcesDirectory,
             docsSourcesConfiguration
         )
+        val configureMultiplatformSourcesTask =
+            configureMultiplatformInputsTasks(
+                project,
+                unzippedMultiplatformSourcesDirectory,
+                multiplatformDocsSourcesConfiguration,
+                mergedProjectMetadata
+            )
 
         configureDackka(
             project,
-            unzippedSourcesForDackka,
-            unzipSourcesForDackkaTask,
+            unzippedJvmSourcesDirectory,
+            unzippedMultiplatformSourcesDirectory,
+            unzipJvmSourcesTask,
+            configureMultiplatformSourcesTask,
             unzippedSamplesSources,
             unzipSamplesTask,
             dependencyClasspath,
             buildOnServer,
             docsSourcesConfiguration,
+            mergedProjectMetadata
         )
     }
 
@@ -181,17 +208,18 @@
      *
      * This is a modified version of [configureUnzipTask], customized for Dackka usage.
      */
-    private fun configureDackkaUnzipTask(
+    private fun configureUnzipJvmSourcesTasks(
         project: Project,
         destinationDirectory: File,
         docsConfiguration: Configuration
     ): TaskProvider<Sync> {
-        return project.tasks.register("unzipSourcesForDackka", Sync::class.java) { task ->
+        return project.tasks.register("unzipJvmSources", Sync::class.java) { task ->
             val sources = docsConfiguration.incoming.artifactView { }.files
 
             // Store archiveOperations into a local variable to prevent access to the plugin
             // during the task execution, as that breaks configuration caching.
             val localVar = archiveOperations
+            task.into(destinationDirectory)
             task.from(
                 sources.elements.map { jars ->
                     jars.map {
@@ -199,12 +227,45 @@
                     }
                 }
             )
-            task.into(destinationDirectory)
             task.duplicatesStrategy = DuplicatesStrategy.INCLUDE
         }
     }
 
     /**
+     * Creates multiple tasks to unzip multiplatform sources and merge their metadata to be used
+     * as input for Dackka. Returns a single umbrella task which depends on the others.
+     */
+    private fun configureMultiplatformInputsTasks(
+        project: Project,
+        unzippedMultiplatformSourcesDirectory: File,
+        multiplatformDocsSourcesConfiguration: Configuration,
+        mergedProjectMetadata: File
+    ): TaskProvider<MergeMultiplatformMetadataTask> {
+        val tempMultiplatformMetadataDirectory = File(
+            project.buildDir,
+            "tmp/multiplatformMetadataFiles"
+        )
+        // unzip the sources into source folder and metadata files into folders per project
+        val unzipMultiplatformSources = project.tasks.register(
+            "unzipMultiplatformSources",
+            UnzipMultiplatformSourcesTask::class.java
+        ) {
+            it.inputJars.set(multiplatformDocsSourcesConfiguration.incoming.artifactView { }.files)
+            it.metadataOutput = tempMultiplatformMetadataDirectory
+            it.sourceOutput = unzippedMultiplatformSourcesDirectory
+        }
+        // merge all the metadata files from the individual project dirs
+        return project.tasks.register(
+            "mergeMultiplatformMetadata",
+            MergeMultiplatformMetadataTask::class.java
+        ) {
+            it.dependsOn(unzipMultiplatformSources)
+            it.mergedProjectMetadata = mergedProjectMetadata
+            it.inputDirectory = tempMultiplatformMetadataDirectory
+        }
+    }
+
+    /**
      *  The following configurations are created to build a list of projects that need to be
      * documented and should be used from build.gradle of docs projects for the following:
      * - docs(project(":foo:foo") or docs("androidx.foo:foo:1.0.0") for docs sources
@@ -221,6 +282,10 @@
             it.isCanBeResolved = false
             it.isCanBeConsumed = false
         }
+        val multiplatformDocsConfiguration = project.configurations.create("kmpDocs") {
+            it.isCanBeResolved = false
+            it.isCanBeConsumed = false
+        }
         val samplesConfiguration = project.configurations.create("samples") {
             it.isCanBeResolved = false
             it.isCanBeConsumed = false
@@ -256,6 +321,31 @@
             it.setResolveSources()
             it.extendsFrom(docsConfiguration)
         }
+        multiplatformDocsSourcesConfiguration = project.configurations.create(
+            "multiplatform-docs-sources"
+        ) { configuration ->
+            configuration.isTransitive = false
+            configuration.isCanBeConsumed = false
+            configuration.attributes {
+                it.attribute(
+                    Usage.USAGE_ATTRIBUTE,
+                    project.multiplatformUsage
+                )
+                it.attribute(
+                    Category.CATEGORY_ATTRIBUTE,
+                    project.objects.named<Category>(Category.DOCUMENTATION)
+                )
+                it.attribute(
+                    DocsType.DOCS_TYPE_ATTRIBUTE,
+                    project.objects.named<DocsType>(DocsType.SOURCES)
+                )
+                it.attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    project.objects.named<LibraryElements>(LibraryElements.JAR)
+                )
+            }
+            configuration.extendsFrom(multiplatformDocsConfiguration)
+        }
         samplesSourcesConfiguration = project.configurations.create("samples-sources") {
             it.setResolveSources()
             it.extendsFrom(samplesConfiguration)
@@ -312,15 +402,18 @@
 
     private fun configureDackka(
         project: Project,
-        unzippedDocsSources: File,
-        unzipDocsTask: TaskProvider<Sync>,
+        unzippedJvmSourcesDirectory: File,
+        unzippedMultiplatformSourcesDirectory: File,
+        unzipJvmSourcesTask: TaskProvider<Sync>,
+        configureMultiplatformSourcesTask: TaskProvider<MergeMultiplatformMetadataTask>,
         unzippedSamplesSources: File,
         unzipSamplesTask: TaskProvider<Sync>,
         dependencyClasspath: FileCollection,
         buildOnServer: TaskProvider<*>,
-        docsConfiguration: Configuration
+        docsConfiguration: Configuration,
+        mergedProjectMetadata: File
     ) {
-        val generatedDocsDir = project.file("${project.buildDir}/dackkaDocs")
+        val generatedDocsDir = project.file("${project.buildDir}/docs")
 
         val dackkaConfiguration = project.configurations.create("dackka").apply {
             dependencies.add(project.dependencies.create(project.getLibraryByName("dackka")))
@@ -342,11 +435,12 @@
             task.destinationFile.set(getMetadataRegularFile(project))
         }
 
-        val dackkaTask = project.tasks.register("dackkaDocs", DackkaTask::class.java) { task ->
+        val dackkaTask = project.tasks.register("docs", DackkaTask::class.java) { task ->
             task.apply {
-                dependsOn(unzipDocsTask)
+                dependsOn(unzipJvmSourcesTask)
                 dependsOn(unzipSamplesTask)
                 dependsOn(generateMetadataTask)
+                dependsOn(configureMultiplatformSourcesTask)
 
                 description = "Generates reference documentation using a Google devsite Dokka" +
                     " plugin. Places docs in $generatedDocsDir"
@@ -356,7 +450,8 @@
                 destinationDir = generatedDocsDir
                 frameworkSamplesDir = File(project.rootDir, "samples")
                 samplesDir = unzippedSamplesSources
-                sourcesDir = unzippedDocsSources
+                jvmSourcesDir = unzippedJvmSourcesDirectory
+                multiplatformSourcesDir = unzippedMultiplatformSourcesDirectory
                 docsProjectDir = File(project.rootDir, "docs-public")
                 dependenciesClasspath = project.getAndroidJar() + dependencyClasspath
                 excludedPackages = hiddenPackages.toSet()
@@ -364,15 +459,19 @@
                 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?" +
+                    "q=file:%s+class:%s&ss=androidx/platform/frameworks/support"
             }
         }
 
-        val zipTask = project.tasks.register("zipDackkaDocs", Zip::class.java) { task ->
+        val zipTask = project.tasks.register("zipDocs", Zip::class.java) { task ->
             task.apply {
                 dependsOn(dackkaTask)
                 from(generatedDocsDir)
 
-                val baseName = "dackka-$docsType-docs"
+                val baseName = "docs-$docsType"
                 val buildId = getBuildId()
                 archiveBaseName.set(baseName)
                 archiveVersion.set(buildId)
@@ -429,7 +528,7 @@
     @[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
     fun getRequiredFiles(): List<File> {
         return listOf(
-            File(distributionDirectory, "dackka-$docsType-docs-$buildId.zip"),
+            File(distributionDirectory, "docs-$docsType-$buildId.zip"),
         )
     }
 
@@ -496,3 +595,98 @@
     "androidx.*compose.*",
     "androidx.*glance.*",
 )
+
+/**
+ * Data class that matches JSON structure of kotlin source set metadata
+ */
+data class ProjectStructureMetadata(
+    var sourceSets: List<SourceSetMetadata>
+)
+
+data class SourceSetMetadata(
+    val name: String,
+    val analysisPlatform: String,
+    var dependencies: List<String>
+)
+
+@CacheableTask
+abstract class UnzipMultiplatformSourcesTask() : DefaultTask() {
+
+    @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE)
+    abstract val inputJars: ListProperty<File>
+
+    @OutputDirectory
+    lateinit var metadataOutput: File
+
+    @OutputDirectory
+    lateinit var sourceOutput: File
+
+    @get:Inject
+    abstract val fileSystemOperations: FileSystemOperations
+    @get:Inject
+    abstract val archiveOperations: ArchiveOperations
+
+    @TaskAction
+    fun execute() {
+        val sources = inputJars.get().associate { it.name to archiveOperations.zipTree(it) }
+        fileSystemOperations.sync {
+            it.duplicatesStrategy = DuplicatesStrategy.FAIL
+            it.from(sources.values)
+            it.into(sourceOutput)
+            it.exclude("META-INF/*")
+        }
+        sources.forEach { (name, fileTree) ->
+            fileSystemOperations.sync {
+                it.from(fileTree)
+                it.into(metadataOutput.resolve(name))
+                it.include("META-INF/*")
+            }
+        }
+    }
+}
+
+/**
+ * Merges multiplatform metadata files created by [CreateMultiplatformMetadata]
+ */
+@CacheableTask
+abstract class MergeMultiplatformMetadataTask() : DefaultTask() {
+
+    @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE)
+    lateinit var inputDirectory: File
+    @OutputFile
+    lateinit var mergedProjectMetadata: File
+    @TaskAction
+    fun execute() {
+        val mergedMetadata = ProjectStructureMetadata(sourceSets = listOf())
+        inputDirectory.walkTopDown().filter { file ->
+            file.name == PROJECT_STRUCTURE_METADATA_FILENAME
+        }.forEach { metaFile ->
+            val gson = GsonBuilder().create()
+            val metadata = gson.fromJson(
+                metaFile.readText(),
+                ProjectStructureMetadata::class.java
+            )
+            mergedMetadata.merge(metadata)
+        }
+        val gson = GsonBuilder().setPrettyPrinting().create()
+        val json = gson.toJson(mergedMetadata)
+        mergedProjectMetadata.apply {
+            parentFile.mkdirs()
+            createNewFile()
+            writeText(json)
+        }
+    }
+
+    private fun ProjectStructureMetadata.merge(metadata: ProjectStructureMetadata) {
+        val originalSourceSets = this.sourceSets
+        metadata.sourceSets.forEach { newSourceSet ->
+            val existingSourceSet = originalSourceSets.find { it.name == newSourceSet.name }
+            if (existingSourceSet != null) {
+                existingSourceSet.dependencies =
+                    (newSourceSet.dependencies + existingSourceSet.dependencies).toSet().toList()
+            } else {
+                sourceSets += listOf(newSourceSet)
+            }
+        }
+    }
+}
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaInputModels.kt b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaInputModels.kt
index 26ce114..ce64b52 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaInputModels.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaInputModels.kt
@@ -83,6 +83,12 @@
         @get:InputFiles
         @PathSensitive(PathSensitivity.RELATIVE)
         val sourceRoots: FileCollection,
+        @get:InputFiles
+        @PathSensitive(PathSensitivity.RELATIVE)
+        val samples: FileCollection,
+        @get:InputFiles
+        @PathSensitive(PathSensitivity.RELATIVE)
+        val includes: FileCollection,
         @get:Input
         val analysisPlatform: String,
         @get:Input
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
index c6e7b35..d6f7f70 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/dokka/kmpDocs/DokkaPartialDocsTask.kt
@@ -290,14 +290,16 @@
                         )
                     }.sortedBy {
                         it.localDirectory
-                    }
+                    },
+                    samples = project.files(),
+                    includes = project.files()
                 )
             }.sortedBy { it.displayName }
         }
     }
 }
 
-private enum class DokkaAnalysisPlatform(val jsonName: String) {
+enum class DokkaAnalysisPlatform(val jsonName: String) {
     JVM("jvm"),
     ANDROID("jvm"), // intentionally same as JVM as dokka only support jvm
     JS("js"),
@@ -323,7 +325,7 @@
     name
 }
 
-private fun KotlinTarget.docsPlatform() = when (platformType) {
+fun KotlinTarget.docsPlatform() = when (platformType) {
     KotlinPlatformType.common -> DokkaAnalysisPlatform.COMMON
     KotlinPlatformType.jvm -> DokkaAnalysisPlatform.JVM
     KotlinPlatformType.js -> DokkaAnalysisPlatform.JS
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index f19f385..33d305a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -129,7 +129,7 @@
     "tasks",
 
     // More information about the fact that these dackka tasks rerun can be found at b/167569304
-    "dackkaDocs",
+    "docs",
 
     // We know that these tasks are never up to date due to maven-metadata.xml changing
     // https://github.com/gradle/gradle/issues/11203
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
index bf29754..802d8ef 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/KmpPlatforms.kt
@@ -33,8 +33,20 @@
     MAC,
     LINUX;
     companion object {
-        val enabledByDefault = listOf(JVM)
         val native = listOf(MAC, LINUX)
+        val enabledByDefault = listOf(JVM)
+        private const val JVM_PLATFORM = "jvm"
+        private const val JS_PLATFORM = "js"
+        private const val MAC_ARM_64 = "macosarm64"
+        private const val MAC_OSX_64 = "macosx64"
+        private const val LINUX_64 = "linuxx64"
+        private const val IOS_SIMULATOR_ARM_64 = "iossimulatorarm64"
+        private const val IOS_X_64 = "iosx64"
+        private const val IOS_ARM_64 = "iosarm64"
+        val macPlatforms = listOf(MAC_ARM_64, MAC_OSX_64)
+        val linuxPlatforms = listOf(LINUX_64)
+        val iosPlatforms = listOf(IOS_SIMULATOR_ARM_64, IOS_ARM_64, IOS_X_64)
+        val nativePlatforms = macPlatforms + linuxPlatforms + iosPlatforms
     }
 }
 
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
index e1d2da9..5a4424f 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryType.kt
@@ -81,7 +81,6 @@
         val ANNOTATION_PROCESSOR_UTILS = AnnotationProcessorUtils()
         val OTHER_CODE_PROCESSOR = OtherCodeProcessor()
         val IDE_PLUGIN = IdePlugin()
-        val KMP_LIBRARY = KmpLibrary()
         val UNSET = Unset()
     }
     open class PublishedLibrary(allowCallingVisibleForTestsApis: Boolean = false) : LibraryType(
@@ -166,12 +165,6 @@
         // Android Studio, rather than by a client of the library, but also a host-side component.
         compilationTarget = CompilationTarget.DEVICE
     )
-    class KmpLibrary : LibraryType(
-        publish = Publish.SNAPSHOT_AND_RELEASE,
-        sourceJars = true,
-        checkApi = RunApiTasks.Yes(),
-        compilationTarget = CompilationTarget.DEVICE
-    )
     class Unset : LibraryType()
 }
 
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt b/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt
deleted file mode 100644
index 0b79b6d..0000000
--- a/buildSrc/public/src/main/kotlin/androidx/build/PublishExtension.kt
+++ /dev/null
@@ -1,87 +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.build
-
-/**
- * Sub-extension to configure which platforms are built / published.
- */
-open class PublishExtension {
-    var android = Publish.UNSET
-    var jvm = Publish.UNSET
-    var js = Publish.UNSET
-    var mac = Publish.UNSET
-    var linux = Publish.UNSET
-    var ios = Publish.UNSET
-
-    /**
-     * List of platforms names which should be published to maven. e.g. ["jvm", "js"]
-     */
-    val publishPlatforms: List<String>
-        get() {
-            val platforms = mutableListOf<String>()
-            if (jvm.shouldPublish()) {
-                platforms.add(JVM_PLATFORM)
-            }
-            if (js.shouldPublish()) {
-                platforms.add(JS_PLATFORM)
-            }
-            if (mac.shouldPublish()) {
-                platforms.addAll(macPlatforms)
-            }
-            if (linux.shouldPublish()) {
-                platforms.addAll(linuxPlatforms)
-            }
-            if (ios.shouldPublish()) {
-                platforms.addAll(iosPlatforms)
-            }
-            return platforms
-        }
-    private val allExtendedPlatforms
-        get() = listOf(jvm, js, mac, linux, ios)
-    private val allPlatforms
-        get() = listOf(android) + allExtendedPlatforms
-    private val activeExtendedPlatforms
-        get() = allExtendedPlatforms.filter { it != Publish.UNSET }
-
-    /**
-     * Returns true if we need to publish any non-android platforms
-     */
-    fun shouldEnableMultiplatform() = activeExtendedPlatforms.isNotEmpty()
-    fun shouldPublishAny() = allPlatforms.any {
-        it.shouldPublish()
-    }
-    fun shouldReleaseAny() = allPlatforms.any {
-        it.shouldRelease()
-    }
-    fun isPublishConfigured() = allPlatforms.any {
-        it != Publish.UNSET
-    }
-
-    companion object {
-        private const val JVM_PLATFORM = "jvm"
-        private const val JS_PLATFORM = "js"
-        private const val MAC_ARM_64 = "macosarm64"
-        private const val MAC_OSX_64 = "macosx64"
-        private const val LINUX_64 = "linuxx64"
-        private const val IOS_SIMULATOR_ARM_64 = "iossimulatorarm64"
-        private const val IOS_X_64 = "iosx64"
-        private const val IOS_ARM_64 = "iosarm64"
-        private val macPlatforms = listOf(MAC_ARM_64, MAC_OSX_64)
-        private val linuxPlatforms = listOf(LINUX_64)
-        private val iosPlatforms = listOf(IOS_SIMULATOR_ARM_64, IOS_ARM_64, IOS_X_64)
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
index 34afbfe..95ed3c0 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/Camera2CameraControlDeviceTest.kt
@@ -36,6 +36,7 @@
 import android.hardware.camera2.CaptureRequest.CONTROL_CAPTURE_INTENT_MANUAL
 import android.hardware.camera2.CaptureRequest.Key
 import android.hardware.camera2.CaptureRequest.SCALER_CROP_REGION
+import android.hardware.camera2.TotalCaptureResult
 import android.hardware.camera2.params.MeteringRectangle
 import android.os.Build
 import androidx.camera.camera2.pipe.FrameInfo
@@ -413,7 +414,7 @@
         // Assert.
         registerListener().verify(
             { _, captureResult: FrameInfo ->
-                captureResult.unwrap()!!.let { totalCaptureResult ->
+                captureResult.unwrapAs(TotalCaptureResult::class)!!.let { totalCaptureResult ->
                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
                         totalCaptureResult.physicalCameraTotalResults.containsKey(
                             physicalCameraId
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
index acd475c7..3793665 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeMetadata.kt
@@ -36,6 +36,7 @@
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.FrameMetadata
 import androidx.camera.camera2.pipe.StreamId
+import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
 private val fakeCameraIds = atomic(0)
@@ -99,12 +100,7 @@
     override fun awaitPhysicalMetadata(cameraId: CameraId): CameraMetadata =
         physicalMetadata[cameraId]!!
 
-    /** @throws UnsupportedOperationException */
-    override fun unwrap(): CameraCharacteristics? {
-        throw UnsupportedOperationException(
-            "FakeCameraMetadata does not wrap CameraCharacteristics"
-        )
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
 }
 
 /**
@@ -123,12 +119,7 @@
     override fun <T> get(key: CaptureRequest.Key<T>): T? = requestParameters[key] as T?
     override fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T = get(key) ?: default
 
-    /** @throws UnsupportedOperationException */
-    override fun unwrap(): CaptureRequest? {
-        throw UnsupportedOperationException(
-            "FakeRequestMetadata does not wrap a real CaptureRequest"
-        )
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
 }
 
 /**
@@ -147,12 +138,7 @@
 
     override fun <T> getOrDefault(key: CaptureResult.Key<T>, default: T): T = get(key) ?: default
 
-    /** @throws UnsupportedOperationException */
-    override fun unwrap(): CaptureResult? {
-        throw UnsupportedOperationException(
-            "FakeFrameMetadata does not wrap a real CaptureResult"
-        )
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
 }
 
 /**
@@ -171,11 +157,5 @@
 
     override val frameNumber: FrameNumber
         get() = metadata.frameNumber
-
-    /** @throws UnsupportedOperationException */
-    override fun unwrap(): TotalCaptureResult? {
-        throw UnsupportedOperationException(
-            "FakeFrameInfo does not wrap a real TotalCaptureResult object!"
-        )
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
index f3eccd4..78f1d39 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraMetadata.kt
@@ -33,7 +33,7 @@
  * across all OS levels and makes behavior that depends on [CameraMetadata] easier to test and
  * reason about.
  */
-public interface CameraMetadata : Metadata, UnsafeWrapper<CameraCharacteristics> {
+public interface CameraMetadata : Metadata, UnsafeWrapper {
     public operator fun <T> get(key: CameraCharacteristics.Key<T>): T?
     public fun <T> getOrDefault(key: CameraCharacteristics.Key<T>, default: T): T
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
index 2022d8c..2c1067c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Metadata.kt
@@ -70,7 +70,7 @@
  * [CameraGraph]. This class will report the actual keys / values that were sent to camera2 (if
  * different) from the request that was used to create the Camera2 [CaptureRequest].
  */
-public interface RequestMetadata : Metadata, UnsafeWrapper<CaptureRequest> {
+public interface RequestMetadata : Metadata, UnsafeWrapper {
     public operator fun <T> get(key: CaptureRequest.Key<T>): T?
     public fun <T> getOrDefault(key: CaptureRequest.Key<T>, default: T): T
 
@@ -97,7 +97,7 @@
 /**
  * [FrameInfo] is a wrapper around [TotalCaptureResult].
  */
-public interface FrameInfo : UnsafeWrapper<TotalCaptureResult> {
+public interface FrameInfo : UnsafeWrapper {
     public val metadata: FrameMetadata
 
     /**
@@ -114,7 +114,7 @@
 /**
  * [FrameMetadata] is a wrapper around [CaptureResult].
  */
-public interface FrameMetadata : Metadata, UnsafeWrapper<CaptureResult> {
+public interface FrameMetadata : Metadata, UnsafeWrapper {
     public operator fun <T> get(key: CaptureResult.Key<T>): T?
     public fun <T> getOrDefault(key: CaptureResult.Key<T>, default: T): T
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/UnsafeWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/UnsafeWrapper.kt
index 4218951..15c221d 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/UnsafeWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/UnsafeWrapper.kt
@@ -17,14 +17,27 @@
 package androidx.camera.camera2.pipe
 
 import androidx.annotation.RequiresApi
+import kotlin.reflect.KClass
 
 /**
+ * An interface for wrapper objects that should not normally be accessed directly.
+ *
  * This interface indicates that an object or interface wraps a specific Android object or type and
  * provides a way to retrieve the underlying object directly. Accessing the underlying objects can
- * be useful for compatibility and testing, but it is extremely risky if the lifetime of the object
- * is managed by Camera Pipe and the wrapped object is closed, released, or altered.
+ * be useful for compatibility and testing, but is extremely risky if the state or lifetime of the
+ * of the object is managed by CameraPipe.
  */
 @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public interface UnsafeWrapper<T> {
-    public fun unwrap(): T?
+public interface UnsafeWrapper {
+    /**
+     * Attempt to unwrap this object into an underlying type.
+     *
+     * This operation is not safe and should be used with caution as it makes no guarantees about
+     * the state of the underlying objects. In particular, implementations should assume that fakes,
+     * test wrappers will always return null. Finally this method should return null when unwrapping
+     * into the provided type is not supported.
+     *
+     * @return unwrapped object matching T or null
+     */
+    public fun <T : Any> unwrapAs(type: KClass<T>): T?
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
index 95608eb..652299c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraMetadata.kt
@@ -27,6 +27,7 @@
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.Metadata
 import androidx.camera.camera2.pipe.core.Debug
+import kotlin.reflect.KClass
 
 /**
  * This implementation provides access to [CameraCharacteristics] and lazy caching of properties
@@ -87,7 +88,11 @@
     override fun <T> getOrDefault(key: CameraCharacteristics.Key<T>, default: T): T =
         get(key) ?: default
 
-    override fun unwrap(): CameraCharacteristics = characteristics
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CameraCharacteristics::class -> characteristics as T
+        else -> null
+    }
 
     override val keys: Set<CameraCharacteristics.Key<*>> get() = _keys.value
     override val requestKeys: Set<CaptureRequest.Key<*>> get() = _requestKeys.value
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
index b831791..267b739 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CaptureSequenceProcessor.kt
@@ -36,6 +36,7 @@
 import androidx.camera.camera2.pipe.core.Threads
 import androidx.camera.camera2.pipe.writeParameters
 import javax.inject.Inject
+import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
 internal interface Camera2CaptureSequenceProcessorFactory {
@@ -307,5 +308,9 @@
 
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
 
-    override fun unwrap(): CaptureRequest = captureRequest
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CaptureRequest::class -> captureRequest as T
+        else -> null
+    }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
index 464e312..9115b43 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CameraDeviceWrapper.kt
@@ -22,6 +22,7 @@
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.TotalCaptureResult
 import android.hardware.camera2.params.InputConfiguration
+import android.hardware.camera2.params.OutputConfiguration
 import android.os.Build
 import android.os.Handler
 import android.view.Surface
@@ -35,6 +36,7 @@
 import androidx.camera.camera2.pipe.core.Timestamps
 import androidx.camera.camera2.pipe.core.Timestamps.formatMs
 import androidx.camera.camera2.pipe.writeParameter
+import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
 /** Interface around a [CameraDevice] with minor modifications.
@@ -42,7 +44,7 @@
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-internal interface CameraDeviceWrapper : UnsafeWrapper<CameraDevice> {
+internal interface CameraDeviceWrapper : UnsafeWrapper {
     /** @see [CameraDevice.getId] */
     val cameraId: CameraId
 
@@ -112,7 +114,7 @@
 
 internal fun CameraDeviceWrapper?.closeWithTrace() {
     this?.let {
-        it.unwrap().closeWithTrace()
+        it.unwrapAs(CameraDevice::class).closeWithTrace()
         it.onDeviceClosed()
     }
 }
@@ -134,7 +136,7 @@
     private val cameraMetadata: CameraMetadata,
     private val cameraDevice: CameraDevice,
     override val cameraId: CameraId
-) : CameraDeviceWrapper, UnsafeWrapper<CameraDevice> {
+) : CameraDeviceWrapper {
     private val _lastStateCallback = atomic<CameraCaptureSessionWrapper.StateCallback?>(null)
 
     override fun createCaptureSession(
@@ -212,7 +214,7 @@
         // running on older versions of the OS.
         Api24Compat.createCaptureSessionByOutputConfigurations(
             cameraDevice,
-            outputConfigurations.map { it.unwrap() },
+            outputConfigurations.map { it.unwrapAs(OutputConfiguration::class) },
             AndroidCaptureSessionStateCallback(this, stateCallback, previousStateCallback),
             handler
         )
@@ -236,7 +238,7 @@
             Api23Compat.newInputConfiguration(
                 inputConfig.width, inputConfig.height, inputConfig.format
             ),
-            outputs.map { it.unwrap() },
+            outputs.map { it.unwrapAs(OutputConfiguration::class) },
             AndroidCaptureSessionStateCallback(this, stateCallback, previousStateCallback),
             handler
         )
@@ -250,7 +252,7 @@
 
         val sessionConfig = Api28Compat.newSessionConfiguration(
             config.sessionType,
-            config.outputConfigurations.map { it.unwrap() },
+            config.outputConfigurations.map { it.unwrapAs(OutputConfiguration::class) },
             config.executor,
             AndroidCaptureSessionStateCallback(this, stateCallback, previousStateCallback)
         )
@@ -301,7 +303,10 @@
         lastStateCallback?.onSessionFinalized()
     }
 
-    override fun unwrap(): CameraDevice? {
-        return cameraDevice
-    }
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? =
+        when (type) {
+            CameraDevice::class -> cameraDevice as T
+            else -> null
+        }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
index 71dfc26..1b1ad62 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/CaptureSessionWrapper.kt
@@ -21,6 +21,7 @@
 import android.hardware.camera2.CameraCaptureSession
 import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.OutputConfiguration
 import android.os.Build
 import android.os.Handler
 import android.view.Surface
@@ -28,6 +29,7 @@
 import androidx.camera.camera2.pipe.UnsafeWrapper
 import androidx.camera.camera2.pipe.core.Log
 import java.io.Closeable
+import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
 /**
@@ -36,7 +38,7 @@
  * This interface has been modified to correct nullness, adjust exceptions, and to return or produce
  * wrapper interfaces instead of the native Camera2 types.
  */
-internal interface CameraCaptureSessionWrapper : UnsafeWrapper<CameraCaptureSession>, Closeable {
+internal interface CameraCaptureSessionWrapper : UnsafeWrapper, Closeable {
 
     /**
      * @see [CameraCaptureSession.getDevice]
@@ -256,6 +258,7 @@
         finalizeLastSession()
         stateCallback.onSessionFinalized()
     }
+
     private fun finalizeLastSession() {
         // Clear out the reference to the previous session, if one was set.
         val previousSession = _lastStateCallback.getAndSet(null)
@@ -355,15 +358,15 @@
         rethrowCamera2Exceptions {
             Api26Compat.finalizeOutputConfigurations(
                 cameraCaptureSession,
-                outputConfigs.map {
-                    it.unwrap()
-                }
+                outputConfigs.map { it.unwrapAs(OutputConfiguration::class) }
             )
         }
     }
 
-    override fun unwrap(): CameraCaptureSession? {
-        return cameraCaptureSession
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CameraCaptureSession::class -> cameraCaptureSession as T?
+        else -> null
     }
 
     override fun close() {
@@ -407,4 +410,10 @@
             throw ObjectUnavailableException(e)
         }
     }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CameraConstrainedHighSpeedCaptureSession::class -> session as T?
+        else -> super.unwrapAs(type)
+    }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
index 3079060..a4fc62e 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Configuration.kt
@@ -38,6 +38,7 @@
 import androidx.camera.camera2.pipe.core.checkOOrHigher
 import androidx.camera.camera2.pipe.core.checkPOrHigher
 import java.util.concurrent.Executor
+import kotlin.reflect.KClass
 
 /**
  * A data class that mirrors the fields in [android.hardware.camera2.params.SessionConfiguration] so
@@ -81,7 +82,7 @@
  * [OutputConfiguration]'s are NOT immutable, and changing state of an [OutputConfiguration] may
  * require the CameraCaptureSession to be finalized or updated.
  */
-internal interface OutputConfigurationWrapper : UnsafeWrapper<OutputConfiguration> {
+internal interface OutputConfigurationWrapper : UnsafeWrapper {
     /**
      * This method will return null if the output configuration was created without a Surface,
      * and until addSurface is called for the first time.
@@ -306,7 +307,11 @@
     override val surfaceGroupId: Int
         get() = output.surfaceGroupId
 
-    override fun unwrap(): OutputConfiguration = output
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        OutputConfiguration::class -> output as T
+        else -> null
+    }
 
     override fun toString(): String = output.toString()
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
index 704af63..1fcd0e4 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ExternalRequestProcessor.kt
@@ -36,6 +36,7 @@
 import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.graph.GraphListener
 import androidx.camera.camera2.pipe.graph.GraphRequestProcessor
+import kotlin.reflect.KClass
 import kotlinx.atomicfu.atomic
 
 @RequiresApi(21)
@@ -226,9 +227,6 @@
 
         override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
 
-        override fun unwrap(): CaptureRequest? {
-            // CustomRequestMetadata does not extend a Camera2 CaptureRequest.
-            return null
-        }
+        override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
index 0a13bd1..8a648c6 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/FrameMetadata.kt
@@ -29,6 +29,7 @@
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Metadata
 import androidx.camera.camera2.pipe.RequestMetadata
+import kotlin.reflect.KClass
 
 /**
  * An implementation of [FrameMetadata] that retrieves values from a [CaptureResult] object
@@ -54,7 +55,11 @@
 
     override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
 
-    override fun unwrap(): CaptureResult? = null
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CaptureResult::class -> captureResult as T
+        else -> null
+    }
 }
 
 /**
@@ -83,7 +88,8 @@
     override val frameNumber: FrameNumber
         get() = frameMetadata.frameNumber
 
-    override fun unwrap(): CaptureResult? = frameMetadata.unwrap()
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = frameMetadata.unwrapAs(type)
 }
 
 /**
@@ -135,5 +141,6 @@
     override val frameNumber: FrameNumber
         get() = result.frameNumber
 
-    override fun unwrap(): TotalCaptureResult? = totalCaptureResult
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = totalCaptureResult as? T?
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
index 6bbd539..231ec8b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/VirtualCamera.kt
@@ -238,7 +238,7 @@
         }
 
         closeWith(
-            device?.unwrap(),
+            device?.unwrapAs(CameraDevice::class),
             @Suppress("SyntheticAccessor")
             ClosingInfo(
                 ClosedReason.APP_CLOSED
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
index cca0004..4d1cfb7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/compat/VirtualCameraTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.camera2.pipe.compat
 
+import android.hardware.camera2.CameraDevice
 import android.os.Build
 import android.os.Looper.getMainLooper
 import androidx.camera.camera2.pipe.core.Timestamps
@@ -185,7 +186,11 @@
         listener.onOpened(testCamera.cameraDevice)
 
         assertThat(listener.state.value).isInstanceOf(CameraStateOpen::class.java)
-        assertThat((listener.state.value as CameraStateOpen).cameraDevice.unwrap())
+        assertThat(
+            (listener.state.value as CameraStateOpen)
+                .cameraDevice
+                .unwrapAs(CameraDevice::class)
+        )
             .isSameInstanceAs(testCamera.cameraDevice)
 
         mainLooper.idleFor(1000, TimeUnit.MILLISECONDS)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
index c8ed333..c854130 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDeviceWrapper.kt
@@ -31,6 +31,7 @@
 import androidx.camera.camera2.pipe.compat.InputConfigData
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper
 import androidx.camera.camera2.pipe.compat.SessionConfigData
+import kotlin.reflect.KClass
 
 /**
  * Fake implementation of [CameraDeviceWrapper] for tests.
@@ -117,5 +118,9 @@
         return nextSession
     }
 
-    override fun unwrap(): CameraDevice? = fakeCamera.cameraDevice
+    @Suppress("UNCHECKED_CAST")
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+        CameraDevice::class -> fakeCamera.cameraDevice as T
+        else -> null
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
index a73f197..63ba737 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCaptureSessionWrapper.kt
@@ -23,6 +23,7 @@
 import androidx.camera.camera2.pipe.compat.CameraCaptureSessionWrapper
 import androidx.camera.camera2.pipe.compat.CameraDeviceWrapper
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper
+import kotlin.reflect.KClass
 
 internal class FakeCaptureSessionWrapper(
     override val device: CameraDeviceWrapper,
@@ -102,12 +103,7 @@
         )
     }
 
-    override fun unwrap(): CameraCaptureSession? {
-        throw UnsupportedOperationException(
-            "FakeCaptureSessionWrapper does not wrap CameraCaptureSession"
-        )
-    }
-
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
     override fun close() {
         closed = true
     }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
index 8e5a267..60fa9c1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeOutputConfigurationWrapper.kt
@@ -16,10 +16,10 @@
 
 package androidx.camera.camera2.pipe.testing
 
-import android.hardware.camera2.params.OutputConfiguration
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.compat.OutputConfigurationWrapper
+import kotlin.reflect.KClass
 
 /**
  * Fake [OutputConfigurationWrapper] for use in tests.
@@ -53,7 +53,5 @@
         _surfaces.remove(surface)
     }
 
-    override fun unwrap(): OutputConfiguration? {
-        return null
-    }
+    override fun <T : Any> unwrapAs(type: KClass<T>): T? = null
 }
\ No newline at end of file
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
index 98d2189..3ab6e95 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageSaver.java
@@ -251,7 +251,7 @@
                 }
                 outputUri = Uri.fromFile(targetFile);
             }
-        } catch (IOException | IllegalArgumentException e) {
+        } catch (IOException | IllegalArgumentException | SecurityException e) {
             saveError = SaveError.FILE_IO_FAILED;
             errorMessage = "Failed to write destination file.";
             exception = e;
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 7735d8f..045c458 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -174,14 +174,15 @@
                 ? new ContentValues(options.getContentValues())
                 : new ContentValues();
         setContentValuePendingFlag(values, PENDING);
-        Uri uri = contentResolver.insert(options.getSaveCollection(), values);
-        if (uri == null) {
-            throw new ImageCaptureException(
-                    ERROR_FILE_IO, "Failed to insert a MediaStore URI.", null);
-        }
+        Uri uri = null;
         try {
+            uri = contentResolver.insert(options.getSaveCollection(), values);
+            if (uri == null) {
+                throw new ImageCaptureException(
+                        ERROR_FILE_IO, "Failed to insert a MediaStore URI.", null);
+            }
             copyTempFileToUri(file, uri, contentResolver);
-        } catch (IOException e) {
+        } catch (IOException | SecurityException e) {
             throw new ImageCaptureException(
                     ERROR_FILE_IO, "Failed to write to MediaStore URI: " + uri, e);
         } finally {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
index 587ff9a..e1f52298 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/OpenGlRenderer.java
@@ -457,28 +457,32 @@
             mProgramHandle = -1;
         }
 
-        // Destroy EGLSurfaces
-        for (OutputSurface outputSurface : mOutputSurfaceMap.values()) {
-            EGL14.eglDestroySurface(mEglDisplay, outputSurface.getEglSurface());
-        }
-        mOutputSurfaceMap.clear();
-
-        // Destroy temp surface
-        if (!Objects.equals(mTempSurface, EGL14.EGL_NO_SURFACE)) {
-            EGL14.eglDestroySurface(mEglDisplay, mTempSurface);
-            mTempSurface = EGL14.EGL_NO_SURFACE;
-        }
-
-        // Destroy EGLContext and terminate display
         if (!Objects.equals(mEglDisplay, EGL14.EGL_NO_DISPLAY)) {
+            EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
+                    EGL14.EGL_NO_CONTEXT);
+
+            // Destroy EGLSurfaces
+            for (OutputSurface outputSurface : mOutputSurfaceMap.values()) {
+                if (!Objects.equals(outputSurface.getEglSurface(), EGL14.EGL_NO_SURFACE)) {
+                    if (!EGL14.eglDestroySurface(mEglDisplay, outputSurface.getEglSurface())) {
+                        checkEglErrorOrLog("eglDestroySurface");
+                    }
+                }
+            }
+            mOutputSurfaceMap.clear();
+
+            // Destroy temp surface
+            if (!Objects.equals(mTempSurface, EGL14.EGL_NO_SURFACE)) {
+                EGL14.eglDestroySurface(mEglDisplay, mTempSurface);
+                mTempSurface = EGL14.EGL_NO_SURFACE;
+            }
+
+            // Destroy EGLContext and terminate display
             if (!Objects.equals(mEglContext, EGL14.EGL_NO_CONTEXT)) {
-                // Ignore the result of eglMakeCurrent with EGL_NO_SURFACE because it returns false
-                // on some devices.
-                EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
-                        mEglContext);
                 EGL14.eglDestroyContext(mEglDisplay, mEglContext);
                 mEglContext = EGL14.EGL_NO_CONTEXT;
             }
+            EGL14.eglReleaseThread();
             EGL14.eglTerminate(mEglDisplay);
             mEglDisplay = EGL14.EGL_NO_DISPLAY;
         }
@@ -634,6 +638,14 @@
         }
     }
 
+    private static void checkEglErrorOrLog(@NonNull String op) {
+        try {
+            checkEglErrorOrThrow(op);
+        } catch (IllegalStateException e) {
+            Logger.e(TAG, e.getMessage(), e);
+        }
+    }
+
     private static void checkGlErrorOrThrow(@NonNull String op) {
         int error = GLES20.glGetError();
         if (error != GLES20.GL_NO_ERROR) {
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureWithoutStoragePermissionTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureWithoutStoragePermissionTest.kt
new file mode 100644
index 0000000..d4bfc80
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ImageCaptureWithoutStoragePermissionTest.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.core
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.fakes.FakeLifecycleOwner
+import androidx.core.content.ContextCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+private val BACK_SELECTOR = CameraSelector.DEFAULT_BACK_CAMERA
+private const val BACK_LENS_FACING = CameraSelector.LENS_FACING_BACK
+private const val CAPTURE_TIMEOUT = 10_000.toLong() //  10 seconds
+
+@LargeTest
+@RunWith(Parameterized::class)
+class ImageCaptureWithoutStoragePermissionTest(
+    implName: String,
+    private val cameraXConfig: CameraXConfig
+) {
+
+    @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+    )
+
+    @get:Rule
+    val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(cameraXConfig)
+    )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val mainExecutor = ContextCompat.getMainExecutor(context)
+    private val defaultBuilder = ImageCapture.Builder()
+    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var fakeLifecycleOwner: FakeLifecycleOwner
+
+    @Before
+    fun setUp(): Unit = runBlocking {
+        Assume.assumeTrue(CameraUtil.hasCameraWithLensFacing(BACK_LENS_FACING))
+        createDefaultPictureFolderIfNotExist()
+        ProcessCameraProvider.configureInstance(cameraXConfig)
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        withContext(Dispatchers.Main) {
+            fakeLifecycleOwner = FakeLifecycleOwner()
+            fakeLifecycleOwner.startAndResume()
+        }
+    }
+
+    @After
+    fun tearDown(): Unit = runBlocking {
+        if (::cameraProvider.isInitialized) {
+            withContext(Dispatchers.Main) {
+                cameraProvider.unbindAll()
+                cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+            }
+        }
+    }
+
+    @Test
+    fun takePictureReturnsError_FILE_IO_whenNotStoragePermissionGranted(): Unit = runBlocking {
+
+        val checkPermissionResult =
+            ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+        // This test is only for storage permission that is not granted.
+        Assume.assumeTrue(checkPermissionResult == PackageManager.PERMISSION_DENIED)
+
+        // Arrange.
+        val useCase = defaultBuilder.build()
+        withContext(Dispatchers.Main) {
+            cameraProvider.bindToLifecycle(fakeLifecycleOwner, BACK_SELECTOR, useCase)
+        }
+
+        val contentValues = ContentValues()
+        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+        val outputFileOptions = ImageCapture.OutputFileOptions.Builder(
+            context.contentResolver,
+            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+            contentValues
+        ).build()
+
+        val callback = FakeImageSavedCallback(capturesCount = 1)
+
+        // Act.
+        useCase.takePicture(outputFileOptions, mainExecutor, callback)
+
+        // Wait for the signal that saving the image has failed
+        callback.awaitCapturesAndAssert(errorsCount = 1)
+
+        // Assert.
+        val error = callback.errors.first().imageCaptureError
+        Truth.assertThat(error).isEqualTo(ImageCapture.ERROR_FILE_IO)
+    }
+
+    private fun createDefaultPictureFolderIfNotExist() {
+        val pictureFolder = Environment.getExternalStoragePublicDirectory(
+            Environment.DIRECTORY_PICTURES
+        )
+        if (!pictureFolder.exists()) {
+            pictureFolder.mkdir()
+        }
+    }
+
+    private class FakeImageSavedCallback(capturesCount: Int) :
+        ImageCapture.OnImageSavedCallback {
+
+        private val latch = CountdownDeferred(capturesCount)
+        val results = mutableListOf<ImageCapture.OutputFileResults>()
+        val errors = mutableListOf<ImageCaptureException>()
+
+        override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
+            results.add(outputFileResults)
+            latch.countDown()
+        }
+
+        override fun onError(exception: ImageCaptureException) {
+            errors.add(exception)
+            latch.countDown()
+        }
+
+        suspend fun awaitCapturesAndAssert(
+            timeout: Long = CAPTURE_TIMEOUT,
+            savedImagesCount: Int = 0,
+            errorsCount: Int = 0
+        ) {
+            Truth.assertThat(withTimeoutOrNull(timeout) {
+                latch.await()
+            }).isNotNull()
+            Truth.assertThat(results.size).isEqualTo(savedImagesCount)
+            Truth.assertThat(errors.size).isEqualTo(errorsCount)
+        }
+    }
+
+    private class CountdownDeferred(count: Int) {
+
+        private val deferredItems = mutableListOf<CompletableDeferred<Unit>>().apply {
+            repeat(count) { add(CompletableDeferred()) }
+        }
+        private var index = 0
+
+        fun countDown() {
+            deferredItems[index++].complete(Unit)
+        }
+
+        suspend fun await() {
+            deferredItems.forEach { it.await() }
+        }
+    }
+}
\ No newline at end of file
diff --git a/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
new file mode 100644
index 0000000..0fb4867
--- /dev/null
+++ b/car/app/app-samples/navigation/automotive/src/main/AndroidManifestWithSdkVersion.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     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.
+-->
+<!--
+  This manifest is a copy of AndroidManifest with uses-sdk tag under the same folder,
+  please update this manifest after changing the other AndroidManifest.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="androidx.car.app.sample.navigation"
+    android:versionCode="1"
+    android:versionName="1.0">
+
+  <uses-sdk
+      android:minSdkVersion="29"
+      android:targetSdkVersion="33" />
+
+  <uses-permission android:name="android.permission.INTERNET"/>
+  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+  <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+
+  <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
+  <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
+  <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+
+  <!-- Various required feature settings for an automotive app. -->
+  <uses-feature
+      android:name="android.hardware.type.automotive"
+      android:required="true" />
+  <uses-feature
+      android:name="android.software.car.templates_host"
+      android:required="true" />
+  <uses-feature
+      android:name="android.hardware.wifi"
+      android:required="false" />
+  <uses-feature
+      android:name="android.hardware.screen.portrait"
+      android:required="false" />
+  <uses-feature
+      android:name="android.hardware.screen.landscape"
+      android:required="false" />
+
+  <application
+    android:label="@string/app_name"
+    android:icon="@drawable/ic_launcher"
+    android:extractNativeLibs="false">
+
+    <meta-data
+        android:name="com.android.automotive"
+        android:resource="@xml/automotive_app_desc"
+        tools:ignore="MetadataTagInsideApplicationTag" />
+
+    <meta-data android:name="androidx.car.app.minCarApiLevel"
+        android:value="1"
+        tools:ignore="MetadataTagInsideApplicationTag" />
+
+    <service
+        android:name="androidx.car.app.sample.navigation.common.car.NavigationCarAppService"
+        android:foregroundServiceType="location"
+        android:exported="true">
+
+      <intent-filter>
+        <action android:name="androidx.car.app.CarAppService" />
+        <category android:name="androidx.car.app.category.NAVIGATION"/>
+        <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
+      </intent-filter>
+    </service>
+    <service
+        android:name="androidx.car.app.sample.navigation.common.nav.NavigationService"
+        android:enabled="true"
+        android:exported="true">
+    </service>
+
+    <activity
+        android:name="androidx.car.app.activity.CarAppActivity"
+        android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
+        android:exported="true"
+        android:launchMode="singleTask"
+        android:label="Navigation">
+
+      <intent-filter>
+        <action android:name="android.intent.action.MAIN" />
+        <category android:name="android.intent.category.LAUNCHER" />
+      </intent-filter>
+      <intent-filter>
+        <action android:name="androidx.car.app.action.NAVIGATE" />
+        <category android:name="android.intent.category.DEFAULT"/>
+        <data android:scheme="geo" />
+      </intent-filter>
+      <meta-data android:name="distractionOptimized" android:value="true"/>
+    </activity>
+
+  </application>
+</manifest>
diff --git a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SearchResultsScreen.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SearchResultsScreen.java
index 8693519..75d1a7a 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SearchResultsScreen.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/car/SearchResultsScreen.java
@@ -39,6 +39,8 @@
 import androidx.car.app.sample.navigation.common.model.DemoScripts;
 import androidx.car.app.sample.navigation.common.model.PlaceInfo;
 
+import java.util.Locale;
+
 /** Screen for showing a list of places from a search. */
 public final class SearchResultsScreen extends Screen {
     @NonNull
@@ -71,8 +73,8 @@
         for (int i = 0; i < numItems; i++) {
             PlaceInfo place =
                     new PlaceInfo(
-                            String.format("Result %d", i + 1),
-                            String.format("%d Main Street.", (i + 1) * 10));
+                            String.format(Locale.US, "Result %d", i + 1),
+                            String.format(Locale.US, "%d Main Street.", (i + 1) * 10));
 
             SpannableString address = new SpannableString("  \u00b7 " + place.getDisplayAddress());
             DistanceSpan distanceSpan =
diff --git a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/model/DemoScripts.java b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/model/DemoScripts.java
index 3bf0aa8..d6632f1 100644
--- a/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/model/DemoScripts.java
+++ b/car/app/app-samples/navigation/common/src/main/java/androidx/car/app/sample/navigation/common/model/DemoScripts.java
@@ -87,6 +87,7 @@
 import java.util.Date;
 import java.util.GregorianCalendar;
 import java.util.List;
+import java.util.Locale;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
@@ -421,7 +422,7 @@
                             getCurrentDateTimeZoneWithOffset(distanceIncrement))
                             .setRemainingTimeSeconds(/* remainingTimeSeconds= */ distanceIncrement)
                             .build();
-            String notificationTitle = String.format("%dm", stepDistanceRemaining);
+            String notificationTitle = String.format(Locale.US, "%dm", stepDistanceRemaining);
             Instruction.Builder instruction =
                     Instruction.builder(
                             Instruction.Type.SET_TRIP_POSITION_NAVIGATION,
diff --git a/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
new file mode 100644
index 0000000..3fda109c
--- /dev/null
+++ b/car/app/app-samples/navigation/mobile/src/main/AndroidManifestWithSdkVersion.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2021 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<!--
+  This manifest is a copy of AndroidManifest with uses-sdk tag under the same folder,
+  please update this manifest after changing the other AndroidManifest.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="androidx.car.app.sample.navigation"
+    android:versionCode="1"
+    android:versionName="1.0">
+
+    <uses-sdk
+        android:minSdkVersion="29"
+        android:targetSdkVersion="33" />
+
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+
+    <uses-permission android:name="androidx.car.app.NAVIGATION_TEMPLATES"/>
+    <uses-permission android:name="androidx.car.app.ACCESS_SURFACE"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
+
+    <!-- Various required feature settings for an automotive app. -->
+    <uses-feature
+        android:name="android.hardware.type.automotive"
+        android:required="true" />
+    <uses-feature
+        android:name="android.software.car.templates_host"
+        android:required="true" />
+    <uses-feature
+        android:name="android.hardware.wifi"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.screen.portrait"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.screen.landscape"
+        android:required="false" />
+
+    <application
+        android:icon="@drawable/ic_launcher"
+        android:label="@string/app_name"
+        android:extractNativeLibs="false">
+
+         <meta-data
+             android:name="com.android.automotive"
+             android:resource="@xml/automotive_app_desc"
+             tools:ignore="MetadataTagInsideApplicationTag" />
+
+        <meta-data android:name="androidx.car.app.minCarApiLevel"
+            android:value="1"
+            tools:ignore="MetadataTagInsideApplicationTag" />
+
+        <service
+            android:name="androidx.car.app.sample.navigation.common.car.NavigationCarAppService"
+            android:foregroundServiceType="location"
+            android:exported="true">
+
+            <intent-filter>
+                <action android:name="androidx.car.app.CarAppService" />
+                <category android:name="androidx.car.app.category.NAVIGATION"/>
+                <category android:name="androidx.car.app.category.FEATURE_CLUSTER"/>
+            </intent-filter>
+        </service>
+        <service
+            android:name="androidx.car.app.sample.navigation.common.nav.NavigationService"
+            android:enabled="true"
+            android:exported="true">
+        </service>
+
+        <activity
+            android:theme="@android:style/Theme.DeviceDefault.NoActionBar"
+            android:name="androidx.car.app.activity.CarAppActivity"
+            android:exported="true"
+            android:launchMode="singleTask"
+            android:label="Navigation">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.car.app.action.NAVIGATE" />
+                <category android:name="android.intent.category.DEFAULT"/>
+                <data android:scheme="geo" />
+            </intent-filter>
+            <meta-data android:name="distractionOptimized" android:value="true"/>
+        </activity>
+
+    </application>
+</manifest>
\ No newline at end of file
diff --git a/collection/collection/api/api_lint.ignore b/collection/collection/api/api_lint.ignore
index 2e98cb5..b0605dd 100644
--- a/collection/collection/api/api_lint.ignore
+++ b/collection/collection/api/api_lint.ignore
@@ -1,6 +1,12 @@
 // Baseline format: 1.0
 ArrayReturn: androidx.collection.ArraySet#ArraySet(E[]) parameter #0:
     Method parameter should be Collection<E> (or subclass) instead of raw array; was `E[]`
+ArrayReturn: androidx.collection.ArraySet#toArray():
+    Method should return Collection<Object> (or subclass) instead of raw array; was `java.lang.Object[]`
+ArrayReturn: androidx.collection.ArraySet#toArray(T[]):
+    Method should return Collection<T> (or subclass) instead of raw array; was `T[]`
+ArrayReturn: androidx.collection.ArraySet#toArray(T[]) parameter #0:
+    Method parameter should be Collection<T> (or subclass) instead of raw array; was `T[]`
 
 
 KotlinOperator: androidx.collection.SparseArrayCompat#get(int, E):
diff --git a/collection/collection/api/current.txt b/collection/collection/api/current.txt
index 9d0e5a1..9697918 100644
--- a/collection/collection/api/current.txt
+++ b/collection/collection/api/current.txt
@@ -23,24 +23,30 @@
     method public static <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
   }
 
-  public final class ArraySet<E> extends kotlin.collections.AbstractMutableCollection<E> implements kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
+  public final class ArraySet<E> implements java.util.Collection<E> kotlin.jvm.internal.markers.KMutableCollection kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
     ctor public ArraySet(optional int capacity);
     ctor public ArraySet();
     ctor public ArraySet(androidx.collection.ArraySet<? extends E>? set);
     ctor public ArraySet(java.util.Collection<? extends E>? set);
     ctor public ArraySet(E![]? array);
+    method public boolean add(E? element);
     method public void addAll(androidx.collection.ArraySet<? extends E> array);
+    method public boolean addAll(java.util.Collection<? extends E> elements);
+    method public void clear();
     method public operator boolean contains(E? element);
     method public boolean containsAll(java.util.Collection<E!> elements);
     method public void ensureCapacity(int minimumCapacity);
     method public int getSize();
     method public int indexOf(Object? key);
+    method public boolean isEmpty();
     method public java.util.Iterator<E> iterator();
     method public boolean remove(E? element);
     method public boolean removeAll(androidx.collection.ArraySet<? extends E> array);
     method public boolean removeAll(java.util.Collection<E!> elements);
     method public E! removeAt(int index);
     method public boolean retainAll(java.util.Collection<E!> elements);
+    method public Object![] toArray();
+    method public <T> T![] toArray(T![] array);
     method public E! valueAt(int index);
     property public int size;
   }
diff --git a/collection/collection/api/public_plus_experimental_current.txt b/collection/collection/api/public_plus_experimental_current.txt
index 9d0e5a1..9697918 100644
--- a/collection/collection/api/public_plus_experimental_current.txt
+++ b/collection/collection/api/public_plus_experimental_current.txt
@@ -23,24 +23,30 @@
     method public static <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
   }
 
-  public final class ArraySet<E> extends kotlin.collections.AbstractMutableCollection<E> implements kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
+  public final class ArraySet<E> implements java.util.Collection<E> kotlin.jvm.internal.markers.KMutableCollection kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
     ctor public ArraySet(optional int capacity);
     ctor public ArraySet();
     ctor public ArraySet(androidx.collection.ArraySet<? extends E>? set);
     ctor public ArraySet(java.util.Collection<? extends E>? set);
     ctor public ArraySet(E![]? array);
+    method public boolean add(E? element);
     method public void addAll(androidx.collection.ArraySet<? extends E> array);
+    method public boolean addAll(java.util.Collection<? extends E> elements);
+    method public void clear();
     method public operator boolean contains(E? element);
     method public boolean containsAll(java.util.Collection<E!> elements);
     method public void ensureCapacity(int minimumCapacity);
     method public int getSize();
     method public int indexOf(Object? key);
+    method public boolean isEmpty();
     method public java.util.Iterator<E> iterator();
     method public boolean remove(E? element);
     method public boolean removeAll(androidx.collection.ArraySet<? extends E> array);
     method public boolean removeAll(java.util.Collection<E!> elements);
     method public E! removeAt(int index);
     method public boolean retainAll(java.util.Collection<E!> elements);
+    method public Object![] toArray();
+    method public <T> T![] toArray(T![] array);
     method public E! valueAt(int index);
     property public int size;
   }
diff --git a/collection/collection/api/restricted_current.txt b/collection/collection/api/restricted_current.txt
index 9d0e5a1..9697918 100644
--- a/collection/collection/api/restricted_current.txt
+++ b/collection/collection/api/restricted_current.txt
@@ -23,24 +23,30 @@
     method public static <K, V> androidx.collection.ArrayMap<K,V> arrayMapOf(kotlin.Pair<? extends K,? extends V>... pairs);
   }
 
-  public final class ArraySet<E> extends kotlin.collections.AbstractMutableCollection<E> implements kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
+  public final class ArraySet<E> implements java.util.Collection<E> kotlin.jvm.internal.markers.KMutableCollection kotlin.jvm.internal.markers.KMutableSet java.util.Set<E> {
     ctor public ArraySet(optional int capacity);
     ctor public ArraySet();
     ctor public ArraySet(androidx.collection.ArraySet<? extends E>? set);
     ctor public ArraySet(java.util.Collection<? extends E>? set);
     ctor public ArraySet(E![]? array);
+    method public boolean add(E? element);
     method public void addAll(androidx.collection.ArraySet<? extends E> array);
+    method public boolean addAll(java.util.Collection<? extends E> elements);
+    method public void clear();
     method public operator boolean contains(E? element);
     method public boolean containsAll(java.util.Collection<E!> elements);
     method public void ensureCapacity(int minimumCapacity);
     method public int getSize();
     method public int indexOf(Object? key);
+    method public boolean isEmpty();
     method public java.util.Iterator<E> iterator();
     method public boolean remove(E? element);
     method public boolean removeAll(androidx.collection.ArraySet<? extends E> array);
     method public boolean removeAll(java.util.Collection<E!> elements);
     method public E! removeAt(int index);
     method public boolean retainAll(java.util.Collection<E!> elements);
+    method public Object![] toArray();
+    method public <T> T![] toArray(T![] array);
     method public E! valueAt(int index);
     property public int size;
   }
diff --git a/collection/collection/build.gradle b/collection/collection/build.gradle
index 8bd119ab..7178862 100644
--- a/collection/collection/build.gradle
+++ b/collection/collection/build.gradle
@@ -137,7 +137,7 @@
 
 androidx {
     name = "Android Support Library collections"
-    type = LibraryType.KMP_LIBRARY
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.COLLECTION
     inceptionYear = "2018"
     description = "Standalone efficient collections."
diff --git a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
index 64bfa78..6d7a336 100644
--- a/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
+++ b/collection/collection/src/commonMain/kotlin/androidx/collection/ArraySet.kt
@@ -60,7 +60,7 @@
  * will grow once items are added to it.
  */
 public class ArraySet<E> @JvmOverloads constructor(capacity: Int = 0) :
-    AbstractMutableCollection<E>(), MutableSet<E> {
+    MutableCollection<E>, MutableSet<E> {
 
     private var hashes: IntArray = EMPTY_INTS
     private var array: Array<Any?> = EMPTY_OBJECTS
@@ -448,6 +448,26 @@
         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
+        }
+    }
+
     /**
      * This implementation returns false if the object is not a set, or
      * if the sets have different sizes.  Otherwise, for each value in this
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
index ce4cfd3..cef6022 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractCompilerTest.kt
@@ -16,34 +16,33 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.mock.MockProject
 import com.intellij.openapi.Disposable
 import com.intellij.openapi.util.Disposer
 import com.intellij.openapi.util.io.FileUtil
 import com.intellij.openapi.util.text.StringUtil
+import java.io.File
+import java.net.MalformedURLException
+import java.net.URL
+import java.net.URLClassLoader
 import junit.framework.TestCase
 import org.jetbrains.annotations.Contract
 import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
 import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
 import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation
+import org.jetbrains.kotlin.cli.common.messages.IrMessageCollector
 import org.jetbrains.kotlin.cli.common.messages.MessageCollector
 import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
-import org.jetbrains.kotlin.codegen.ClassBuilderFactories
+import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
 import org.jetbrains.kotlin.codegen.ClassFileFactory
 import org.jetbrains.kotlin.codegen.GeneratedClassLoader
 import org.jetbrains.kotlin.config.CommonConfigurationKeys
 import org.jetbrains.kotlin.config.CompilerConfiguration
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
+import org.jetbrains.kotlin.ir.util.IrMessageLogger
 import org.jetbrains.kotlin.utils.rethrow
 import org.junit.After
-import java.io.File
-import java.net.MalformedURLException
-import java.net.URL
-import java.net.URLClassLoader
-import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
 
 private const val KOTLIN_RUNTIME_VERSION = "1.3.11"
 
@@ -110,8 +109,9 @@
     }
 
     protected open fun setupEnvironment(environment: KotlinCoreEnvironment) {
-        ComposeComponentRegistrar.registerProjectExtensions(
-            environment.project as MockProject,
+        ComposeComponentRegistrar.registerCommonExtensions(environment.project)
+        ComposeComponentRegistrar.registerIrExtension(
+            environment.project,
             environment.configuration
         )
     }
@@ -170,10 +170,7 @@
             try {
                 val environment = myEnvironment ?: error("Environment not initialized")
                 val files = myFiles ?: error("Files not initialized")
-                val generationState = GenerationUtils.compileFiles(
-                    files.psiFiles, environment, ClassBuilderFactories.TEST,
-                    NoScopeRecordCliBindingTrace()
-                )
+                val generationState = GenerationUtils.compileFiles(environment, files.psiFiles)
                 generationState.factory.also { classFileFactory = it }
             } catch (e: TestsCompilerError) {
                 if (reportProblems) {
@@ -284,7 +281,7 @@
     return FileUtil.toCanonicalPath(dir.absolutePath)
 }
 
-private const val TEST_MODULE_NAME = "test-module"
+const val TEST_MODULE_NAME = "test-module"
 
 fun newConfiguration(): CompilerConfiguration {
     val configuration = CompilerConfiguration()
@@ -295,30 +292,30 @@
 
     configuration.put(JVMConfigurationKeys.VALIDATE_IR, true)
 
-    configuration.put(
-        CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY,
-        object : MessageCollector {
-            override fun clear() {}
+    val messageCollector = object : MessageCollector {
+        override fun clear() {}
 
-            override fun report(
-                severity: CompilerMessageSeverity,
-                message: String,
-                location: CompilerMessageSourceLocation?
-            ) {
-                if (severity === CompilerMessageSeverity.ERROR) {
-                    val prefix = if (location == null)
-                        ""
-                    else
-                        "(" + location.path + ":" + location.line + ":" + location.column + ") "
-                    throw AssertionError(prefix + message)
-                }
-            }
-
-            override fun hasErrors(): Boolean {
-                return false
+        override fun report(
+            severity: CompilerMessageSeverity,
+            message: String,
+            location: CompilerMessageSourceLocation?
+        ) {
+            if (severity === CompilerMessageSeverity.ERROR) {
+                val prefix = if (location == null)
+                    ""
+                else
+                    "(" + location.path + ":" + location.line + ":" + location.column + ") "
+                throw AssertionError(prefix + message)
             }
         }
-    )
+
+        override fun hasErrors(): Boolean {
+            return false
+        }
+    }
+
+    configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector)
+    configuration.put(IrMessageLogger.IR_MESSAGE_LOGGER, IrMessageCollector(messageCollector))
 
     return configuration
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
index cfc467a..8806902 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractComposeDiagnosticsTest.kt
@@ -16,15 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
-import org.jetbrains.kotlin.checkers.DiagnosedRange
-import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.config.JvmTarget
-import org.jetbrains.kotlin.diagnostics.Diagnostic
 import java.io.File
+import org.jetbrains.kotlin.checkers.DiagnosedRange
+import org.jetbrains.kotlin.checkers.utils.CheckerTestUtil
+import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.diagnostics.Diagnostic
 
 abstract class AbstractComposeDiagnosticsTest : AbstractCompilerTest() {
 
@@ -40,16 +36,7 @@
         val files = listOf(file)
 
         // Use the JVM version of the analyzer to allow using classes in .jar files
-        val moduleTrace = NoScopeRecordCliBindingTrace()
-        val result = TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
-            environment.project,
-            files,
-            moduleTrace,
-            environment.configuration.copy().apply {
-                this.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-            },
-            environment::createPackagePartProvider
-        )
+        val result = JvmResolveUtil.analyze(environment, files)
 
         // Collect the errors
         val errors = result.bindingContext.diagnostics.all().toMutableList()
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
index c1d0123..7a6d998 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractControlFlowTransformTests.kt
@@ -18,13 +18,12 @@
 
 import org.intellij.lang.annotations.Language
 
-abstract class AbstractControlFlowTransformTests : ComposeIrTransformTest() {
+abstract class AbstractControlFlowTransformTests : AbstractIrTransformTest() {
     protected fun controlFlow(
         @Language("kotlin")
         source: String,
         expectedTransformed: String,
         dumpTree: Boolean = false,
-        compilation: Compilation = JvmCompilation()
     ) = verifyComposeIrTransform(
         """
             import androidx.compose.runtime.Composable
@@ -60,6 +59,5 @@
             var c = 3
         """.trimIndent(),
         dumpTree = dumpTree,
-        compilation = compilation
     )
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
index 66d1372..594bc5d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractIrTransformTest.kt
@@ -20,55 +20,26 @@
 import java.io.File
 import org.intellij.lang.annotations.Language
 import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
-import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
-import org.jetbrains.kotlin.backend.common.extensions.IrPluginContextImpl
-import org.jetbrains.kotlin.backend.common.ir.BuiltinSymbolsBase
-import org.jetbrains.kotlin.backend.common.serialization.DescriptorByIdSignatureFinderImpl
-import org.jetbrains.kotlin.backend.jvm.JvmGeneratorExtensionsImpl
-import org.jetbrains.kotlin.backend.jvm.JvmIrTypeSystemContext
-import org.jetbrains.kotlin.backend.jvm.serialization.JvmIdSignatureDescriptor
+import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
+import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
+import org.jetbrains.kotlin.backend.jvm.jvmPhases
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
 import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
 import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots
 import org.jetbrains.kotlin.cli.jvm.config.configureJdkClasspathRoots
+import org.jetbrains.kotlin.codegen.ClassBuilderFactories
+import org.jetbrains.kotlin.codegen.CodegenFactory
+import org.jetbrains.kotlin.codegen.state.GenerationState
+import org.jetbrains.kotlin.config.CompilerConfiguration
 import org.jetbrains.kotlin.config.JVMConfigurationKeys
 import org.jetbrains.kotlin.config.JvmTarget
-import org.jetbrains.kotlin.config.LanguageFeature
-import org.jetbrains.kotlin.config.LanguageVersionSettings
-import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
-import org.jetbrains.kotlin.config.languageVersionSettings
-import org.jetbrains.kotlin.descriptors.ModuleDescriptor
-import org.jetbrains.kotlin.descriptors.konan.DeserializedKlibModuleOrigin
-import org.jetbrains.kotlin.descriptors.konan.KlibModuleOrigin
-import org.jetbrains.kotlin.ir.IrBuiltIns
 import org.jetbrains.kotlin.ir.IrElement
-import org.jetbrains.kotlin.ir.backend.jvm.serialization.JvmDescriptorMangler
-import org.jetbrains.kotlin.ir.backend.jvm.serialization.JvmIrLinker
-import org.jetbrains.kotlin.ir.builders.TranslationPluginContext
-import org.jetbrains.kotlin.ir.builders.declarations.buildClass
-import org.jetbrains.kotlin.ir.declarations.IrClass
-import org.jetbrains.kotlin.ir.declarations.IrDeclarationOrigin
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
-import org.jetbrains.kotlin.ir.declarations.impl.IrFactoryImpl
-import org.jetbrains.kotlin.ir.util.ExternalDependenciesGenerator
-import org.jetbrains.kotlin.ir.util.IrMessageLogger
-import org.jetbrains.kotlin.ir.util.ReferenceSymbolTable
-import org.jetbrains.kotlin.ir.util.SymbolTable
-import org.jetbrains.kotlin.ir.util.TypeTranslator
-import org.jetbrains.kotlin.ir.util.createParameterDeclarations
 import org.jetbrains.kotlin.ir.util.dump
-import org.jetbrains.kotlin.load.kotlin.JvmPackagePartSource
 import org.jetbrains.kotlin.psi.KtFile
-import org.jetbrains.kotlin.psi2ir.Psi2IrConfiguration
-import org.jetbrains.kotlin.psi2ir.Psi2IrTranslator
-import org.jetbrains.kotlin.psi2ir.generators.DeclarationStubGeneratorImpl
-import org.jetbrains.kotlin.psi2ir.generators.GeneratorContext
-import org.jetbrains.kotlin.resolve.AnalyzingUtils
-import org.jetbrains.kotlin.serialization.deserialization.descriptors.DeserializedContainerSource
-import org.jetbrains.kotlin.utils.addToStdlib.safeAs
 
-@Suppress("LeakingThis")
-abstract class ComposeIrTransformTest : AbstractIrTransformTest() {
+abstract class AbstractIrTransformTest : AbstractCodegenTest() {
     open val liveLiteralsEnabled get() = false
     open val liveLiteralsV2Enabled get() = false
     open val generateFunctionKeyMetaClasses get() = false
@@ -76,50 +47,21 @@
     open val intrinsicRememberEnabled get() = true
     open val decoysEnabled get() = false
     open val metricsDestination: String? get() = null
+    open val reportsDestination: String? get() = null
+    open val validateIr: Boolean get() = true
 
-    protected var extension: ComposeIrGenerationExtension? = null
-
-    override fun setUp() {
-        super.setUp()
-        extension = ComposeIrGenerationExtension(
-            myEnvironment!!.configuration,
+    protected fun createComposeIrGenerationExtension(): ComposeIrGenerationExtension =
+        ComposeIrGenerationExtension(
             liveLiteralsEnabled,
             liveLiteralsV2Enabled,
             generateFunctionKeyMetaClasses,
             sourceInformationEnabled,
             intrinsicRememberEnabled,
             decoysEnabled,
-            metricsDestination
+            metricsDestination,
+            reportsDestination,
+            validateIr,
         )
-    }
-
-    override fun postProcessingStep(
-        module: IrModuleFragment,
-        context: IrPluginContext
-    ) {
-        extension!!.generate(
-            module,
-            context
-        )
-    }
-
-    override fun tearDown() {
-        extension = null
-        super.tearDown()
-    }
-}
-
-abstract class AbstractIrTransformTest : AbstractCodegenTest() {
-    private var testLocalUnique = 0
-    protected var classesDirectory = tmpDir(
-        "kotlin-${testLocalUnique++}-classes"
-    )
-    override val additionalPaths: List<File> = listOf(classesDirectory)
-
-    abstract fun postProcessingStep(
-        module: IrModuleFragment,
-        context: IrPluginContext,
-    )
 
     fun verifyCrossModuleComposeIrTransform(
         @Language("kotlin")
@@ -136,6 +78,7 @@
         setUp()
 
         val dependencyFileName = "Test_REPLACEME_${uniqueNumber++}"
+        val classesDirectory = tmpDir("kotlin-classes")
 
         classLoader(dependencySource, dependencyFileName, dumpClasses)
             .allGeneratedFiles
@@ -154,7 +97,8 @@
             source,
             expectedTransformed,
             "",
-            dumpTree = dumpTree
+            dumpTree = dumpTree,
+            additionalPaths = listOf(classesDirectory)
         )
     }
 
@@ -164,21 +108,17 @@
         expectedTransformed: String,
         @Language("kotlin")
         extra: String = "",
-        validator: (element: IrElement) -> Unit = { },
+        validator: (element: IrElement) -> Unit = {},
         dumpTree: Boolean = false,
         truncateTracingInfoMode: TruncateTracingInfoMode = TruncateTracingInfoMode.TRUNCATE_KEY,
-        compilation: Compilation = JvmCompilation()
+        additionalPaths: List<File> = listOf(),
+        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
     ) {
-        if (!compilation.enabled) {
-            // todo indicate ignore?
-            return
-        }
-
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$')),
             sourceFile("Extra.kt", extra.replace('%', '$'))
         )
-        val irModule = compilation.compile(files)
+        val irModule = compileToIr(files, additionalPaths, applyExtraConfiguration)
         val keySet = mutableListOf<Int>()
         fun IrElement.validate(): IrElement = this.also { validator(it) }
         val actualTransformed = irModule
@@ -231,9 +171,7 @@
                         "([^\"\\n]*)\"(.*)\"\\)"
                 )
             ) {
-                "${it.groupValues[1]}\"${
-                generateSourceInfo(it.groupValues[4], source)
-                }\")"
+                "${it.groupValues[1]}\"${generateSourceInfo(it.groupValues[4], source)}\")"
             }
             .replace(
                 Regex("(sourceInformation(MarkerStart)?\\(.*)\"(.*)\"\\)")
@@ -246,9 +184,7 @@
                         "([^\"\\n]*)\"(.*)\"\\)"
                 )
             ) {
-                "${it.groupValues[1]}\"${
-                generateSourceInfo(it.groupValues[2], source)
-                }\")"
+                "${it.groupValues[1]}\"${generateSourceInfo(it.groupValues[2], source)}\")"
             }
             // replace source keys for joinKey calls
             .replace(
@@ -353,7 +289,7 @@
 
         while (currentResult != null) {
             val mr = currentResult!!
-            if (mr.range.start != current) {
+            if (mr.range.first != current) {
                 return "invalid source info at $current: '$sourceInfo'"
             }
             when {
@@ -377,157 +313,58 @@
         return result
     }
 
-    fun facadeClassGenerator(
-        generatorContext: GeneratorContext,
-        source: DeserializedContainerSource
-    ): IrClass? {
-        val jvmPackagePartSource = source.safeAs<JvmPackagePartSource>() ?: return null
-        val facadeName = jvmPackagePartSource.facadeClassName ?: jvmPackagePartSource.className
-        return generatorContext.irFactory.buildClass {
-            origin = IrDeclarationOrigin.FILE_CLASS
-            name = facadeName.fqNameForTopLevelClassMaybeWithDollars.shortName()
-        }.also {
-            it.createParameterDeclarations()
-        }
-    }
+    fun compileToIr(
+        files: List<KtFile>,
+        additionalPaths: List<File> = listOf(),
+        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
+    ): IrModuleFragment = compileToIrWithExtension(
+        files, createComposeIrGenerationExtension(), additionalPaths, applyExtraConfiguration
+    )
 
-    inner class JvmCompilation(
-        private val specificFeature: Set<LanguageFeature> = emptySet()
-    ) : Compilation {
-        override val enabled: Boolean = true
+    fun compileToIrWithExtension(
+        files: List<KtFile>,
+        extension: IrGenerationExtension,
+        additionalPaths: List<File> = listOf(),
+        applyExtraConfiguration: CompilerConfiguration.() -> Unit = {}
+    ): IrModuleFragment {
+        val classPath = createClasspath() + additionalPaths
+        val configuration = newConfiguration()
+        configuration.addJvmClasspathRoots(classPath)
+        configuration.put(JVMConfigurationKeys.IR, true)
+        configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
+        configuration.applyExtraConfiguration()
 
-        override fun compile(files: List<KtFile>): IrModuleFragment {
-            val classPath = createClasspath() + additionalPaths
-            val configuration = newConfiguration()
-            configuration.addJvmClasspathRoots(classPath)
-            configuration.put(JVMConfigurationKeys.IR, true)
-            configuration.put(JVMConfigurationKeys.JVM_TARGET, JvmTarget.JVM_1_8)
-            configuration.languageVersionSettings =
-                configuration.languageVersionSettings.setFeatures(specificFeature)
+        configuration.configureJdkClasspathRoots()
 
-            configuration.configureJdkClasspathRoots()
+        val environment = KotlinCoreEnvironment.createForTests(
+            myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
+        )
 
-            val environment = KotlinCoreEnvironment.createForTests(
-                myTestRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES
-            ).also { setupEnvironment(it) }
+        ComposeComponentRegistrar.registerCommonExtensions(environment.project)
+        IrGenerationExtension.registerExtension(environment.project, extension)
 
-            val mangler = JvmDescriptorMangler(null)
+        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
+        val codegenFactory = JvmIrCodegenFactory(
+            configuration,
+            configuration.get(CLIConfigurationKeys.PHASE_CONFIG) ?: PhaseConfig(jvmPhases)
+        )
 
-            val psi2ir = Psi2IrTranslator(
-                environment.configuration.languageVersionSettings,
-                Psi2IrConfiguration(ignoreErrors = false)
-            )
-            val messageLogger = environment.configuration[IrMessageLogger.IR_MESSAGE_LOGGER]
-                ?: IrMessageLogger.None
-            val symbolTable = SymbolTable(
-                JvmIdSignatureDescriptor(mangler),
-                IrFactoryImpl
-            )
+        val state = GenerationState.Builder(
+            environment.project,
+            ClassBuilderFactories.TEST,
+            analysisResult.moduleDescriptor,
+            analysisResult.bindingContext,
+            files,
+            configuration
+        ).isIrBackend(true).codegenFactory(codegenFactory).build()
 
-            val analysisResult = JvmResolveUtil.analyze(files, environment)
-            if (!psi2ir.configuration.ignoreErrors) {
-                analysisResult.throwIfError()
-                AnalyzingUtils.throwExceptionOnErrors(analysisResult.bindingContext)
-            }
-            val extensions = JvmGeneratorExtensionsImpl(configuration)
-            val generatorContext = psi2ir.createGeneratorContext(
-                analysisResult.moduleDescriptor,
-                analysisResult.bindingContext,
-                symbolTable,
-                extensions = extensions
-            )
-            val stubGenerator = DeclarationStubGeneratorImpl(
-                generatorContext.moduleDescriptor,
-                generatorContext.symbolTable,
-                generatorContext.irBuiltIns,
-                DescriptorByIdSignatureFinderImpl(generatorContext.moduleDescriptor, mangler),
-                extensions
-            )
-            val frontEndContext = object : TranslationPluginContext {
-                override val moduleDescriptor: ModuleDescriptor
-                    get() = generatorContext.moduleDescriptor
-                override val symbolTable: ReferenceSymbolTable
-                    get() = symbolTable
-                override val typeTranslator: TypeTranslator
-                    get() = generatorContext.typeTranslator
-                override val irBuiltIns: IrBuiltIns
-                    get() = generatorContext.irBuiltIns
-            }
-            val irLinker = JvmIrLinker(
-                generatorContext.moduleDescriptor,
-                messageLogger,
-                JvmIrTypeSystemContext(generatorContext.irBuiltIns),
-                generatorContext.symbolTable,
-                frontEndContext,
-                stubGenerator,
-                mangler,
-                true
-            )
+        state.beforeCompile()
 
-            generatorContext.moduleDescriptor.allDependencyModules.map {
-                val capability = it.getCapability(KlibModuleOrigin.CAPABILITY)
-                val kotlinLibrary = (capability as? DeserializedKlibModuleOrigin)?.library
-                irLinker.deserializeIrModuleHeader(
-                    it,
-                    kotlinLibrary,
-                    _moduleName = it.name.asString()
-                )
-            }
-
-            val irProviders = listOf(irLinker)
-
-            val symbols = BuiltinSymbolsBase(
-                generatorContext.irBuiltIns,
-                symbolTable,
-            )
-
-            ExternalDependenciesGenerator(
-                generatorContext.symbolTable,
-                irProviders
-            ).generateUnboundSymbolsAsDependencies()
-
-            psi2ir.addPostprocessingStep { module ->
-                val old = stubGenerator.unboundSymbolGeneration
-                try {
-                    stubGenerator.unboundSymbolGeneration = true
-                    val context = IrPluginContextImpl(
-                        module = generatorContext.moduleDescriptor,
-                        bindingContext = generatorContext.bindingContext,
-                        languageVersionSettings = generatorContext.languageVersionSettings,
-                        st = generatorContext.symbolTable,
-                        typeTranslator = generatorContext.typeTranslator,
-                        irBuiltIns = generatorContext.irBuiltIns,
-                        linker = irLinker,
-                        symbols = symbols,
-                        diagnosticReporter = IrMessageLogger.None,
-                    )
-                    postProcessingStep(module, context)
-                } finally {
-                    stubGenerator.unboundSymbolGeneration = old
-                }
-            }
-
-            val irModuleFragment = psi2ir.generateModuleFragment(
-                generatorContext,
-                files,
-                irProviders,
-                IrGenerationExtension.getInstances(myEnvironment!!.project),
-                expectDescriptorToSymbol = null
-            )
-            irLinker.postProcess()
-
-            // See JvmIrCodegenFactory. This is necessary to avoid unbound symbols when inspecting
-            // lazy declarations in `irModuleFragment`.
-            stubGenerator.unboundSymbolGeneration = true
-
-            return irModuleFragment
-        }
-    }
-
-    // This interface enables different Compilation variants for compiler tests
-    interface Compilation {
-        val enabled: Boolean
-        fun compile(files: List<KtFile>): IrModuleFragment
+        val psi2irInput = CodegenFactory.IrConversionInput.fromGenerationStateAndFiles(
+            state,
+            files
+        )
+        return codegenFactory.convertToIr(psi2irInput).irModuleFragment
     }
 
     enum class TruncateTracingInfoMode {
@@ -535,11 +372,3 @@
         KEEP_INFO_STRING, // truncates everything except for the `info` string
     }
 }
-
-internal fun LanguageVersionSettings.setFeatures(
-    features: Set<LanguageFeature>
-) = LanguageVersionSettingsImpl(
-    languageVersion = languageVersion,
-    apiVersion = apiVersion,
-    specificFeatures = features.associateWith { LanguageFeature.State.ENABLED }
-)
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt
new file mode 100644
index 0000000..335ab22
--- /dev/null
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractLiveLiteralTransformTests.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.compiler.plugins.kotlin
+
+import androidx.compose.compiler.plugins.kotlin.lower.DurableKeyVisitor
+import androidx.compose.compiler.plugins.kotlin.lower.LiveLiteralTransformer
+import org.intellij.lang.annotations.Language
+import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
+import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
+import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
+import org.jetbrains.kotlin.psi.KtFile
+
+abstract class AbstractLiveLiteralTransformTests : AbstractIrTransformTest() {
+    private fun computeKeys(files: List<KtFile>): List<String> {
+        var builtKeys = mutableSetOf<String>()
+        compileToIrWithExtension(
+            files,
+            object : IrGenerationExtension {
+                override fun generate(
+                    moduleFragment: IrModuleFragment,
+                    pluginContext: IrPluginContext
+                ) {
+                    val symbolRemapper = DeepCopySymbolRemapper()
+                    val keyVisitor = DurableKeyVisitor(builtKeys)
+                    val transformer = object : LiveLiteralTransformer(
+                        liveLiteralsEnabled || liveLiteralsV2Enabled,
+                        liveLiteralsV2Enabled,
+                        keyVisitor,
+                        pluginContext,
+                        symbolRemapper,
+                        ModuleMetricsImpl("temp")
+                    ) {
+                        override fun makeKeySet(): MutableSet<String> {
+                            return super.makeKeySet().also { builtKeys = it }
+                        }
+                    }
+                    transformer.lower(moduleFragment)
+                }
+            }
+        )
+        return builtKeys.toList()
+    }
+
+    // since the lowering will throw an exception if duplicate keys are found, all we have to do
+    // is run the lowering
+    protected fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
+        computeKeys(
+            listOf(
+                sourceFile("Test.kt", src.replace('%', '$'))
+            )
+        )
+    }
+
+    // For a given src string, a
+    protected fun assertKeys(vararg keys: String, makeSrc: () -> String) {
+        val builtKeys = computeKeys(
+            listOf(
+                sourceFile("Test.kt", makeSrc().replace('%', '$'))
+            )
+        )
+        assertEquals(
+            keys.toList().sorted().joinToString(separator = ",\n") {
+                "\"${it.replace('$', '%')}\""
+            },
+            builtKeys.toList().sorted().joinToString(separator = ",\n") {
+                "\"${it.replace('$', '%')}\""
+            }
+        )
+    }
+
+    // test: have two src strings (before/after) and assert that the keys of the params didn't change
+    protected fun assertDurableChange(before: String, after: String) {
+        val beforeKeys = computeKeys(
+            listOf(
+                sourceFile("Test.kt", before.replace('%', '$'))
+            )
+        )
+
+        val afterKeys = computeKeys(
+            listOf(
+                sourceFile("Test.kt", after.replace('%', '$'))
+            )
+        )
+
+        assertEquals(
+            beforeKeys.toList().sorted().joinToString(separator = "\n"),
+            afterKeys.toList().sorted().joinToString(separator = "\n")
+        )
+    }
+
+    protected fun assertTransform(
+        unchecked: String,
+        checked: String,
+        expectedTransformed: String,
+        dumpTree: Boolean = false
+    ) = verifyComposeIrTransform(
+        """
+            import androidx.compose.runtime.Composable
+            $checked
+        """.trimIndent(),
+        expectedTransformed,
+        """
+            import androidx.compose.runtime.Composable
+            $unchecked
+        """.trimIndent(),
+        dumpTree = dumpTree
+    )
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
index 7ebff19..f1ed006 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/AbstractMetricsTransformTest.kt
@@ -16,31 +16,20 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
-
-abstract class AbstractMetricsTransformTest : ComposeIrTransformTest() {
-    override val metricsDestination: String?
-        get() = null
-
-    override fun postProcessingStep(
-        module: IrModuleFragment,
-        context: IrPluginContext
-    ) {
-        extension!!.metrics = ModuleMetricsImpl(module.name.asString())
-        super.postProcessingStep(module, context)
-    }
-
-    fun verifyMetrics(
+abstract class AbstractMetricsTransformTest : AbstractIrTransformTest() {
+    private fun verifyMetrics(
         source: String,
-        compilation: Compilation = JvmCompilation(),
         verify: ModuleMetrics.() -> Unit
     ) {
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$')),
         )
-        compilation.compile(files)
-        extension!!.metrics.verify()
+
+        val extension = createComposeIrGenerationExtension()
+        val metrics = ModuleMetricsImpl(TEST_MODULE_NAME)
+        extension.metrics = metrics
+        compileToIrWithExtension(files, extension)
+        metrics.verify()
     }
 
     fun assertClasses(
@@ -102,9 +91,4 @@
                 .trimTrailingWhitespacesAndAddNewlineAtEOF(),
         )
     }
-
-    override fun tearDown() {
-        extension!!.metrics = EmptyModuleMetrics
-        super.tearDown()
-    }
 }
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
index c3a5112..c395c4b 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ClassStabilityTransformTests.kt
@@ -28,7 +28,7 @@
 import org.jetbrains.kotlin.ir.util.statements
 import org.junit.Test
 
-class ClassStabilityTransformTests : ComposeIrTransformTest() {
+class ClassStabilityTransformTests : AbstractIrTransformTest() {
 
     @Test
     fun testEmptyClassIsStable() = assertStability(
@@ -1194,7 +1194,7 @@
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$'))
         )
-        val irModule = JvmCompilation().compile(files)
+        val irModule = compileToIr(files)
         val irClass = irModule.files.last().declarations.first() as IrClass
         val classStability = stabilityOf(irClass.defaultType as IrType)
 
@@ -1287,6 +1287,7 @@
             $externalSrc
         """.trimIndent()
 
+        val classesDirectory = tmpDir("kotlin-classes")
         classLoader(dependencySrc, dependencyFileName, dumpClasses)
             .allGeneratedFiles
             .also {
@@ -1316,7 +1317,7 @@
         val files = listOf(
             sourceFile("Test.kt", source.replace('%', '$'))
         )
-        return JvmCompilation().compile(files)
+        return compileToIr(files, additionalPaths = listOf(classesDirectory))
     }
 
     private fun assertTransform(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
index e52cc06..9b45bd8 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposeCallResolverTests.kt
@@ -221,9 +221,9 @@
         val environment = myEnvironment ?: error("Environment not initialized")
 
         val ktFile = KtPsiFactory(environment.project).createFile(text)
-        val bindingContext = JvmResolveUtil.analyze(
-            ktFile,
-            environment
+        val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
+            environment,
+            listOf(ktFile)
         ).bindingContext
 
         carets.forEachIndexed { index, (offset, calltype) ->
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
index 9ed0139..78e84bf 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ComposerParamTransformTests.kt
@@ -24,7 +24,7 @@
 import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid
 import org.junit.Test
 
-class ComposerParamTransformTests : ComposeIrTransformTest() {
+class ComposerParamTransformTests : AbstractIrTransformTest() {
     private fun composerParam(
         @Language("kotlin")
         source: String,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
index ae4fc32..fd5aa06 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ContextReceiversTransformTests.kt
@@ -18,10 +18,11 @@
 
 import org.intellij.lang.annotations.Language
 import org.jetbrains.kotlin.config.LanguageFeature
+import org.jetbrains.kotlin.config.LanguageVersionSettingsImpl
+import org.jetbrains.kotlin.config.languageVersionSettings
 import org.junit.Test
 
-class ContextReceiversTransformTests : ComposeIrTransformTest() {
-
+class ContextReceiversTransformTests : AbstractIrTransformTest() {
     private fun contextReceivers(
         @Language("kotlin")
         unchecked: String,
@@ -42,7 +43,15 @@
 
             fun used(x: Any?) {}
         """.trimIndent(),
-        compilation = JvmCompilation(specificFeature = setOf(LanguageFeature.ContextReceivers))
+        applyExtraConfiguration = {
+            languageVersionSettings = LanguageVersionSettingsImpl(
+                languageVersion = languageVersionSettings.languageVersion,
+                apiVersion = languageVersionSettings.apiVersion,
+                specificFeatures = mapOf(
+                    LanguageFeature.ContextReceivers to LanguageFeature.State.ENABLED
+                )
+            )
+        }
     )
 
     @Test
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
index 89b3ba3..07fd352 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/DefaultParamTransformTests.kt
@@ -19,7 +19,7 @@
 import org.intellij.lang.annotations.Language
 import org.junit.Test
 
-class DefaultParamTransformTests : ComposeIrTransformTest() {
+class DefaultParamTransformTests : AbstractIrTransformTest() {
     private fun defaultParams(
         @Language("kotlin")
         unchecked: String,
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 d214ded..656fb00 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
@@ -19,7 +19,7 @@
 import org.intellij.lang.annotations.Language
 import org.junit.Test
 
-abstract class FunctionBodySkippingTransfomrTestsBase : ComposeIrTransformTest() {
+abstract class FunctionBodySkippingTransformTestsBase : AbstractIrTransformTest() {
     protected fun comparisonPropagation(
         @Language("kotlin")
         unchecked: String,
@@ -46,7 +46,7 @@
     )
 }
 
-class FunctionBodySkippingTransformTests : FunctionBodySkippingTransfomrTestsBase() {
+class FunctionBodySkippingTransformTests : FunctionBodySkippingTransformTestsBase() {
 
     @Test
     fun testIfInLambda(): Unit = comparisonPropagation(
@@ -399,7 +399,7 @@
                 if (isTraceInProgress()) {
                   traceEventStart(<>, %dirty, -1, <>)
                 }
-                Text("hello world", null, colors.getColor(%composer, 0b1110 and %dirty), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, null, null, %composer, 0b0110, 0, 0b1111111111111010)
+                Text("hello world", null, colors.getColor(%composer, 0b1110 and %dirty), <unsafe-coerce>(0L), null, null, null, <unsafe-coerce>(0L), null, null, <unsafe-coerce>(0L), <unsafe-coerce>(0), false, 0, 0, null, null, %composer, 0b0110, 0, 0b00011111111111111010)
                 if (isTraceInProgress()) {
                   traceEventEnd()
                 }
@@ -3884,7 +3884,7 @@
     )
 }
 
-class FunctionBodySkippingTransformTestsNoSource : FunctionBodySkippingTransfomrTestsBase() {
+class FunctionBodySkippingTransformTestsNoSource : FunctionBodySkippingTransformTestsBase() {
     override val sourceInformationEnabled: Boolean get() = false
 
     @Test
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
index 0df3b5e..d77cf5d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/GenerationUtils.kt
@@ -16,80 +16,41 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.psi.search.GlobalSearchScope
 import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
 import org.jetbrains.kotlin.backend.jvm.JvmIrCodegenFactory
 import org.jetbrains.kotlin.backend.jvm.jvmPhases
 import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
-import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.codegen.ClassBuilderFactories
-import org.jetbrains.kotlin.codegen.ClassBuilderFactory
-import org.jetbrains.kotlin.codegen.DefaultCodegenFactory
 import org.jetbrains.kotlin.codegen.KotlinCodegenFacade
 import org.jetbrains.kotlin.codegen.state.GenerationState
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
-import org.jetbrains.kotlin.load.kotlin.PackagePartProvider
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.resolve.AnalyzingUtils
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 object GenerationUtils {
-    @JvmStatic
-    @JvmOverloads
     fun compileFiles(
-        files: List<KtFile>,
         environment: KotlinCoreEnvironment,
-        classBuilderFactory: ClassBuilderFactory = ClassBuilderFactories.TEST,
-        trace: BindingTrace = NoScopeRecordCliBindingTrace()
-    ): GenerationState =
-        compileFiles(
-            files,
-            environment.configuration,
-            classBuilderFactory,
-            environment::createPackagePartProvider,
-            trace
-        )
-
-    @JvmStatic
-    @JvmOverloads
-    fun compileFiles(
         files: List<KtFile>,
-        configuration: CompilerConfiguration,
-        classBuilderFactory: ClassBuilderFactory,
-        packagePartProvider: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = NoScopeRecordCliBindingTrace()
     ): GenerationState {
-        val analysisResult =
-            JvmResolveUtil.analyzeAndCheckForErrors(
-                files.first().project,
-                files,
-                configuration,
-                packagePartProvider,
-                trace
-            )
+        val analysisResult = JvmResolveUtil.analyzeAndCheckForErrors(environment, files)
         analysisResult.throwIfError()
 
         val state = GenerationState.Builder(
-            files.first().project,
-            classBuilderFactory,
+            environment.project,
+            ClassBuilderFactories.TEST,
             analysisResult.moduleDescriptor,
             analysisResult.bindingContext,
             files,
-            configuration
+            environment.configuration
         ).codegenFactory(
-            if (configuration.getBoolean(JVMConfigurationKeys.IR))
-                JvmIrCodegenFactory(
-                    configuration,
-                    configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
-                        ?: PhaseConfig(jvmPhases)
-                )
-            else DefaultCodegenFactory
-        ).build()
-        if (analysisResult.shouldGenerateCode) {
-            KotlinCodegenFacade.compileCorrectFiles(state)
-        }
+            JvmIrCodegenFactory(
+                environment.configuration,
+                environment.configuration.get(CLIConfigurationKeys.PHASE_CONFIG)
+                    ?: PhaseConfig(jvmPhases)
+            )
+        ).isIrBackend(true).build()
+
+        KotlinCodegenFacade.compileCorrectFiles(state)
 
         // For JVM-specific errors
         try {
@@ -100,4 +61,4 @@
 
         return state
     }
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
index adadec6..629bfca 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/JvmResolveUtil.kt
@@ -16,26 +16,17 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import com.intellij.openapi.project.Project
-import com.intellij.psi.search.GlobalSearchScope
 import org.jetbrains.kotlin.analyzer.AnalysisResult
-import org.jetbrains.kotlin.cli.jvm.compiler.CliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment
+import org.jetbrains.kotlin.cli.jvm.compiler.NoScopeRecordCliBindingTrace
 import org.jetbrains.kotlin.cli.jvm.compiler.TopDownAnalyzerFacadeForJVM
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.load.kotlin.PackagePartProvider
 import org.jetbrains.kotlin.psi.KtFile
 import org.jetbrains.kotlin.resolve.AnalyzingUtils
-import org.jetbrains.kotlin.resolve.BindingTrace
 
 object JvmResolveUtil {
-    @JvmStatic
     fun analyzeAndCheckForErrors(
-        project: Project,
-        files: Collection<KtFile>,
-        configuration: CompilerConfiguration,
-        packagePartProvider: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = CliBindingTrace()
+        environment: KotlinCoreEnvironment,
+        files: Collection<KtFile>
     ): AnalysisResult {
         for (file in files) {
             try {
@@ -45,13 +36,7 @@
             }
         }
 
-        return analyze(
-            project,
-            files,
-            configuration,
-            packagePartProvider,
-            trace
-        ).apply {
+        return analyze(environment, files).apply {
             try {
                 AnalyzingUtils.throwExceptionOnErrors(bindingContext)
             } catch (e: Exception) {
@@ -60,40 +45,12 @@
         }
     }
 
-    @JvmStatic
-    fun analyze(file: KtFile, environment: KotlinCoreEnvironment): AnalysisResult =
-        analyze(setOf(file), environment)
-
-    @JvmStatic
-    fun analyze(files: Collection<KtFile>, environment: KotlinCoreEnvironment): AnalysisResult =
-        analyze(
-            files,
-            environment,
-            environment.configuration
-        )
-
-    @JvmStatic
-    fun analyze(
-        files: Collection<KtFile>,
-        environment: KotlinCoreEnvironment,
-        configuration: CompilerConfiguration
-    ): AnalysisResult =
-        analyze(
+    fun analyze(environment: KotlinCoreEnvironment, files: Collection<KtFile>): AnalysisResult =
+        TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
             environment.project,
             files,
-            configuration,
+            NoScopeRecordCliBindingTrace(),
+            environment.configuration,
             environment::createPackagePartProvider
         )
-
-    private fun analyze(
-        project: Project,
-        files: Collection<KtFile>,
-        configuration: CompilerConfiguration,
-        packagePartProviderFactory: (GlobalSearchScope) -> PackagePartProvider,
-        trace: BindingTrace = CliBindingTrace()
-    ): AnalysisResult {
-        return TopDownAnalyzerFacadeForJVM.analyzeFilesWithJavaIntegration(
-            project, files, trace, configuration, packagePartProviderFactory
-        )
-    }
 }
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
index 07f487d..da19e2d 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/KtxCrossModuleTests.kt
@@ -187,6 +187,88 @@
         }
     }
 
+    @Test // see: b/255983530
+    fun testNonComposableWithComposableReturnTypeCrossModule(): Unit = ensureSetup {
+        compile(
+            mapOf(
+                "library module" to mapOf(
+                    "x/MakeComposable.kt" to """
+                      package x
+                      import androidx.compose.runtime.Composable
+
+                      fun makeComposable(): @Composable () -> Unit = @Composable {}
+                    """.trimIndent()
+                ),
+                "Main" to mapOf(
+                    "y/User.kt" to """
+                      package y
+                      import x.makeComposable
+                      import androidx.compose.runtime.Composable
+
+                      fun acceptComposable(composable: @Composable () -> Unit) {
+
+                      }
+
+                      fun test() {
+                        acceptComposable(makeComposable())
+                      }
+                    """.trimIndent()
+                )
+            ),
+        ) {
+            assert(
+                it.contains("public final static makeComposable()Lkotlin/jvm/functions/Function2;")
+            )
+            assert(
+                !it.contains(
+                "INVOKESTATIC x/MakeComposableKt.makeComposable ()Lkotlin/jvm/functions/Function0;"
+                )
+            )
+            assert(
+                it.contains(
+                "INVOKESTATIC x/MakeComposableKt.makeComposable ()Lkotlin/jvm/functions/Function2;"
+                )
+            )
+        }
+    }
+
+    @Test // see: b/255983530
+    fun testNonComposableWithNestedComposableReturnTypeCrossModule(): Unit = ensureSetup {
+        compile(
+            mapOf(
+                "library module" to mapOf(
+                    "x/MakeComposable.kt" to """
+                      package x
+                      import androidx.compose.runtime.Composable
+
+                      fun makeComposable(): List<@Composable () -> Unit> = listOf(@Composable {})
+                    """.trimIndent()
+                ),
+                "Main" to mapOf(
+                    "y/User.kt" to """
+                      package y
+                      import x.makeComposable
+                      import androidx.compose.runtime.Composable
+
+                      fun acceptComposable(composable: @Composable () -> Unit) {
+
+                      }
+
+                      fun test() {
+                        acceptComposable(makeComposable().single())
+                      }
+                    """.trimIndent()
+                )
+            ),
+        ) {
+            assert(
+                it.contains("INVOKESTATIC x/MakeComposableKt.makeComposable ()Ljava/util/List;")
+            )
+            assert(!it.contains("CHECKCAST kotlin/jvm/functions/Function0"))
+            assert(it.contains("CHECKCAST kotlin/jvm/functions/Function2"))
+        }
+    }
+
     @Test
     fun testInlineClassOverloading(): Unit = ensureSetup {
         compile(
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt
index 7e02920..e33bf13 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt
@@ -26,7 +26,7 @@
  * The Android Studio debugger searches for `ComposableSingletons` classes by name.
  * Any changes to the naming scheme have to be reflected in the Android Studio code.
  */
-class LambdaMemoizationRegressionTests : ComposeIrTransformTest() {
+class LambdaMemoizationRegressionTests : AbstractIrTransformTest() {
     @Test
     fun testNestedComposableSingletonsClass() = verifyComposeIrTransform(
         """
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 6856b56..31274fc 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -18,7 +18,7 @@
 
 import org.junit.Test
 
-class LambdaMemoizationTransformTests : ComposeIrTransformTest() {
+class LambdaMemoizationTransformTests : AbstractIrTransformTest() {
     @Test
     fun testCapturedThisFromFieldInitializer(): Unit = verifyComposeIrTransform(
         """
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
index f2c4d0d..792f46f 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralTransformTests.kt
@@ -16,15 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import androidx.compose.compiler.plugins.kotlin.lower.DurableKeyVisitor
-import androidx.compose.compiler.plugins.kotlin.lower.LiveLiteralTransformer
-import org.intellij.lang.annotations.Language
-import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
-import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.junit.Test
 
-class LiveLiteralTransformTests : AbstractIrTransformTest() {
+class LiveLiteralTransformTests : AbstractLiveLiteralTransformTests() {
+    override val liveLiteralsEnabled: Boolean
+        get() = true
 
     fun testSiblingCallArgs() = assertNoDuplicateKeys(
         """
@@ -372,7 +368,7 @@
                 val tmp0 = State%Int%fun-bar%class-%no-name-provided%%fun-a
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%fun-bar%class-%no-name-provided%%fun-a", Int%fun-bar%class-%no-name-provided%%fun-a)
-                  <set-State%Int%fun-bar%class-%no-name-provided%%fun-a>(tmp1)
+                  State%Int%fun-bar%class-%no-name-provided%%fun-a = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -424,7 +420,7 @@
                 val tmp0 = State%Int%arg-0%call-print%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-print%fun-A", Int%arg-0%call-print%fun-A)
-                  <set-State%Int%arg-0%call-print%fun-A>(tmp1)
+                  State%Int%arg-0%call-print%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -441,7 +437,7 @@
                 val tmp0 = State%String%arg-0%call-print-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("String%arg-0%call-print-1%fun-A", String%arg-0%call-print-1%fun-A)
-                  <set-State%String%arg-0%call-print-1%fun-A>(tmp1)
+                  State%String%arg-0%call-print-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -458,7 +454,7 @@
                 val tmp0 = State%Boolean%cond%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Boolean%cond%if%fun-A", Boolean%cond%if%fun-A)
-                  <set-State%Boolean%cond%if%fun-A>(tmp1)
+                  State%Boolean%cond%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -475,7 +471,7 @@
                 val tmp0 = State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%%this%call-plus%arg-0%call-print%branch%if%fun-A", Int%%this%call-plus%arg-0%call-print%branch%if%fun-A)
-                  <set-State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A>(tmp1)
+                  State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -492,7 +488,7 @@
                 val tmp0 = State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A", Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A)
-                  <set-State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A>(tmp1)
+                  State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -509,7 +505,7 @@
                 val tmp0 = State%Boolean%cond%if-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Boolean%cond%if-1%fun-A", Boolean%cond%if-1%fun-A)
-                  <set-State%Boolean%cond%if-1%fun-A>(tmp1)
+                  State%Boolean%cond%if-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -526,7 +522,7 @@
                 val tmp0 = State%Float%arg-0%call-print%branch%if-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Float%arg-0%call-print%branch%if-1%fun-A", Float%arg-0%call-print%branch%if-1%fun-A)
-                  <set-State%Float%arg-0%call-print%branch%if-1%fun-A>(tmp1)
+                  State%Float%arg-0%call-print%branch%if-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -543,7 +539,7 @@
                 val tmp0 = State%Int%arg-0%call-print-2%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-print-2%fun-A", Int%arg-0%call-print-2%fun-A)
-                  <set-State%Int%arg-0%call-print-2%fun-A>(tmp1)
+                  State%Int%arg-0%call-print-2%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -553,97 +549,4 @@
             }
         """
     )
-
-    private var builtKeys = mutableSetOf<String>()
-
-    override fun postProcessingStep(
-        module: IrModuleFragment,
-        context: IrPluginContext
-    ) {
-        val symbolRemapper = DeepCopySymbolRemapper()
-        val keyVisitor = DurableKeyVisitor(builtKeys)
-        val transformer = object : LiveLiteralTransformer(
-            true,
-            false,
-            keyVisitor,
-            context,
-            symbolRemapper,
-            ModuleMetricsImpl("temp")
-        ) {
-            override fun makeKeySet(): MutableSet<String> {
-                return super.makeKeySet().also { builtKeys = it }
-            }
-        }
-        transformer.lower(module)
-    }
-
-    // since the lowering will throw an exception if duplicate keys are found, all we have to do
-    // is run the lowering
-    private fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", src.replace('%', '$'))
-            )
-        )
-    }
-
-    // For a given src string, a
-    private fun assertKeys(vararg keys: String, makeSrc: () -> String) {
-        builtKeys = mutableSetOf()
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", makeSrc().replace('%', '$'))
-            )
-        )
-        assertEquals(
-            keys.toList().sorted().joinToString(separator = ",\n") {
-                "\"${it.replace('$', '%')}\""
-            },
-            builtKeys.toList().sorted().joinToString(separator = ",\n") {
-                "\"${it.replace('$', '%')}\""
-            }
-        )
-    }
-
-    // test: have two src strings (before/after) and assert that the keys of the params didn't change
-    private fun assertDurableChange(before: String, after: String) {
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", before.replace('%', '$'))
-            )
-        )
-        val beforeKeys = builtKeys
-
-        builtKeys = mutableSetOf()
-
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", after.replace('%', '$'))
-            )
-        )
-        val afterKeys = builtKeys
-
-        assertEquals(
-            beforeKeys.toList().sorted().joinToString(separator = "\n"),
-            afterKeys.toList().sorted().joinToString(separator = "\n")
-        )
-    }
-
-    private fun assertTransform(
-        unchecked: String,
-        checked: String,
-        expectedTransformed: String,
-        dumpTree: Boolean = false
-    ) = verifyComposeIrTransform(
-        """
-            import androidx.compose.runtime.Composable
-            $checked
-        """.trimIndent(),
-        expectedTransformed,
-        """
-            import androidx.compose.runtime.Composable
-            $unchecked
-        """.trimIndent(),
-        dumpTree = dumpTree
-    )
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
index 380be60..58c6696 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LiveLiteralV2TransformTests.kt
@@ -16,15 +16,11 @@
 
 package androidx.compose.compiler.plugins.kotlin
 
-import androidx.compose.compiler.plugins.kotlin.lower.DurableKeyVisitor
-import androidx.compose.compiler.plugins.kotlin.lower.LiveLiteralTransformer
-import org.intellij.lang.annotations.Language
-import org.jetbrains.kotlin.backend.common.extensions.IrPluginContext
-import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
-import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.junit.Test
 
-class LiveLiteralV2TransformTests : AbstractIrTransformTest() {
+class LiveLiteralV2TransformTests : AbstractLiveLiteralTransformTests() {
+    override val liveLiteralsV2Enabled: Boolean
+        get() = true
 
     fun testSiblingCallArgs() = assertNoDuplicateKeys(
         """
@@ -373,7 +369,7 @@
                 val tmp0 = State%Int%fun-bar%class-%no-name-provided%%fun-a
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%fun-bar%class-%no-name-provided%%fun-a", Int%fun-bar%class-%no-name-provided%%fun-a)
-                  <set-State%Int%fun-bar%class-%no-name-provided%%fun-a>(tmp1)
+                  State%Int%fun-bar%class-%no-name-provided%%fun-a = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -426,7 +422,7 @@
                 val tmp0 = State%Int%arg-0%call-print%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-print%fun-A", Int%arg-0%call-print%fun-A)
-                  <set-State%Int%arg-0%call-print%fun-A>(tmp1)
+                  State%Int%arg-0%call-print%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -443,7 +439,7 @@
                 val tmp0 = State%String%arg-0%call-print-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("String%arg-0%call-print-1%fun-A", String%arg-0%call-print-1%fun-A)
-                  <set-State%String%arg-0%call-print-1%fun-A>(tmp1)
+                  State%String%arg-0%call-print-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -460,7 +456,7 @@
                 val tmp0 = State%Boolean%cond%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Boolean%cond%if%fun-A", Boolean%cond%if%fun-A)
-                  <set-State%Boolean%cond%if%fun-A>(tmp1)
+                  State%Boolean%cond%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -477,7 +473,7 @@
                 val tmp0 = State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%%this%call-plus%arg-0%call-print%branch%if%fun-A", Int%%this%call-plus%arg-0%call-print%branch%if%fun-A)
-                  <set-State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A>(tmp1)
+                  State%Int%%this%call-plus%arg-0%call-print%branch%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -494,7 +490,7 @@
                 val tmp0 = State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A", Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A)
-                  <set-State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A>(tmp1)
+                  State%Int%arg-0%call-plus%arg-0%call-print%branch%if%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -511,7 +507,7 @@
                 val tmp0 = State%Boolean%cond%if-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Boolean%cond%if-1%fun-A", Boolean%cond%if-1%fun-A)
-                  <set-State%Boolean%cond%if-1%fun-A>(tmp1)
+                  State%Boolean%cond%if-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -528,7 +524,7 @@
                 val tmp0 = State%Float%arg-0%call-print%branch%if-1%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Float%arg-0%call-print%branch%if-1%fun-A", Float%arg-0%call-print%branch%if-1%fun-A)
-                  <set-State%Float%arg-0%call-print%branch%if-1%fun-A>(tmp1)
+                  State%Float%arg-0%call-print%branch%if-1%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -545,7 +541,7 @@
                 val tmp0 = State%Int%arg-0%call-print-2%fun-A
                 return if (tmp0 == null) {
                   val tmp1 = liveLiteral("Int%arg-0%call-print-2%fun-A", Int%arg-0%call-print-2%fun-A)
-                  <set-State%Int%arg-0%call-print-2%fun-A>(tmp1)
+                  State%Int%arg-0%call-print-2%fun-A = tmp1
                   tmp1
                 } else {
                   tmp0
@@ -555,97 +551,4 @@
             }
         """
     )
-
-    private var builtKeys = mutableSetOf<String>()
-
-    override fun postProcessingStep(
-        module: IrModuleFragment,
-        context: IrPluginContext
-    ) {
-        val symbolRemapper = DeepCopySymbolRemapper()
-        val keyVisitor = DurableKeyVisitor(builtKeys)
-        val transformer = object : LiveLiteralTransformer(
-            true,
-            true,
-            keyVisitor,
-            context,
-            symbolRemapper,
-            ModuleMetricsImpl("temp")
-        ) {
-            override fun makeKeySet(): MutableSet<String> {
-                return super.makeKeySet().also { builtKeys = it }
-            }
-        }
-        transformer.lower(module)
-    }
-
-    // since the lowering will throw an exception if duplicate keys are found, all we have to do
-    // is run the lowering
-    private fun assertNoDuplicateKeys(@Language("kotlin") src: String) {
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", src.replace('%', '$'))
-            )
-        )
-    }
-
-    // For a given src string, a
-    private fun assertKeys(vararg keys: String, makeSrc: () -> String) {
-        builtKeys = mutableSetOf()
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", makeSrc().replace('%', '$'))
-            )
-        )
-        assertEquals(
-            keys.toList().sorted().joinToString(separator = ",\n") {
-                "\"${it.replace('$', '%')}\""
-            },
-            builtKeys.toList().sorted().joinToString(separator = ",\n") {
-                "\"${it.replace('$', '%')}\""
-            }
-        )
-    }
-
-    // test: have two src strings (before/after) and assert that the keys of the params didn't change
-    private fun assertDurableChange(before: String, after: String) {
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", before.replace('%', '$'))
-            )
-        )
-        val beforeKeys = builtKeys
-
-        builtKeys = mutableSetOf()
-
-        JvmCompilation().compile(
-            listOf(
-                sourceFile("Test.kt", after.replace('%', '$'))
-            )
-        )
-        val afterKeys = builtKeys
-
-        assertEquals(
-            beforeKeys.toList().sorted().joinToString(separator = "\n"),
-            afterKeys.toList().sorted().joinToString(separator = "\n")
-        )
-    }
-
-    private fun assertTransform(
-        unchecked: String,
-        checked: String,
-        expectedTransformed: String,
-        dumpTree: Boolean = false
-    ) = verifyComposeIrTransform(
-        """
-            import androidx.compose.runtime.Composable
-            $checked
-        """.trimIndent(),
-        expectedTransformed,
-        """
-            import androidx.compose.runtime.Composable
-            $unchecked
-        """.trimIndent(),
-        dumpTree = dumpTree
-    )
-}
\ No newline at end of file
+}
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
index 154b042..26f1440 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/RememberIntrinsicTransformTests.kt
@@ -19,7 +19,7 @@
 import org.intellij.lang.annotations.Language
 import org.junit.Test
 
-class RememberIntrinsicTransformTests : ComposeIrTransformTest() {
+class RememberIntrinsicTransformTests : AbstractIrTransformTest() {
     override val intrinsicRememberEnabled: Boolean
         get() = true
 
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
index e201d0a..5f00b26 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/ScopeComposabilityTests.kt
@@ -152,10 +152,8 @@
 
         val ktFile = KtPsiFactory(environment.project).createFile(text)
         val bindingContext = JvmResolveUtil.analyzeAndCheckForErrors(
-            environment.project,
+            environment,
             listOf(ktFile),
-            environment.configuration,
-            environment::createPackagePartProvider
         ).bindingContext
 
         carets.forEachIndexed { index, (offset, marking) ->
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
index 10aac7d..76a1d26 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/StabilityPropagationTransformTests.kt
@@ -19,7 +19,7 @@
 import org.intellij.lang.annotations.Language
 import org.junit.Test
 
-class StabilityPropagationTransformTests : ComposeIrTransformTest() {
+class StabilityPropagationTransformTests : AbstractIrTransformTest() {
     private fun stabilityPropagation(
         @Language("kotlin")
         unchecked: String,
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
index ff87214..cfa4136 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
@@ -19,7 +19,7 @@
 import org.junit.Test
 
 @Suppress("SpellCheckingInspection") // Expected strings can have partial words
-class TargetAnnotationsTransformTests : ComposeIrTransformTest() {
+class TargetAnnotationsTransformTests : AbstractIrTransformTest() {
     @Test
     fun testInferUIFromCall() = verify(
         """
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 136d1d6..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
@@ -26,7 +26,7 @@
  * More complex cases tested in other IrTransform tests that use
  * the [TruncateTracingInfoMode.KEEP_INFO_STRING].
  */
-class TraceInformationTest : ComposeIrTransformTest() {
+class TraceInformationTest : AbstractIrTransformTest() {
     @Test
     fun testBasicComposableFunctions() = verifyComposeIrTransform(
         """
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
index fc6c275..2f620f9 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/analysis/ComposableDeclarationCheckerTests.kt
@@ -178,7 +178,7 @@
             interface Bar {
                 @Composable
                 fun composableFunction(param: Boolean): Boolean
-                val composableProperty: Boolean @Composable get()
+                @get:Composable val composableProperty: Boolean
                 fun nonComposableFunction(param: Boolean): Boolean
                 val nonComposableProperty: Boolean
             }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
index 71cf52f..17bc538 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposeIrGenerationExtension.kt
@@ -38,8 +38,6 @@
 import org.jetbrains.kotlin.backend.common.serialization.DeclarationTable
 import org.jetbrains.kotlin.backend.common.serialization.signature.IdSignatureSerializer
 import org.jetbrains.kotlin.backend.common.serialization.signature.PublicIdSignatureComputer
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.JVMConfigurationKeys
 import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsGlobalDeclarationTable
 import org.jetbrains.kotlin.ir.backend.js.lower.serialization.ir.JsManglerIr
 import org.jetbrains.kotlin.ir.declarations.IrModuleFragment
@@ -47,7 +45,6 @@
 import org.jetbrains.kotlin.platform.jvm.isJvm
 
 class ComposeIrGenerationExtension(
-    private val configuration: CompilerConfiguration,
     @Suppress("unused") private val liveLiteralsEnabled: Boolean = false,
     @Suppress("unused") private val liveLiteralsV2Enabled: Boolean = false,
     private val generateFunctionKeyMetaClasses: Boolean = false,
@@ -55,7 +52,8 @@
     private val intrinsicRememberEnabled: Boolean = true,
     private val decoysEnabled: Boolean = false,
     private val metricsDestination: String? = null,
-    private val reportsDestination: String? = null
+    private val reportsDestination: String? = null,
+    private val validateIr: Boolean = false,
 ) : IrGenerationExtension {
     var metrics: ModuleMetrics = EmptyModuleMetrics
 
@@ -68,7 +66,7 @@
 
         // Input check.  This should always pass, else something is horribly wrong upstream.
         // Necessary because oftentimes the issue is upstream (compiler bug, prior plugin, etc)
-        if (configuration.getBoolean(JVMConfigurationKeys.VALIDATE_IR))
+        if (validateIr)
             validateIr(moduleFragment, pluginContext.irBuiltIns)
 
         // create a symbol remapper to be used across all transforms
@@ -217,7 +215,7 @@
         }
 
         // Verify that our transformations didn't break something
-        if (configuration.getBoolean(JVMConfigurationKeys.VALIDATE_IR))
+        if (validateIr)
             validateIr(moduleFragment, pluginContext.irBuiltIns)
     }
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
index 53e3219..f465c25 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/ComposePlugin.kt
@@ -20,17 +20,18 @@
 import com.intellij.mock.MockProject
 import com.intellij.openapi.project.Project
 import org.jetbrains.kotlin.backend.common.extensions.IrGenerationExtension
+import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
+import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
 import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
 import org.jetbrains.kotlin.compiler.plugin.CliOption
 import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException
 import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor
 import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar
 import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
-import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
-import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
 import org.jetbrains.kotlin.config.CompilerConfigurationKey
+import org.jetbrains.kotlin.config.JVMConfigurationKeys
 import org.jetbrains.kotlin.config.KotlinCompilerVersion
+import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor
 import org.jetbrains.kotlin.extensions.internal.TypeResolutionInterceptor
 import org.jetbrains.kotlin.serialization.DescriptorSerializerPlugin
 
@@ -192,19 +193,14 @@
         project: MockProject,
         configuration: CompilerConfiguration
     ) {
-        registerProjectExtensions(
-            project as Project,
-            configuration
-        )
+        if (checkCompilerVersion(configuration)) {
+            registerCommonExtensions(project)
+            registerIrExtension(project, configuration)
+        }
     }
 
     companion object {
-
-        @Suppress("UNUSED_PARAMETER")
-        fun registerProjectExtensions(
-            project: Project,
-            configuration: CompilerConfiguration
-        ) {
+        fun checkCompilerVersion(configuration: CompilerConfiguration): Boolean {
             val KOTLIN_VERSION_EXPECTATION = "1.7.20"
             KotlinCompilerVersion.getVersion()?.let { version ->
                 val msgCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
@@ -256,47 +252,13 @@
                     // Return without registering the Compose plugin because the registration
                     // APIs may have changed and thus throw an exception during registration,
                     // preventing the diagnostic from being emitted.
-                    return
+                    return false
                 }
             }
+            return true
+        }
 
-            val liveLiteralsEnabled = configuration.get(
-                ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
-                false
-            )
-            val liveLiteralsV2Enabled = configuration.get(
-                ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY,
-                false
-            )
-            val generateFunctionKeyMetaClasses = configuration.get(
-                ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY,
-                false
-            )
-            val sourceInformationEnabled = configuration.get(
-                ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY,
-                false
-            )
-            val intrinsicRememberEnabled = configuration.get(
-                ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
-                true
-            )
-            val decoysEnabled = configuration.get(
-                ComposeConfiguration.DECOYS_ENABLED_KEY,
-                false
-            )
-            val metricsDestination = configuration.get(
-                ComposeConfiguration.METRICS_DESTINATION_KEY,
-                ""
-            ).let {
-                if (it.isBlank()) null else it
-            }
-            val reportsDestination = configuration.get(
-                ComposeConfiguration.REPORTS_DESTINATION_KEY,
-                ""
-            ).let {
-                if (it.isBlank()) null else it
-            }
-
+        fun registerCommonExtensions(project: Project) {
             StorageComponentContainerContributor.registerExtension(
                 project,
                 ComposableCallChecker()
@@ -319,10 +281,50 @@
                 @Suppress("IllegalExperimentalApiUsage")
                 ComposeTypeResolutionInterceptorExtension()
             )
+            DescriptorSerializerPlugin.registerExtension(
+                project,
+                ClassStabilityFieldSerializationPlugin()
+            )
+        }
+
+        fun registerIrExtension(
+            project: Project,
+            configuration: CompilerConfiguration
+        ) {
+            val liveLiteralsEnabled = configuration.getBoolean(
+                ComposeConfiguration.LIVE_LITERALS_ENABLED_KEY,
+            )
+            val liveLiteralsV2Enabled = configuration.getBoolean(
+                ComposeConfiguration.LIVE_LITERALS_V2_ENABLED_KEY,
+            )
+            val generateFunctionKeyMetaClasses = configuration.getBoolean(
+                ComposeConfiguration.GENERATE_FUNCTION_KEY_META_CLASSES_KEY,
+            )
+            val sourceInformationEnabled = configuration.getBoolean(
+                ComposeConfiguration.SOURCE_INFORMATION_ENABLED_KEY,
+            )
+            val intrinsicRememberEnabled = configuration.get(
+                ComposeConfiguration.INTRINSIC_REMEMBER_OPTIMIZATION_ENABLED_KEY,
+                true
+            )
+            val decoysEnabled = configuration.getBoolean(
+                ComposeConfiguration.DECOYS_ENABLED_KEY,
+            )
+            val metricsDestination = configuration.get(
+                ComposeConfiguration.METRICS_DESTINATION_KEY,
+                ""
+            ).ifBlank { null }
+            val reportsDestination = configuration.get(
+                ComposeConfiguration.REPORTS_DESTINATION_KEY,
+                ""
+            ).ifBlank { null }
+            val validateIr = configuration.getBoolean(
+                JVMConfigurationKeys.VALIDATE_IR
+            )
+
             IrGenerationExtension.registerExtension(
                 project,
                 ComposeIrGenerationExtension(
-                    configuration = configuration,
                     liveLiteralsEnabled = liveLiteralsEnabled,
                     liveLiteralsV2Enabled = liveLiteralsV2Enabled,
                     generateFunctionKeyMetaClasses = generateFunctionKeyMetaClasses,
@@ -331,12 +333,9 @@
                     decoysEnabled = decoysEnabled,
                     metricsDestination = metricsDestination,
                     reportsDestination = reportsDestination,
+                    validateIr = validateIr,
                 )
             )
-            DescriptorSerializerPlugin.registerExtension(
-                project,
-                ClassStabilityFieldSerializationPlugin()
-            )
         }
     }
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
index 7ea2d2b..33fecf3f8 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/AbstractComposeLowering.kt
@@ -134,7 +134,6 @@
 import org.jetbrains.kotlin.ir.util.fqNameForIrSerialization
 import org.jetbrains.kotlin.ir.util.functions
 import org.jetbrains.kotlin.ir.util.getArguments
-import org.jetbrains.kotlin.ir.util.getPrimitiveArrayElementType
 import org.jetbrains.kotlin.ir.util.getPropertyGetter
 import org.jetbrains.kotlin.ir.util.hasAnnotation
 import org.jetbrains.kotlin.ir.util.isCrossinline
@@ -788,26 +787,25 @@
         null
     )
 
-    @OptIn(ObsoleteDescriptorBasedAPI::class)
     protected fun irForLoop(
         elementType: IrType,
         subject: IrExpression,
         loopBody: (IrValueDeclaration) -> IrExpression
     ): IrStatement {
-        val primitiveType = subject.type.getPrimitiveArrayElementType()
-        val iteratorSymbol = primitiveType?.let {
-            context.symbols.primitiveIteratorsByType[it]
-        } ?: context.symbols.iterator
-        val unitType = context.irBuiltIns.unitType
-
         val getIteratorFunction = subject.type.classOrNull!!.owner.functions
             .single { it.name.asString() == "iterator" }
 
-        val iteratorType = iteratorSymbol.typeWith(elementType)
+        val iteratorSymbol = getIteratorFunction.returnType.classOrNull!!
+        val iteratorType = if (iteratorSymbol.owner.typeParameters.isNotEmpty()) {
+            iteratorSymbol.typeWith(elementType)
+        } else {
+            iteratorSymbol.defaultType
+        }
+
         val nextSymbol = iteratorSymbol.owner.functions
-            .single { it.descriptor.name.asString() == "next" }
+            .single { it.name.asString() == "next" }
         val hasNextSymbol = iteratorSymbol.owner.functions
-            .single { it.descriptor.name.asString() == "hasNext" }
+            .single { it.name.asString() == "hasNext" }
 
         val call = IrCallImpl(
             UNDEFINED_OFFSET,
@@ -829,14 +827,14 @@
             origin = IrDeclarationOrigin.FOR_LOOP_ITERATOR
         )
         return irBlock(
-            type = unitType,
+            type = builtIns.unitType,
             origin = IrStatementOrigin.FOR_LOOP,
             statements = listOf(
                 iteratorVar,
                 IrWhileLoopImpl(
                     UNDEFINED_OFFSET,
                     UNDEFINED_OFFSET,
-                    unitType,
+                    builtIns.unitType,
                     IrStatementOrigin.FOR_LOOP_INNER_WHILE
                 ).apply {
                     val loopVar = irTemporary(
@@ -862,7 +860,7 @@
                         dispatchReceiver = irGet(iteratorVar)
                     )
                     body = irBlock(
-                        type = unitType,
+                        type = builtIns.unitType,
                         origin = IrStatementOrigin.FOR_LOOP_INNER_WHILE,
                         statements = listOf(
                             loopVar,
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
index 62ab2de..ce8b7aa 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposableTypeRemapper.kt
@@ -43,8 +43,10 @@
 import org.jetbrains.kotlin.ir.expressions.IrCall
 import org.jetbrains.kotlin.ir.expressions.IrConstructorCall
 import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression
+import org.jetbrains.kotlin.ir.expressions.IrWhen
 import org.jetbrains.kotlin.ir.expressions.impl.IrCallImpl
 import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl
+import org.jetbrains.kotlin.ir.expressions.impl.IrIfThenElseImpl
 import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
 import org.jetbrains.kotlin.ir.types.IrSimpleType
 import org.jetbrains.kotlin.ir.types.IrType
@@ -56,6 +58,7 @@
 import org.jetbrains.kotlin.ir.types.impl.IrTypeAbbreviationImpl
 import org.jetbrains.kotlin.ir.types.impl.makeTypeProjection
 import org.jetbrains.kotlin.ir.types.isClassWithFqName
+import org.jetbrains.kotlin.ir.types.typeOrNull
 import org.jetbrains.kotlin.ir.util.DeepCopyIrTreeWithSymbols
 import org.jetbrains.kotlin.ir.util.DeepCopySymbolRemapper
 import org.jetbrains.kotlin.ir.util.SymbolRemapper
@@ -128,6 +131,22 @@
         }
     }
 
+    override fun visitWhen(expression: IrWhen): IrWhen {
+        if (expression is IrIfThenElseImpl) {
+            return IrIfThenElseImpl(
+                expression.startOffset,
+                expression.endOffset,
+                expression.type.remapType(),
+                mapStatementOrigin(expression.origin),
+            ).also {
+                expression.branches.mapTo(it.branches) { branch ->
+                    branch.transform()
+                }
+            }.copyAttributes(expression)
+        }
+        return super.visitWhen(expression)
+    }
+
     override fun visitConstructorCall(expression: IrConstructorCall): IrConstructorCall {
         if (!expression.symbol.isBound)
             (context as IrPluginContextImpl).linker.getDeclaration(expression.symbol)
@@ -139,7 +158,7 @@
         if (
             ownerFn != null &&
             ownerFn.origin == IrDeclarationOrigin.IR_EXTERNAL_DECLARATION_STUB &&
-            ownerFn.hasComposableArguments()
+            ownerFn.needsComposableRemapping()
         ) {
             if (symbolRemapper.getReferencedConstructor(ownerFn.symbol) == ownerFn.symbol) {
                 // Not remapped yet, so remap now.
@@ -167,18 +186,27 @@
         return super.visitConstructorCall(expression)
     }
 
-    private fun IrFunction.hasComposableArguments(): Boolean {
+    private fun IrFunction.needsComposableRemapping(): Boolean {
         if (
-            dispatchReceiverParameter?.type?.isComposable() == true ||
-            extensionReceiverParameter?.type?.isComposable() == true
+            needsComposableRemapping(dispatchReceiverParameter?.type) ||
+            needsComposableRemapping(extensionReceiverParameter?.type) ||
+            needsComposableRemapping(returnType)
         ) return true
 
         for (param in valueParameters) {
-            if (param.type.isComposable()) return true
+            if (needsComposableRemapping(param.type)) return true
         }
         return false
     }
 
+    private fun needsComposableRemapping(type: IrType?): Boolean {
+        if (type == null) return false
+        if (type !is IrSimpleType) return false
+        if (type.isComposable()) return true
+        if (type.arguments.any { needsComposableRemapping(it.typeOrNull) }) return true
+        return false
+    }
+
     @OptIn(ObsoleteDescriptorBasedAPI::class)
     override fun visitCall(expression: IrCall): IrCall {
         val ownerFn = expression.symbol.owner as? IrSimpleFunction
@@ -247,7 +275,7 @@
                 // avoid java properties since they go through a different lowering and it is
                 // also impossible for them to have composable types
                 if (property.origin != IrDeclarationOrigin.IR_EXTERNAL_JAVA_DECLARATION_STUB &&
-                    property.getter?.returnType?.isComposable() == true
+                    property.getter?.needsComposableRemapping() == true
                 ) {
                     if (symbolRemapper.getReferencedProperty(property.symbol) == property.symbol) {
                         // Not remapped yet, so remap now.
@@ -261,7 +289,7 @@
                         }
                     }
                 }
-            } else if (ownerFn.hasComposableArguments()) {
+            } else if (ownerFn.needsComposableRemapping()) {
                 if (symbolRemapper.getReferencedSimpleFunction(ownerFn.symbol) == ownerFn.symbol) {
                     // Not remapped yet, so remap now.
                     // Remap only once to avoid IdSignature clash (on k/js 1.7.20).
@@ -281,7 +309,7 @@
 
         if (
             ownerFn != null &&
-            ownerFn.hasComposableArguments()
+            ownerFn.needsComposableRemapping()
         ) {
             val newFn = visitSimpleFunction(ownerFn).also {
                 it.overriddenSymbols = ownerFn.overriddenSymbols.map { override ->
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
index 169258c..df85536 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/LiveLiteralTransformer.kt
@@ -270,6 +270,7 @@
                 visibility = DescriptorVisibilities.PRIVATE
                 origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR
             }.also { fn ->
+                fn.correspondingPropertySymbol = p.symbol
                 val thisParam = clazz.thisReceiver!!.copyTo(fn)
                 fn.dispatchReceiverParameter = thisParam
                 fn.body = DeclarationIrBuilder(context, fn.symbol).irBlockBody {
@@ -296,6 +297,7 @@
                 visibility = DescriptorVisibilities.PRIVATE
                 origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR
             }.also { fn ->
+                fn.correspondingPropertySymbol = p.symbol
                 val thisParam = clazz.thisReceiver!!.copyTo(fn)
                 fn.dispatchReceiverParameter = thisParam
                 fn.body = DeclarationIrBuilder(context, fn.symbol).irBlockBody {
@@ -307,6 +309,7 @@
                 visibility = DescriptorVisibilities.PRIVATE
                 origin = IrDeclarationOrigin.DEFAULT_PROPERTY_ACCESSOR
             }.also { fn ->
+                fn.correspondingPropertySymbol = p.symbol
                 val thisParam = clazz.thisReceiver!!.copyTo(fn)
                 fn.dispatchReceiverParameter = thisParam
                 val valueParam = fn.addValueParameter("value", stateType)
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
index 48800c8..30ed0df 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Column.kt
@@ -99,21 +99,21 @@
 internal fun columnMeasurePolicy(
     verticalArrangement: Arrangement.Vertical,
     horizontalAlignment: Alignment.Horizontal
-) = remember(verticalArrangement, horizontalAlignment) {
-    if (verticalArrangement == Arrangement.Top && horizontalAlignment == Alignment.Start) {
+) = if (verticalArrangement == Arrangement.Top && horizontalAlignment == Alignment.Start) {
         DefaultColumnMeasurePolicy
     } else {
-        rowColumnMeasurePolicy(
-            orientation = LayoutOrientation.Vertical,
-            arrangement = { totalSize, size, _, density, outPosition ->
-                with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
-            },
-            arrangementSpacing = verticalArrangement.spacing,
-            crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment),
-            crossAxisSize = SizeMode.Wrap
-        )
+        remember(verticalArrangement, horizontalAlignment) {
+            rowColumnMeasurePolicy(
+                orientation = LayoutOrientation.Vertical,
+                arrangement = { totalSize, size, _, density, outPosition ->
+                    with(verticalArrangement) { density.arrange(totalSize, size, outPosition) }
+                },
+                arrangementSpacing = verticalArrangement.spacing,
+                crossAxisAlignment = CrossAxisAlignment.horizontal(horizontalAlignment),
+                crossAxisSize = SizeMode.Wrap
+            )
+        }
     }
-}
 
 /**
  * Scope for the children of [Column].
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
index b6a6697..7a99435 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/Row.kt
@@ -103,23 +103,23 @@
 internal fun rowMeasurePolicy(
     horizontalArrangement: Arrangement.Horizontal,
     verticalAlignment: Alignment.Vertical
-) = remember(horizontalArrangement, verticalAlignment) {
-    if (horizontalArrangement == Arrangement.Start && verticalAlignment == Alignment.Top) {
+) = if (horizontalArrangement == Arrangement.Start && verticalAlignment == Alignment.Top) {
         DefaultRowMeasurePolicy
     } else {
-        rowColumnMeasurePolicy(
-            orientation = LayoutOrientation.Horizontal,
-            arrangement = { totalSize, size, layoutDirection, density, outPosition ->
-                with(horizontalArrangement) {
-                    density.arrange(totalSize, size, layoutDirection, outPosition)
-                }
-            },
-            arrangementSpacing = horizontalArrangement.spacing,
-            crossAxisAlignment = CrossAxisAlignment.vertical(verticalAlignment),
-            crossAxisSize = SizeMode.Wrap
-        )
+        remember(horizontalArrangement, verticalAlignment) {
+            rowColumnMeasurePolicy(
+                orientation = LayoutOrientation.Horizontal,
+                arrangement = { totalSize, size, layoutDirection, density, outPosition ->
+                    with(horizontalArrangement) {
+                        density.arrange(totalSize, size, layoutDirection, outPosition)
+                    }
+                },
+                arrangementSpacing = horizontalArrangement.spacing,
+                crossAxisAlignment = CrossAxisAlignment.vertical(verticalAlignment),
+                crossAxisSize = SizeMode.Wrap
+            )
+        }
     }
-}
 
 /**
  * Scope for the children of [Row].
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 7c40323..1a91794 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,3 +1,9 @@
 // Baseline format: 1.0
 RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
     Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
+
+
+RemovedMethod: androidx.compose.foundation.gestures.TapGestureDetectorKt#awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, boolean, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>):
+    Removed method androidx.compose.foundation.gestures.TapGestureDetectorKt.awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope,boolean,kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>)
+RemovedMethod: androidx.compose.foundation.gestures.TapGestureDetectorKt#waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>):
+    Removed method androidx.compose.foundation.gestures.TapGestureDetectorKt.waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope,kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>)
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index 15b1f37..28d77a3 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -256,9 +256,11 @@
   }
 
   public final class TapGestureDetectorKt {
-    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed);
     method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onTap, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope);
   }
 
   public final class TransformGestureDetectorKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 361a9e6..2b2aca4 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -309,9 +309,11 @@
   }
 
   public final class TapGestureDetectorKt {
-    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed);
     method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onTap, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope);
   }
 
   public final class TransformGestureDetectorKt {
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 7c40323..1a91794 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,3 +1,9 @@
 // Baseline format: 1.0
 RemovedClass: androidx.compose.foundation.text.MaxLinesHeightModifierKt:
     Removed class androidx.compose.foundation.text.MaxLinesHeightModifierKt
+
+
+RemovedMethod: androidx.compose.foundation.gestures.TapGestureDetectorKt#awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, boolean, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>):
+    Removed method androidx.compose.foundation.gestures.TapGestureDetectorKt.awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope,boolean,kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>)
+RemovedMethod: androidx.compose.foundation.gestures.TapGestureDetectorKt#waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>):
+    Removed method androidx.compose.foundation.gestures.TapGestureDetectorKt.waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope,kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>)
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index 15b1f37..28d77a3 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -256,9 +256,11 @@
   }
 
   public final class TapGestureDetectorKt {
-    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! awaitFirstDown(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional boolean requireUnconsumed);
     method public static suspend Object? detectTapGestures(androidx.compose.ui.input.pointer.PointerInputScope, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onDoubleTap, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onLongPress, optional kotlin.jvm.functions.Function3<? super androidx.compose.foundation.gestures.PressGestureScope,? super androidx.compose.ui.geometry.Offset,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> onPress, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.geometry.Offset,kotlin.Unit>? onTap, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
-    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method public static suspend Object? waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope, optional androidx.compose.ui.input.pointer.PointerEventPass pass, optional kotlin.coroutines.Continuation<? super androidx.compose.ui.input.pointer.PointerInputChange>);
+    method @Deprecated public static suspend androidx.compose.ui.input.pointer.PointerInputChange! waitForUpOrCancellation(androidx.compose.ui.input.pointer.AwaitPointerEventScope);
   }
 
   public final class TransformGestureDetectorKt {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
index 21ce522..c34d2b2 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/BaseLazyStaggeredGridWithOrientation.kt
@@ -62,9 +62,30 @@
         crossAxisArrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(0.dp),
         content: LazyStaggeredGridScope.() -> Unit,
     ) {
+        LazyStaggeredGrid(
+            StaggeredGridCells.Fixed(lanes),
+            modifier,
+            state,
+            contentPadding,
+            mainAxisArrangement,
+            crossAxisArrangement,
+            content
+        )
+    }
+
+    @Composable
+    internal fun LazyStaggeredGrid(
+        cells: StaggeredGridCells,
+        modifier: Modifier = Modifier,
+        state: LazyStaggeredGridState = rememberLazyStaggeredGridState(),
+        contentPadding: PaddingValues = PaddingValues(0.dp),
+        mainAxisArrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(0.dp),
+        crossAxisArrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(0.dp),
+        content: LazyStaggeredGridScope.() -> Unit,
+    ) {
         if (orientation == Orientation.Vertical) {
             LazyVerticalStaggeredGrid(
-                columns = StaggeredGridCells.Fixed(lanes),
+                columns = cells,
                 modifier = modifier,
                 contentPadding = contentPadding,
                 verticalArrangement = mainAxisArrangement,
@@ -74,7 +95,7 @@
             )
         } else {
             LazyHorizontalStaggeredGrid(
-                rows = StaggeredGridCells.Fixed(lanes),
+                rows = cells,
                 modifier = modifier,
                 contentPadding = contentPadding,
                 verticalArrangement = crossAxisArrangement,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
index ea72295..fc86b6b 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridTest.kt
@@ -97,12 +97,35 @@
     }
 
     @Test
+    fun showsZeroItems() {
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+
+            LazyStaggeredGrid(
+                lanes = 3,
+                state = state,
+                modifier = Modifier.testTag(LazyStaggeredGridTag)
+            ) { }
+        }
+
+        rule.onNodeWithTag(LazyStaggeredGridTag)
+            .onChildren()
+            .assertCountEquals(0)
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+    }
+
+    @Test
     fun showsOneItem() {
         val itemTestTag = "itemTestTag"
 
         rule.setContent {
+            state = rememberLazyStaggeredGridState()
+
             LazyStaggeredGrid(
                 lanes = 3,
+                state = state,
             ) {
                 item {
                     Spacer(
@@ -116,6 +139,9 @@
 
         rule.onNodeWithTag(itemTestTag)
             .assertIsDisplayed()
+
+        assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+        assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
     }
 
     @Test
@@ -1011,6 +1037,120 @@
     }
 
     @Test
+    fun screenRotate_oneItem_withAdaptiveCells_fillsContentCorrectly() {
+        var rotated by mutableStateOf(false)
+
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+
+            val crossAxis = if (!rotated) itemSizeDp * 6 else itemSizeDp * 9
+            val mainAxis = if (!rotated) itemSizeDp * 9 else itemSizeDp * 6
+
+            LazyStaggeredGrid(
+                cells = StaggeredGridCells.Adaptive(itemSizeDp * 3),
+                state = state,
+                modifier = Modifier
+                    .mainAxisSize(mainAxis)
+                    .crossAxisSize(crossAxis)
+                    .testTag(LazyStaggeredGridTag)
+            ) {
+                item {
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(itemSizeDp)
+                            .testTag("0")
+                    )
+                }
+            }
+        }
+
+        fun verifyState() {
+            assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+            assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+        }
+
+        rule.runOnIdle {
+            rotated = true
+        }
+
+        rule.runOnIdle {
+            verifyState()
+        }
+
+        rule.runOnIdle {
+            rotated = false
+        }
+
+        rule.runOnIdle {
+            verifyState()
+        }
+
+        rule.runOnIdle {
+            rotated = true
+        }
+
+        rule.runOnIdle {
+            verifyState()
+        }
+    }
+
+    @Test
+    fun screenRotate_twoItems_withAdaptiveCells_fillsContentCorrectly() {
+        var rotated by mutableStateOf(false)
+
+        rule.setContent {
+            state = rememberLazyStaggeredGridState()
+
+            val crossAxis = if (!rotated) itemSizeDp * 6 else itemSizeDp * 9
+            val mainAxis = if (!rotated) itemSizeDp * 9 else itemSizeDp * 6
+
+            LazyStaggeredGrid(
+                cells = StaggeredGridCells.Adaptive(itemSizeDp * 3),
+                state = state,
+                modifier = Modifier
+                    .mainAxisSize(mainAxis)
+                    .crossAxisSize(crossAxis)
+                    .testTag(LazyStaggeredGridTag)
+            ) {
+                items(2) {
+                    Spacer(
+                        Modifier
+                            .mainAxisSize(itemSizeDp)
+                            .testTag("$it")
+                    )
+                }
+            }
+        }
+
+        fun verifyState() {
+            rule.runOnIdle {
+                assertThat(state.firstVisibleItemIndex).isEqualTo(0)
+                assertThat(state.firstVisibleItemScrollOffset).isEqualTo(0)
+            }
+            rule.onNodeWithTag("0").assertIsDisplayed()
+            rule.onNodeWithTag("1").assertIsDisplayed()
+        }
+
+        rule.runOnIdle {
+            rotated = true
+        }
+
+        verifyState()
+
+        rule.runOnIdle {
+            rotated = false
+        }
+
+        verifyState()
+
+        rule.runOnIdle {
+            rotated = true
+        }
+
+        verifyState()
+    }
+
+    @Test
     fun scrollingALot_layoutIsNotRecomposed() {
         var recomposed = 0
         rule.setContent {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
index cab90e0..f6ea83a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextLayoutTest.kt
@@ -16,10 +16,11 @@
 
 package androidx.compose.foundation.text
 
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
 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.layout.FirstBaseline
 import androidx.compose.ui.layout.IntrinsicMeasurable
@@ -31,134 +32,83 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.node.Ref
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
-import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 class TextLayoutTest {
-    @Suppress("DEPRECATION")
     @get:Rule
-    internal val activityTestRule = androidx.test.rule.ActivityTestRule(
-        ComponentActivity::class.java
-    )
-    private lateinit var activity: ComponentActivity
-    private lateinit var density: Density
-
-    @Before
-    fun setup() {
-        activity = activityTestRule.activity
-        density = Density(activity)
-    }
+    val rule = createComposeRule()
 
     @Test
-    fun testTextLayout() = with(density) {
-        val layoutLatch = CountDownLatch(2)
+    fun textLayout() {
         val textSize = Ref<IntSize>()
         val doubleTextSize = Ref<IntSize>()
-        show {
+        rule.setContent {
             TestingText(
                 "aa",
                 modifier = Modifier.onGloballyPositioned { coordinates ->
                     textSize.value = coordinates.size
-                    layoutLatch.countDown()
                 }
             )
             TestingText(
                 "aaaa",
                 modifier = Modifier.onGloballyPositioned { coordinates ->
                     doubleTextSize.value = coordinates.size
-                    layoutLatch.countDown()
                 }
             )
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        assertThat(textSize.value).isNotNull()
-        assertThat(doubleTextSize.value).isNotNull()
-        assertThat(textSize.value!!.width).isGreaterThan(0)
-        assertThat(textSize.value!!.height).isGreaterThan(0)
-        assertThat(textSize.value!!.width * 2).isEqualTo(doubleTextSize.value!!.width)
-        assertThat(textSize.value!!.height).isEqualTo(doubleTextSize.value!!.height)
+
+        rule.runOnIdle {
+            assertThat(textSize.value).isNotNull()
+            assertThat(doubleTextSize.value).isNotNull()
+            assertThat(textSize.value!!.width).isGreaterThan(0)
+            assertThat(textSize.value!!.height).isGreaterThan(0)
+            assertThat(textSize.value!!.width * 2).isEqualTo(doubleTextSize.value!!.width)
+            assertThat(textSize.value!!.height).isEqualTo(doubleTextSize.value!!.height)
+        }
     }
 
     @Test
-    fun testTextLayout_intrinsicMeasurements() = with(density) {
-        val layoutLatch = CountDownLatch(2)
+    fun textLayout_intrinsicMeasurements() {
         val textSize = Ref<IntSize>()
         val doubleTextSize = Ref<IntSize>()
-        show {
+        var textMeasurable by mutableStateOf<Measurable?>(null)
+
+        rule.setContent {
             TestingText(
                 "aa ",
-                modifier = Modifier.onGloballyPositioned { coordinates ->
-                    textSize.value = coordinates.size
-                    layoutLatch.countDown()
-                }
+                modifier = Modifier.onSizeChanged { textSize.value = it }
             )
             TestingText(
                 "aa aa ",
-                modifier = Modifier.onGloballyPositioned { coordinates ->
-                    doubleTextSize.value = coordinates.size
-                    layoutLatch.countDown()
-                }
+                modifier = Modifier.onSizeChanged { doubleTextSize.value = it }
             )
-        }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        val textWidth = textSize.value!!.width
-        val textHeight = textSize.value!!.height
-        val doubleTextWidth = doubleTextSize.value!!.width
 
-        val intrinsicsLatch = CountDownLatch(1)
-        show {
-            val text = @Composable {
-                TestingText("aa aa ")
-            }
-            val measurePolicy = remember {
-                object : MeasurePolicy {
+            Layout(
+                content = {
+                    TestingText("aa aa ")
+                },
+                measurePolicy = object : MeasurePolicy {
                     override fun MeasureScope.measure(
                         measurables: List<Measurable>,
                         constraints: Constraints
                     ): MeasureResult {
-                        val textMeasurable = measurables.first()
-                        // Min width.
-                        assertThat(textWidth).isEqualTo(textMeasurable.minIntrinsicWidth(0))
-                        // Min height.
-                        assertThat(textMeasurable.minIntrinsicHeight(textWidth))
-                            .isGreaterThan(textHeight)
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.minIntrinsicHeight(doubleTextWidth))
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.minIntrinsicHeight(Constraints.Infinity))
-                        // Max width.
-                        assertThat(doubleTextWidth).isEqualTo(textMeasurable.maxIntrinsicWidth(0))
-                        // Max height.
-                        assertThat(textMeasurable.maxIntrinsicHeight(textWidth))
-                            .isGreaterThan(textHeight)
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.maxIntrinsicHeight(doubleTextWidth))
-                        assertThat(textHeight)
-                            .isEqualTo(textMeasurable.maxIntrinsicHeight(Constraints.Infinity))
-
-                        intrinsicsLatch.countDown()
-
+                        textMeasurable = measurables.first()
                         return layout(0, 0) {}
                     }
 
@@ -182,79 +132,94 @@
                         width: Int
                     ) = 0
                 }
-            }
-            Layout(
-                content = text,
-                measurePolicy = measurePolicy
             )
         }
-        assertThat(intrinsicsLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            val textWidth = textSize.value!!.width
+            val textHeight = textSize.value!!.height
+            val doubleTextWidth = doubleTextSize.value!!.width
+
+            textMeasurable!!.let { textMeasurable ->
+                // Min width.
+                assertThat(textWidth).isEqualTo(textMeasurable.minIntrinsicWidth(0))
+                // Min height.
+                assertThat(textMeasurable.minIntrinsicHeight(textWidth))
+                    .isGreaterThan(textHeight)
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.minIntrinsicHeight(doubleTextWidth))
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.minIntrinsicHeight(Constraints.Infinity))
+
+                // Max width.
+                assertThat(doubleTextWidth).isEqualTo(textMeasurable.maxIntrinsicWidth(0))
+                // Max height.
+                assertThat(textMeasurable.maxIntrinsicHeight(textWidth))
+                    .isGreaterThan(textHeight)
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.maxIntrinsicHeight(doubleTextWidth))
+                assertThat(textHeight)
+                    .isEqualTo(textMeasurable.maxIntrinsicHeight(Constraints.Infinity))
+            }
+        }
     }
 
     @Test
-    fun testTextLayout_providesBaselines() = with(density) {
-        val layoutLatch = CountDownLatch(2)
-        show {
-            val text = @Composable {
+    fun textLayout_providesBaselines_whenUnconstrained() {
+        var firstBaseline by mutableStateOf(-1)
+        var lastBaseline by mutableStateOf(-1)
+
+        rule.setContent {
+            Layout({
                 TestingText("aa")
-            }
-            Layout(text) { measurables, _ ->
+            }) { measurables, _ ->
                 val placeable = measurables.first().measure(Constraints())
-                assertThat(placeable[FirstBaseline]).isNotNull()
-                assertThat(placeable[LastBaseline]).isNotNull()
-                assertThat(placeable[FirstBaseline]).isEqualTo(placeable[LastBaseline])
-                layoutLatch.countDown()
-                layout(0, 0) {}
-            }
-            Layout(text) { measurables, _ ->
-                val placeable = measurables.first().measure(Constraints(maxWidth = 0))
-                assertThat(placeable[FirstBaseline]).isNotNull()
-                assertThat(placeable[LastBaseline]).isNotNull()
-                assertThat(placeable[FirstBaseline])
-                    .isLessThan(placeable[LastBaseline])
-                layoutLatch.countDown()
+                firstBaseline = placeable[FirstBaseline]
+                lastBaseline = placeable[LastBaseline]
                 layout(0, 0) {}
             }
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        rule.runOnIdle {
+            assertThat(firstBaseline).isGreaterThan(-1)
+            assertThat(lastBaseline).isGreaterThan(-1)
+            assertThat(firstBaseline).isEqualTo(lastBaseline)
+        }
     }
 
     @Test
-    fun testOnTextLayout() = with(density) {
-        val layoutLatch = CountDownLatch(1)
-        val callback = mock<(TextLayoutResult) -> Unit>()
-        show {
-            val text = @Composable {
-                TestingText("aa", onTextLayout = callback)
-            }
-            Layout(text) { measurables, _ ->
-                measurables.first().measure(Constraints())
-                layoutLatch.countDown()
+    fun textLayout_providesBaselines_whenZeroMaxWidth() {
+        var firstBaseline by mutableStateOf(-1)
+        var lastBaseline by mutableStateOf(-1)
+
+        rule.setContent {
+            Layout({
+                TestingText("aa")
+            }) { measurables, _ ->
+                val placeable = measurables.first().measure(Constraints(maxWidth = 0))
+                firstBaseline = placeable[FirstBaseline]
+                lastBaseline = placeable[LastBaseline]
                 layout(0, 0) {}
             }
         }
-        assertThat(layoutLatch.await(1, TimeUnit.SECONDS)).isTrue()
-        verify(callback, times(1)).invoke(any())
+
+        rule.runOnIdle {
+            assertThat(firstBaseline).isGreaterThan(-1)
+            assertThat(lastBaseline).isGreaterThan(-1)
+            assertThat(firstBaseline).isLessThan(lastBaseline)
+        }
     }
 
-    private fun show(composable: @Composable () -> Unit) {
-        val runnable = Runnable {
-            activity.setContent {
-                Layout(composable) { measurables, constraints ->
-                    val placeables = measurables.map {
-                        it.measure(constraints.copy(minWidth = 0, minHeight = 0))
-                    }
-                    layout(constraints.maxWidth, constraints.maxHeight) {
-                        var top = 0
-                        placeables.forEach {
-                            it.placeRelative(0, top)
-                            top += it.height
-                        }
-                    }
-                }
-            }
+    @Test
+    fun textLayout_OnTextLayoutCallback() {
+        val resultsFromCallback = mutableListOf<TextLayoutResult>()
+        rule.setContent {
+            TestingText("aa", onTextLayout = { resultsFromCallback += it })
         }
-        activityTestRule.runOnUiThread(runnable)
+
+        rule.runOnIdle {
+            assertThat(resultsFromCallback).hasSize(1)
+        }
     }
 }
 
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt
new file mode 100644
index 0000000..b5b83db
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/TextStyleInvalidationTest.kt
@@ -0,0 +1,348 @@
+/*
+ * 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.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.ExperimentalTextApi
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.font.FontSynthesis
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.intl.LocaleList
+import androidx.compose.ui.text.style.BaselineShift
+import androidx.compose.ui.text.style.Hyphens
+import androidx.compose.ui.text.style.LineBreak
+import androidx.compose.ui.text.style.LineHeightStyle
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextDirection
+import androidx.compose.ui.text.style.TextGeometricTransform
+import androidx.compose.ui.text.style.TextIndent
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.sp
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
+
+@Suppress("DEPRECATION")
+@RunWith(Parameterized::class)
+@MediumTest
+class TextStyleInvalidationTest(private val config: Config) {
+
+    class Config(
+        private val description: String,
+        val updateStyle: (TextStyle) -> TextStyle,
+        val initializeStyle: (TextStyle) -> TextStyle = { it },
+        val invalidatesMeasure: Boolean = false,
+        val invalidatesPlacement: Boolean = false,
+        val invalidatesDraw: Boolean = false,
+    ) {
+        override fun toString(): String = buildString {
+            append(description)
+            listOfNotNull(
+                "measure".takeIf { invalidatesMeasure },
+                "placement".takeIf { invalidatesPlacement },
+                "draw".takeIf { invalidatesDraw },
+            ).joinTo(this, prefix = " ", separator = ", ") { "invalidates $it" }
+        }
+    }
+
+    @OptIn(ExperimentalTextApi::class)
+    companion object {
+        @Parameters(name = "{0}")
+        @JvmStatic
+        fun parameters() = arrayOf(
+            Config("nothing", { it }),
+            Config(
+                "color",
+                { it.copy(color = Color.Blue) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "to brush",
+                { it.copy(brush = Brush.verticalGradient(0f to Color.Blue, 1f to Color.Magenta)) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "from brush to brush",
+                initializeStyle = {
+                    it.copy(brush = Brush.verticalGradient(0f to Color.Black, 1f to Color.Magenta))
+                },
+                updateStyle = {
+                    it.copy(brush = Brush.verticalGradient(0f to Color.Blue, 1f to Color.Magenta))
+                },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "alpha",
+                initializeStyle = {
+                    it.copy(
+                        alpha = 1f,
+                        brush = Brush.verticalGradient(0f to Color.Blue, 1f to Color.Magenta)
+                    )
+                },
+                updateStyle = { it.copy(alpha = 0.5f, brush = it.brush) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "fontSize",
+                { it.copy(fontSize = it.fontSize * 2) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "fontWeight",
+                { it.copy(fontWeight = FontWeight(it.fontWeight!!.weight * 2)) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "fontStyle",
+                { it.copy(fontStyle = FontStyle.Italic) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true
+            ),
+            Config(
+                "fontSynthesis",
+                { it.copy(fontSynthesis = FontSynthesis.All) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "fontFamily",
+                { it.copy(fontFamily = FontFamily.Cursive) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "fontFeatureSettings",
+                initializeStyle = { it.copy(fontFeatureSettings = "a") },
+                updateStyle = { it.copy(fontFeatureSettings = "b") },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "letterSpacing",
+                { it.copy(letterSpacing = it.letterSpacing * 2) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "baselineShift",
+                { it.copy(baselineShift = BaselineShift.Superscript) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "textGeometricTransform",
+                { it.copy(textGeometricTransform = TextGeometricTransform(scaleX = 2f)) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "localeList",
+                initializeStyle = { it.copy(localeList = LocaleList("en-US")) },
+                updateStyle = { it.copy(localeList = LocaleList("en-GB")) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "background",
+                { it.copy(background = Color.Blue) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "textDecoration",
+                { it.copy(textDecoration = TextDecoration.LineThrough) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "to shadow",
+                { it.copy(shadow = Shadow(Color.Black, blurRadius = 4f)) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "from shadow to shadow",
+                initializeStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 1f)) },
+                updateStyle = { it.copy(shadow = Shadow(Color.Black, blurRadius = 4f)) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "to drawStyle",
+                { it.copy(drawStyle = Stroke(width = 1f)) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "from drawStyle to drawStyle",
+                initializeStyle = { it.copy(drawStyle = Stroke(width = 0f)) },
+                updateStyle = { it.copy(drawStyle = Stroke(width = 1f)) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "textAlign",
+                { it.copy(textAlign = TextAlign.Justify) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "textDirection",
+                { it.copy(textDirection = TextDirection.Rtl) },
+                invalidatesDraw = true,
+            ),
+            Config(
+                "lineHeight",
+                { it.copy(lineHeight = it.lineHeight * 2) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "textIndent",
+                { it.copy(textIndent = TextIndent(firstLine = 5.sp)) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "platformStyle",
+                initializeStyle = {
+                    it.copy(platformStyle = PlatformTextStyle(includeFontPadding = true))
+                },
+                updateStyle = {
+                    it.copy(platformStyle = PlatformTextStyle(includeFontPadding = false))
+                },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "lineHeightStyle",
+                {
+                    it.copy(
+                        lineHeightStyle = LineHeightStyle(
+                            alignment = LineHeightStyle.Alignment.Center,
+                            trim = LineHeightStyle.Trim.FirstLineTop
+                        )
+                    )
+                },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "lineBreak",
+                { it.copy(lineBreak = LineBreak.Heading) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+            Config(
+                "hyphens",
+                { it.copy(hyphens = Hyphens.Auto) },
+                invalidatesMeasure = true,
+                invalidatesDraw = true,
+            ),
+        )
+    }
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun changing() {
+        // Don't leave any TextUnits Unspecified so test cases can double them to invalidate.
+        var style by mutableStateOf(
+            TextStyle(
+                color = Color.Black,
+                fontSize = 12.sp,
+                fontWeight = FontWeight.Normal,
+                fontStyle = null,
+                fontSynthesis = null,
+                fontFamily = TEST_FONT_FAMILY,
+                fontFeatureSettings = null,
+                letterSpacing = 12.sp,
+                baselineShift = null,
+                textGeometricTransform = null,
+                localeList = null,
+                background = Color.White,
+                textDecoration = null,
+                shadow = null,
+                textAlign = TextAlign.Start,
+                textDirection = TextDirection.Ltr,
+                lineHeight = 12.sp,
+                textIndent = null,
+            ).let(config.initializeStyle)
+        )
+        var measures = 0
+        var placements = 0
+        var draws = 0
+
+        rule.setContent {
+            BasicText(
+                "a",
+                style = style,
+                modifier = Modifier
+                    .layout { measurable, constraints ->
+                        measures++
+                        val placeable = measurable.measure(constraints)
+                        layout(placeable.width, placeable.height) {
+                            placements++
+                            placeable.place(IntOffset.Zero)
+                        }
+                    }
+                    .drawBehind {
+                        draws++
+                    }
+            )
+        }
+
+        rule.waitForIdle()
+        val initialMeasures = measures
+        val initialPlacements = placements
+        val initialDraws = draws
+
+        style = config.updateStyle(style)
+
+        rule.runOnIdle {
+            if (config.invalidatesMeasure) {
+                assertThat(measures).isGreaterThan(initialMeasures)
+            }
+            if (config.invalidatesPlacement) {
+                assertThat(placements).isGreaterThan(initialPlacements)
+
+                // If measure is invalidated, placement will also always be invalidated, so ensure
+                // that placement was also invalidated separately from measurement.
+                if (config.invalidatesMeasure) {
+                    assertThat(placements).isGreaterThan(measures)
+                }
+            }
+            if (config.invalidatesDraw) {
+                assertThat(draws).isGreaterThan(initialDraws)
+            }
+        }
+    }
+}
\ No newline at end of file
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 ed29af1..6507207 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
@@ -310,7 +310,7 @@
     orientation: Orientation
 ): Pair<PointerInputChange, Offset>? {
     val initialDown =
-        awaitFirstDownOnPass(requireUnconsumed = false, pass = PointerEventPass.Initial)
+        awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Initial)
     return if (!canDrag.value.invoke(initialDown)) {
         null
     } else if (startDragImmediately.value.invoke()) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
index 13695ce..2614f34 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/TapGestureDetector.kt
@@ -258,18 +258,21 @@
     }
 }
 
-/**
- * Reads events until the first down is received. If [requireUnconsumed] is `true` and the first
- * down is consumed in the [PointerEventPass.Main] pass, that gesture is ignored.
- */
+@Deprecated(
+    "Maintained for binary compatibility. Use version with PointerEventPass instead.",
+    level = DeprecationLevel.HIDDEN)
 suspend fun AwaitPointerEventScope.awaitFirstDown(
     requireUnconsumed: Boolean = true
 ): PointerInputChange =
-    awaitFirstDownOnPass(pass = PointerEventPass.Main, requireUnconsumed = requireUnconsumed)
+    awaitFirstDown(requireUnconsumed = requireUnconsumed, pass = PointerEventPass.Main)
 
-internal suspend fun AwaitPointerEventScope.awaitFirstDownOnPass(
-    pass: PointerEventPass,
-    requireUnconsumed: Boolean
+/**
+ * Reads events until the first down is received in the given [pass]. If [requireUnconsumed] is
+ * `true` and the first down is already consumed in the pass, that gesture is ignored.
+ */
+suspend fun AwaitPointerEventScope.awaitFirstDown(
+    requireUnconsumed: Boolean = true,
+    pass: PointerEventPass = PointerEventPass.Main,
 ): PointerInputChange {
     var event: PointerEvent
     do {
@@ -282,16 +285,24 @@
     return event.changes[0]
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with PointerEventPass instead.",
+    level = DeprecationLevel.HIDDEN)
+suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? =
+    waitForUpOrCancellation(PointerEventPass.Main)
+
 /**
- * Reads events until all pointers are up or the gesture was canceled. The gesture
- * is considered canceled when a pointer leaves the event region, a position change
- * has been consumed or a pointer down change event was consumed in the [PointerEventPass.Main]
+ * Reads events in the given [pass] until all pointers are up or the gesture was canceled.
+ * The gesture is considered canceled when a pointer leaves the event region, a position
+ * change has been consumed or a pointer down change event was already consumed in the given
  * pass. If the gesture was not canceled, the final up change is returned or `null` if the
  * event was canceled.
  */
-suspend fun AwaitPointerEventScope.waitForUpOrCancellation(): PointerInputChange? {
+suspend fun AwaitPointerEventScope.waitForUpOrCancellation(
+    pass: PointerEventPass = PointerEventPass.Main
+): PointerInputChange? {
     while (true) {
-        val event = awaitPointerEvent(PointerEventPass.Main)
+        val event = awaitPointerEvent(pass)
         if (event.changes.fastAll { it.changedToUp() }) {
             // All pointers are up
             return event.changes[0]
@@ -305,7 +316,7 @@
         }
 
         // Check for cancel by position consumption. We can look on the Final pass of the
-        // existing pointer event because it comes after the Main pass we checked above.
+        // existing pointer event because it comes after the pass we checked above.
         val consumeCheck = awaitPointerEvent(PointerEventPass.Final)
         if (consumeCheck.changes.fastAny { it.isConsumed }) {
             return null
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
index edf888d..511da2d0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridMeasure.kt
@@ -81,7 +81,9 @@
                 IntArray(resolvedSlotSums.size).apply {
                     // Try to adjust indices in case grid got resized
                     for (lane in indices) {
-                        this[lane] = if (lane < firstVisibleIndices.size) {
+                        this[lane] = if (
+                            lane < firstVisibleIndices.size && firstVisibleIndices[lane] != -1
+                        ) {
                             firstVisibleIndices[lane]
                         } else {
                             if (lane == 0) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
index d285d87..b6fbafa 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridState.kt
@@ -93,7 +93,11 @@
      * each scroll, potentially causing performance issues.
      */
     val firstVisibleItemIndex: Int
-        get() = scrollPosition.indices.minOrNull() ?: 0
+        get() = scrollPosition.indices.minOfOrNull {
+            // index array can contain -1, indicating lane being empty (cell number > itemCount)
+            // if any of the lanes are empty, we always on 0th item index
+            if (it == -1) 0 else it
+        } ?: 0
 
     /**
      * Current offset of the item with [firstVisibleItemIndex] relative to the container start.
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index f9a41e9..7d6be49 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -307,6 +307,16 @@
             measurables: List<Measurable>,
             constraints: Constraints
         ): MeasureResult {
+            // Reading this state here ensures that we invalidate measure every time we update the
+            // text delegate. That is effectively what was happening before, by accident of how
+            // setting the modifier always invalidated measure, but this CL changes that so we have
+            // to do it manually.
+            // In the future, we shouldn't always invalidate measure just because the delegate
+            // changes – we should only do so when specific things change that actually require
+            // re-measuring. But that's part of the text effort to rewrite all this code with
+            // Modifier.Node. Since the old code was already "over-invalidating", this change keeps
+            // that behavior and is no worse (except for the additional state write/read).
+            state.layoutInvalidation
             // NOTE(text-perf-review): current implementation of layout means that layoutResult
             // will _never_ be the same instance. We should try and fast path case where
             // everything is the same and return same instance in that case.
@@ -462,7 +472,7 @@
             }
             state.previousGlobalPosition = newGlobalPosition
         }
-    }.heightInLines(state.textDelegate.style, state.textDelegate.minLines)
+    }
 
     /*@VisibleForTesting*/
     internal var semanticsModifier = createSemanticsModifierFor(state.textDelegate.text)
@@ -487,6 +497,11 @@
 
     val modifiers: Modifier
         get() = coreModifiers
+            // This is more correct here since before this modifier was created once, with the
+            // initial style and minLines, and then never got updates to those values. Here it gets
+            // created every time it's read, i.e. every recomposition, so it will always have the
+            // latest values.
+            .heightInLines(state.textDelegate.style, state.textDelegate.minLines)
             .then(semanticsModifier)
             .then(selectionModifiers)
 
@@ -515,8 +530,7 @@
 @OptIn(InternalFoundationTextApi::class)
 /*@VisibleForTesting*/
 internal class TextState(
-    /** Should *NEVER* be set directly, only through [TextController.setTextDelegate] */
-    var textDelegate: TextDelegate,
+    textDelegate: TextDelegate,
     /** The selectable Id assigned to the [selectable] */
     val selectableId: Long
 ) {
@@ -528,6 +542,13 @@
     /** The last layout coordinates for the Text's layout, used by selection */
     var layoutCoordinates: LayoutCoordinates? = null
 
+    /** Should *NEVER* be set directly, only through [TextController.setTextDelegate] */
+    var textDelegate: TextDelegate = textDelegate
+        set(value) {
+            layoutInvalidation = Unit
+            field = value
+        }
+
     /** The latest TextLayoutResult calculated in the measure block.*/
     var layoutResult: TextLayoutResult? = null
         set(value) {
@@ -544,6 +565,8 @@
     /** Read in draw scopes to invalidate when layoutResult  */
     var drawScopeInvalidation by mutableStateOf(Unit, neverEqualPolicy())
         private set
+    var layoutInvalidation by mutableStateOf(Unit, neverEqualPolicy())
+        private set
 }
 
 /**
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
deleted file mode 100644
index 90fa1d7..0000000
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/CompilationConsistencyBenchmark.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.integration.macrobenchmark
-
-import android.os.Build
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.StartupMode
-import androidx.benchmark.macro.junit4.MacrobenchmarkRule
-import androidx.test.filters.LargeTest
-import androidx.test.filters.SdkSuppress
-import androidx.testutils.COMPILATION_MODES
-import androidx.testutils.measureStartup
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
-
-/**
- * This benchmark is used to verify that compilation mode performance is consistent across runs,
- * and that compilation state doesn't leak across benchmarks.
- */
-@LargeTest
-@SdkSuppress(minSdkVersion = Build.VERSION_CODES.N)
-@RunWith(Parameterized::class)
-class CompilationConsistencyBenchmark(
-    @Suppress("UNUSED_PARAMETER") iteration: Int,
-    private val compilationMode: CompilationMode
-) {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    fun startup() = benchmarkRule.measureStartup(
-        compilationMode = compilationMode,
-        startupMode = StartupMode.COLD,
-        packageName = packageName
-    ) {
-        action = "androidx.compose.integration.macrobenchmark.target.LAZY_COLUMN_ACTIVITY"
-        putExtra("ITEM_COUNT", 5)
-    }
-
-    companion object {
-        val packageName = "androidx.compose.integration.macrobenchmark.target"
-
-        @Parameterized.Parameters(name = "iter={0},compilation={1}")
-        @JvmStatic
-        fun parameters(): List<Array<Any>> = mutableListOf<Array<Any>>().apply {
-            for (iter in 1..4) {
-                for (compilationMode in COMPILATION_MODES) {
-                    add(arrayOf(iter, compilationMode))
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index c523f6c..c02a4cf 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -368,8 +368,10 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class ProgressIndicatorDefaults {
@@ -623,14 +625,18 @@
   }
 
   public final class TextFieldKt {
-    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index d6e80bb..858095c 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -563,8 +563,10 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class ProgressIndicatorDefaults {
@@ -882,14 +884,18 @@
   }
 
   public final class TextFieldKt {
-    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index c523f6c..c02a4cf 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -368,8 +368,10 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class ProgressIndicatorDefaults {
@@ -623,14 +625,18 @@
   }
 
   public final class TextFieldKt {
-    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
-    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
+    method @Deprecated @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<? extends kotlin.Unit>? trailingIcon, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material.TextFieldColors colors);
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
index dcfafcd..17c6cc8 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ExposedDropdownMenuTest.kt
@@ -19,7 +19,9 @@
 import android.widget.FrameLayout
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -27,9 +29,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
@@ -38,6 +42,9 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -136,6 +143,123 @@
     }
 
     @Test
+    fun expandedBehaviour_doesNotExpandIfTouchEndsOutsideBounds() {
+        var textFieldBounds = Rect.Zero
+        rule.setMaterialContent {
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+                onTextFieldBoundsChanged = {
+                    textFieldBounds = it
+                }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that ends outside the bounds of the anchor should not expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // A swipe that ends within the bounds of the anchor should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) - 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun expandedBehaviour_doesNotExpandIfTouchIsPartOfScroll() {
+        val testIndex = 2
+        var textFieldSize = IntSize.Zero
+        rule.setMaterialContent() {
+            LazyColumn(
+                modifier = Modifier.fillMaxSize(),
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                items(50) { index ->
+                    var expanded by remember { mutableStateOf(false) }
+                    var selectedOptionText by remember { mutableStateOf("") }
+
+                    ExposedDropdownMenuBox(
+                        expanded = expanded,
+                        onExpandedChange = { expanded = it },
+                        modifier = Modifier.padding(8.dp),
+                    ) {
+                        TextField(
+                            modifier = Modifier.then(
+                                if (index == testIndex) Modifier.testTag(TFTag).onSizeChanged {
+                                    textFieldSize = it
+                                } else { Modifier }
+                            ),
+                            value = selectedOptionText,
+                            onValueChange = { selectedOptionText = it },
+                            label = { Text("Label") },
+                            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+                            colors = ExposedDropdownMenuDefaults.textFieldColors()
+                        )
+                        ExposedDropdownMenu(
+                            modifier = if (index == testIndex) {
+                                Modifier.testTag(EDMTag)
+                            } else { Modifier },
+                            expanded = expanded,
+                            onDismissRequest = { expanded = false }
+                        ) {
+                            DropdownMenuItem(
+                                onClick = {
+                                    selectedOptionText = OptionName
+                                    expanded = false
+                                },
+                                modifier = if (index == testIndex) {
+                                    Modifier.testTag(MenuItemTag)
+                                } else { Modifier }
+                            ) {
+                                Text(OptionName)
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that causes a scroll should not expand the menu, even if it remains within the
+        // bounds of the anchor.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY - (textFieldSize.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // But a swipe that does not cause a scroll should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX + (textFieldSize.width / 2) - 1, this.centerY),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
     fun uiProperties_menuMatchesTextWidth() {
         var textFieldBounds by mutableStateOf(Rect.Zero)
         var menuBounds by mutableStateOf(Rect.Zero)
diff --git a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
index 3214402..cfaa288 100644
--- a/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
+++ b/compose/material/material/src/androidMain/kotlin/androidx/compose/material/ExposedDropdownMenu.kt
@@ -22,6 +22,8 @@
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.animation.core.tween
 import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
 import androidx.compose.foundation.interaction.InteractionSource
 import androidx.compose.foundation.interaction.collectIsFocusedAsState
 import androidx.compose.foundation.layout.Box
@@ -47,9 +49,7 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.changedToUp
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
@@ -62,7 +62,6 @@
 import androidx.compose.ui.semantics.onClick
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.DpOffset
-import androidx.compose.ui.util.fastAll
 import kotlin.math.max
 
 /**
@@ -513,13 +512,13 @@
     menuLabel: String
 ) = pointerInput(Unit) {
     awaitEachGesture {
-        var event: PointerEvent
-        do {
-            event = awaitPointerEvent(PointerEventPass.Initial)
-        } while (
-            !event.changes.fastAll { it.changedToUp() }
-        )
-        onExpandedChange.invoke()
+        // Must be PointerEventPass.Initial to observe events before the text field consumes them
+        // in the Main pass
+        awaitFirstDown(pass = PointerEventPass.Initial)
+        val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)
+        if (upEvent != null) {
+            onExpandedChange()
+        }
     }
 }.semantics {
     contentDescription = menuLabel // this should be a localised string
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 6362566..7e4e081 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -116,10 +116,10 @@
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
  * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
  * maxLines attribute will be automatically set to 1
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true
- * [KeyboardOptions.imeAction] field.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of
  * [Interaction]s for this OutlinedTextField. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
@@ -146,7 +146,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = MaterialTheme.shapes.small,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
@@ -185,6 +186,7 @@
         interactionSource = interactionSource,
         singleLine = singleLine,
         maxLines = maxLines,
+        minLines = minLines,
         decorationBox = @Composable { innerTextField ->
             TextFieldDefaults.OutlinedTextFieldDecorationBox(
                 value = value,
@@ -213,6 +215,56 @@
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun OutlinedTextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions.Default,
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape = MaterialTheme.shapes.small,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
+) {
+    OutlinedTextField(
+        value,
+        onValueChange,
+        modifier,
+        enabled,
+        readOnly,
+        textStyle,
+        label,
+        placeholder,
+        leadingIcon,
+        trailingIcon,
+        isError,
+        visualTransformation,
+        keyboardOptions,
+        keyboardActions,
+        singleLine,
+        maxLines,
+        1,
+        interactionSource,
+        shape,
+        colors
+    )
+}
+
 /**
  * <a href="https://material.io/components/text-fields#outlined-text-field" class="external" target="_blank">Material Design outlined text field</a>.
  *
@@ -265,9 +317,10 @@
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
  * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
  * maxLines attribute will be automatically set to 1
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of
  * [Interaction]s for this OutlinedTextField. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
@@ -294,7 +347,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions(),
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
@@ -333,6 +387,7 @@
         interactionSource = interactionSource,
         singleLine = singleLine,
         maxLines = maxLines,
+        minLines = minLines,
         decorationBox = @Composable { innerTextField ->
             TextFieldDefaults.OutlinedTextFieldDecorationBox(
                 value = value.text,
@@ -361,6 +416,56 @@
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun OutlinedTextField(
+    value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions(),
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape = TextFieldDefaults.OutlinedTextFieldShape,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
+) {
+    OutlinedTextField(
+        value,
+        onValueChange,
+        modifier,
+        enabled,
+        readOnly,
+        textStyle,
+        label,
+        placeholder,
+        leadingIcon,
+        trailingIcon,
+        isError,
+        visualTransformation,
+        keyboardOptions,
+        keyboardActions,
+        singleLine,
+        maxLines,
+        1,
+        interactionSource,
+        shape,
+        colors
+    )
+}
+
 /**
  * Layout of the leading and trailing icons and the text field, label and placeholder in
  * [OutlinedTextField].
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
index ec1bd54..aec0946 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
@@ -80,7 +80,9 @@
  * [overflow] and TextAlign may have unexpected effects.
  * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
  * [TextLayoutResult] object that callback provides contains paragraph information, size of the
  * text, baselines and other details. The callback can be used to add additional decoration or
@@ -103,6 +105,7 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
@@ -135,6 +138,51 @@
         overflow = overflow,
         softWrap = softWrap,
         maxLines = maxLines,
+        minLines = minLines
+    )
+}
+
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Text(
+    text: String,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    Text(
+        text,
+        modifier,
+        color,
+        fontSize,
+        fontStyle,
+        fontWeight,
+        fontFamily,
+        letterSpacing,
+        textDecoration,
+        textAlign,
+        lineHeight,
+        overflow,
+        softWrap,
+        maxLines,
+        1,
+        onTextLayout,
+        style
     )
 }
 
@@ -181,7 +229,9 @@
  * [overflow] and TextAlign may have unexpected effects.
  * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param inlineContent A map store composables that replaces certain ranges of the text. It's
  * used to insert composables into text layout. Check [InlineTextContent] for more information.
  * @param onTextLayout Callback that is executed when a new text layout is calculated. A
@@ -206,6 +256,7 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent> = mapOf(),
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
@@ -238,10 +289,57 @@
         overflow = overflow,
         softWrap = softWrap,
         maxLines = maxLines,
+        minLines = minLines,
         inlineContent = inlineContent
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Text(
+    text: AnnotatedString,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    inlineContent: Map<String, InlineTextContent> = mapOf(),
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    Text(
+        text,
+        modifier,
+        color,
+        fontSize,
+        fontStyle,
+        fontWeight,
+        fontFamily,
+        letterSpacing,
+        textDecoration,
+        textAlign,
+        lineHeight,
+        overflow,
+        softWrap,
+        maxLines,
+        1,
+        inlineContent,
+        onTextLayout,
+        style
+    )
+}
+
 /**
  * CompositionLocal containing the preferred [TextStyle] that will be used by [Text] components by
  * default. To set the value for this CompositionLocal, see [ProvideTextStyle] which will merge any
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
index f9ba49c..dfbd2c8 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt
@@ -142,9 +142,10 @@
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
  * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
  * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of
  * [Interaction]s for this TextField. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
@@ -171,7 +172,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions(),
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape =
         MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
@@ -204,6 +206,7 @@
         interactionSource = interactionSource,
         singleLine = singleLine,
         maxLines = maxLines,
+        minLines = minLines,
         decorationBox = @Composable { innerTextField ->
             // places leading icon, text field with label and placeholder, trailing icon
             TextFieldDefaults.TextFieldDecorationBox(
@@ -224,6 +227,57 @@
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun TextField(
+    value: String,
+    onValueChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions(),
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape =
+        MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
+    colors: TextFieldColors = TextFieldDefaults.textFieldColors()
+) {
+    TextField(
+        value,
+        onValueChange,
+        modifier,
+        enabled,
+        readOnly,
+        textStyle,
+        label,
+        placeholder,
+        leadingIcon,
+        trailingIcon,
+        isError,
+        visualTransformation,
+        keyboardOptions,
+        keyboardActions,
+        singleLine,
+        maxLines,
+        1,
+        interactionSource,
+        shape,
+        colors
+    )
+}
+
 /**
  * <a href="https://material.io/components/text-fields#filled-text-field" class="external" target="_blank">Material Design filled text field</a>.
  *
@@ -277,9 +331,10 @@
  * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
  * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
  * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of
  * [Interaction]s for this TextField. You can create and pass in your own remembered
  * [MutableInteractionSource] if you want to observe [Interaction]s and customize the
@@ -306,7 +361,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions(),
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.TextFieldShape,
     colors: TextFieldColors = TextFieldDefaults.textFieldColors()
@@ -338,6 +394,7 @@
         interactionSource = interactionSource,
         singleLine = singleLine,
         maxLines = maxLines,
+        minLines = minLines,
         decorationBox = @Composable { innerTextField ->
             // places leading icon, text field with label and placeholder, trailing icon
             TextFieldDefaults.TextFieldDecorationBox(
@@ -358,6 +415,56 @@
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun TextField(
+    value: TextFieldValue,
+    onValueChange: (TextFieldValue) -> Unit,
+    modifier: Modifier = Modifier,
+    enabled: Boolean = true,
+    readOnly: Boolean = false,
+    textStyle: TextStyle = LocalTextStyle.current,
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable (() -> Unit)? = null,
+    leadingIcon: @Composable (() -> Unit)? = null,
+    trailingIcon: @Composable (() -> Unit)? = null,
+    isError: Boolean = false,
+    visualTransformation: VisualTransformation = VisualTransformation.None,
+    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+    keyboardActions: KeyboardActions = KeyboardActions(),
+    singleLine: Boolean = false,
+    maxLines: Int = Int.MAX_VALUE,
+    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+    shape: Shape = TextFieldDefaults.TextFieldShape,
+    colors: TextFieldColors = TextFieldDefaults.textFieldColors()
+) {
+    TextField(
+        value,
+        onValueChange,
+        modifier,
+        enabled,
+        readOnly,
+        textStyle,
+        label,
+        placeholder,
+        leadingIcon,
+        trailingIcon,
+        isError,
+        visualTransformation,
+        keyboardOptions,
+        keyboardActions,
+        singleLine,
+        maxLines,
+        1,
+        interactionSource,
+        shape,
+        colors
+    )
+}
+
 /**
  * Composable responsible for measuring and laying out leading and trailing icons, label,
  * placeholder and the input field.
diff --git a/compose/material3/material3/api/current.txt b/compose/material3/material3/api/current.txt
index 71a26ff..bc57514 100644
--- a/compose/material3/material3/api/current.txt
+++ b/compose/material3/material3/api/current.txt
@@ -625,8 +625,10 @@
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index 8dc2dc4..906e393 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -601,8 +601,8 @@
   }
 
   public final class OutlinedTextFieldKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void OutlinedTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
   public final class ProgressIndicatorDefaults {
@@ -896,14 +896,16 @@
   }
 
   public final class TextFieldKt {
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void TextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function0<kotlin.Unit>? label, optional kotlin.jvm.functions.Function0<kotlin.Unit>? placeholder, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? supportingText, optional boolean isError, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.TextFieldColors colors);
   }
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material3/material3/api/restricted_current.txt b/compose/material3/material3/api/restricted_current.txt
index 71a26ff..bc57514 100644
--- a/compose/material3/material3/api/restricted_current.txt
+++ b/compose/material3/material3/api/restricted_current.txt
@@ -625,8 +625,10 @@
 
   public final class TextKt {
     method @androidx.compose.runtime.Composable public static void ProvideTextStyle(androidx.compose.ui.text.TextStyle value, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
-    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(String text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
+    method @Deprecated @androidx.compose.runtime.Composable public static void Text(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional long color, optional long fontSize, optional androidx.compose.ui.text.font.FontStyle? fontStyle, optional androidx.compose.ui.text.font.FontWeight? fontWeight, optional androidx.compose.ui.text.font.FontFamily? fontFamily, optional long letterSpacing, optional androidx.compose.ui.text.style.TextDecoration? textDecoration, optional androidx.compose.ui.text.style.TextAlign? textAlign, optional long lineHeight, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.ui.text.TextStyle style);
     method public static androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> getLocalTextStyle();
     property public static final androidx.compose.runtime.ProvidableCompositionLocal<androidx.compose.ui.text.TextStyle> LocalTextStyle;
   }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
index 6c546cb..66ae652 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ExposedDropdownMenuTest.kt
@@ -19,7 +19,9 @@
 import android.widget.FrameLayout
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -27,9 +29,11 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
@@ -38,6 +42,9 @@
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -135,6 +142,122 @@
     }
 
     @Test
+    fun expandedBehaviour_doesNotExpandIfTouchEndsOutsideBounds() {
+        var textFieldBounds = Rect.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            var expanded by remember { mutableStateOf(false) }
+            ExposedDropdownMenuForTest(
+                expanded = expanded,
+                onExpandChange = { expanded = it },
+                onTextFieldBoundsChanged = {
+                    textFieldBounds = it
+                }
+            )
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that ends outside the bounds of the anchor should not expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // A swipe that ends within the bounds of the anchor should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY + (textFieldBounds.height / 2) - 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
+    fun expandedBehaviour_doesNotExpandIfTouchIsPartOfScroll() {
+        val testIndex = 2
+        var textFieldSize = IntSize.Zero
+        rule.setMaterialContent(lightColorScheme()) {
+            LazyColumn(
+                modifier = Modifier.fillMaxSize(),
+                horizontalAlignment = Alignment.CenterHorizontally,
+            ) {
+                items(50) { index ->
+                    var expanded by remember { mutableStateOf(false) }
+                    var selectedOptionText by remember { mutableStateOf("") }
+
+                    ExposedDropdownMenuBox(
+                        expanded = expanded,
+                        onExpandedChange = { expanded = it },
+                        modifier = Modifier.padding(8.dp),
+                    ) {
+                        TextField(
+                            modifier = Modifier.menuAnchor().then(
+                                if (index == testIndex) Modifier.testTag(TFTag).onSizeChanged {
+                                    textFieldSize = it
+                                } else { Modifier }
+                            ),
+                            value = selectedOptionText,
+                            onValueChange = { selectedOptionText = it },
+                            label = { Text("Label") },
+                            trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded) },
+                            colors = ExposedDropdownMenuDefaults.textFieldColors()
+                        )
+                        ExposedDropdownMenu(
+                            modifier = if (index == testIndex) {
+                                Modifier.testTag(EDMTag)
+                            } else { Modifier },
+                            expanded = expanded,
+                            onDismissRequest = { expanded = false }
+                        ) {
+                            DropdownMenuItem(
+                                text = { Text(OptionName) },
+                                onClick = {
+                                    selectedOptionText = OptionName
+                                    expanded = false
+                                },
+                                modifier = if (index == testIndex) {
+                                    Modifier.testTag(MenuItemTag)
+                                } else { Modifier },
+                            )
+                        }
+                    }
+                }
+            }
+        }
+
+        rule.onNodeWithTag(TFTag).assertIsDisplayed()
+        rule.onNodeWithTag(EDMTag).assertDoesNotExist()
+
+        // A swipe that causes a scroll should not expand the menu, even if it remains within the
+        // bounds of the anchor.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX, this.centerY - (textFieldSize.height / 2) + 1),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertDoesNotExist()
+
+        // But a swipe that does not cause a scroll should expand the menu.
+        rule.onNodeWithTag(TFTag).performTouchInput {
+            swipe(
+                start = this.center,
+                end = Offset(this.centerX + (textFieldSize.width / 2) - 1, this.centerY),
+                durationMillis = 100
+            )
+        }
+        rule.onNodeWithTag(MenuItemTag).assertIsDisplayed()
+    }
+
+    @Test
     fun uiProperties_menuMatchesTextWidth() {
         var textFieldBounds by mutableStateOf(Rect.Zero)
         var menuBounds by mutableStateOf(Rect.Zero)
diff --git a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.kt b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.kt
index 3fa4084..25d6063 100644
--- a/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.kt
+++ b/compose/material3/material3/src/androidMain/kotlin/androidx/compose/material3/ExposedDropdownMenu.kt
@@ -21,6 +21,8 @@
 import android.view.ViewTreeObserver
 import androidx.compose.animation.core.MutableTransitionState
 import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.waitForUpOrCancellation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.PaddingValues
@@ -47,9 +49,7 @@
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
-import androidx.compose.ui.input.pointer.changedToUp
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInWindow
@@ -64,7 +64,6 @@
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAll
 import kotlin.math.max
 
 /**
@@ -121,8 +120,12 @@
                     onGloballyPositioned {
                         width = it.size.width
                         coordinates.value = it
-                        updateHeight(view.rootView, coordinates.value, verticalMarginInPx) {
-                            newHeight -> menuHeight = newHeight
+                        updateHeight(
+                            view.rootView,
+                            coordinates.value,
+                            verticalMarginInPx
+                        ) { newHeight ->
+                            menuHeight = newHeight
                         }
                     }.expandable(
                         expanded = expanded,
@@ -527,13 +530,13 @@
     collapsedDescription: String = getString(Strings.MenuCollapsed),
 ) = pointerInput(Unit) {
     awaitEachGesture {
-        var event: PointerEvent
-        do {
-            event = awaitPointerEvent(PointerEventPass.Initial)
-        } while (
-            !event.changes.fastAll { it.changedToUp() }
-        )
-        onExpandedChange()
+        // Must be PointerEventPass.Initial to observe events before the text field consumes them
+        // in the Main pass
+        awaitFirstDown(pass = PointerEventPass.Initial)
+        val upEvent = waitForUpOrCancellation(pass = PointerEventPass.Initial)
+        if (upEvent != null) {
+            onExpandedChange()
+        }
     }
 }.semantics {
     stateDescription = if (expanded) expandedDescription else collapsedDescription
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index a05c14e..8481b8b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -118,9 +118,10 @@
  * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key
  * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will
  * be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this text field. You can create and pass in your own `remember`ed instance to observe
  * [Interaction]s and customize the appearance / behavior of this text field in different states.
@@ -147,7 +148,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.outlinedShape,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
@@ -186,6 +188,7 @@
             interactionSource = interactionSource,
             singleLine = singleLine,
             maxLines = maxLines,
+            minLines = minLines,
             decorationBox = @Composable { innerTextField ->
                 TextFieldDefaults.OutlinedTextFieldDecorationBox(
                     value = value,
@@ -269,9 +272,10 @@
  * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key
  * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will
  * be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this text field. You can create and pass in your own `remember`ed instance to observe
  * [Interaction]s and customize the appearance / behavior of this text field in different states.
@@ -298,7 +302,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.outlinedShape,
     colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors()
@@ -337,6 +342,7 @@
             interactionSource = interactionSource,
             singleLine = singleLine,
             maxLines = maxLines,
+            minLines = minLines,
             decorationBox = @Composable { innerTextField ->
                 TextFieldDefaults.OutlinedTextFieldDecorationBox(
                     value = value.text,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
index 060651b..bfdc8a6 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
@@ -76,9 +76,11 @@
  * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
  * [overflow] and TextAlign may have unexpected effects.
- * @param maxLines an optional maximum number of lines for the text to span, wrapping if
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
  * [TextLayoutResult] object that callback provides contains paragraph information, size of the
  * text, baselines and other details. The callback can be used to add additional decoration or
@@ -101,6 +103,7 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
 ) {
@@ -133,6 +136,51 @@
         overflow,
         softWrap,
         maxLines,
+        minLines
+    )
+}
+
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Text(
+    text: String,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    Text(
+        text,
+        modifier,
+        color,
+        fontSize,
+        fontStyle,
+        fontWeight,
+        fontFamily,
+        letterSpacing,
+        textDecoration,
+        textAlign,
+        lineHeight,
+        overflow,
+        softWrap,
+        maxLines,
+        1,
+        onTextLayout,
+        style
     )
 }
 
@@ -175,9 +223,11 @@
  * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
  * [overflow] and TextAlign may have unexpected effects.
- * @param maxLines an optional maximum number of lines for the text to span, wrapping if
+ * @param maxLines An optional maximum number of lines for the text to span, wrapping if
  * necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
  * @param inlineContent a map storing composables that replaces certain ranges of the text, used to
  * insert composables into text layout. See [InlineTextContent].
  * @param onTextLayout callback that is executed when a new text layout is calculated. A
@@ -202,6 +252,7 @@
     overflow: TextOverflow = TextOverflow.Clip,
     softWrap: Boolean = true,
     maxLines: Int = Int.MAX_VALUE,
+    minLines: Int = 1,
     inlineContent: Map<String, InlineTextContent> = mapOf(),
     onTextLayout: (TextLayoutResult) -> Unit = {},
     style: TextStyle = LocalTextStyle.current
@@ -234,10 +285,57 @@
         overflow = overflow,
         softWrap = softWrap,
         maxLines = maxLines,
+        minLines = minLines,
         inlineContent = inlineContent
     )
 }
 
+@Deprecated(
+    "Maintained for binary compatibility. Use version with minLines instead",
+    level = DeprecationLevel.HIDDEN
+)
+@Composable
+fun Text(
+    text: AnnotatedString,
+    modifier: Modifier = Modifier,
+    color: Color = Color.Unspecified,
+    fontSize: TextUnit = TextUnit.Unspecified,
+    fontStyle: FontStyle? = null,
+    fontWeight: FontWeight? = null,
+    fontFamily: FontFamily? = null,
+    letterSpacing: TextUnit = TextUnit.Unspecified,
+    textDecoration: TextDecoration? = null,
+    textAlign: TextAlign? = null,
+    lineHeight: TextUnit = TextUnit.Unspecified,
+    overflow: TextOverflow = TextOverflow.Clip,
+    softWrap: Boolean = true,
+    maxLines: Int = Int.MAX_VALUE,
+    inlineContent: Map<String, InlineTextContent> = mapOf(),
+    onTextLayout: (TextLayoutResult) -> Unit = {},
+    style: TextStyle = LocalTextStyle.current
+) {
+    Text(
+        text,
+        modifier,
+        color,
+        fontSize,
+        fontStyle,
+        fontWeight,
+        fontFamily,
+        letterSpacing,
+        textDecoration,
+        textAlign,
+        lineHeight,
+        overflow,
+        softWrap,
+        maxLines,
+        1,
+        inlineContent,
+        onTextLayout,
+        style
+    )
+}
+
 /**
  * CompositionLocal containing the preferred [TextStyle] that will be used by [Text] components by
  * default. To set the value for this CompositionLocal, see [ProvideTextStyle] which will merge any
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
index 1060966..6e02c7f 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt
@@ -141,9 +141,10 @@
  * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key
  * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will
  * be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this text field. You can create and pass in your own `remember`ed instance to observe
  * [Interaction]s and customize the appearance / behavior of this text field in different states.
@@ -170,7 +171,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.filledShape,
     colors: TextFieldColors = TextFieldDefaults.textFieldColors()
@@ -201,6 +203,7 @@
             interactionSource = interactionSource,
             singleLine = singleLine,
             maxLines = maxLines,
+            minLines = minLines,
             decorationBox = @Composable { innerTextField ->
                 // places leading icon, text field with label and placeholder, trailing icon
                 TextFieldDefaults.TextFieldDecorationBox(
@@ -278,9 +281,10 @@
  * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return key
  * as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines attribute will
  * be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this text field. You can create and pass in your own `remember`ed instance to observe
  * [Interaction]s and customize the appearance / behavior of this text field in different states.
@@ -307,7 +311,8 @@
     keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
     keyboardActions: KeyboardActions = KeyboardActions.Default,
     singleLine: Boolean = false,
-    maxLines: Int = Int.MAX_VALUE,
+    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+    minLines: Int = 1,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     shape: Shape = TextFieldDefaults.filledShape,
     colors: TextFieldColors = TextFieldDefaults.textFieldColors()
@@ -338,6 +343,7 @@
             interactionSource = interactionSource,
             singleLine = singleLine,
             maxLines = maxLines,
+            minLines = minLines,
             decorationBox = @Composable { innerTextField ->
                 // places leading icon, text field with label and placeholder, trailing icon
                 TextFieldDefaults.TextFieldDecorationBox(
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt
new file mode 100644
index 0000000..4742993
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DateInputModalTokens.kt
@@ -0,0 +1,21 @@
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object DateInputModalTokens {
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level3
+    val ContainerHeight = 512.0.dp
+    val ContainerShape = ShapeKeyTokens.CornerExtraLarge
+    val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val ContainerWidth = 328.0.dp
+    val HeaderContainerHeight = 120.0.dp
+    val HeaderContainerWidth = 328.0.dp
+    val HeaderHeadlineColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeaderHeadlineFont = TypographyKeyTokens.HeadlineLarge
+    val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeaderSupportingTextFont = TypographyKeyTokens.LabelMedium
+}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt
new file mode 100644
index 0000000..f84cc5d
--- /dev/null
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/DatePickerModalTokens.kt
@@ -0,0 +1,55 @@
+// VERSION: v0_126
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+package androidx.compose.material3.tokens
+
+import androidx.compose.ui.unit.dp
+
+internal object DatePickerModalTokens {
+    val ContainerColor = ColorSchemeKeyTokens.Surface
+    val ContainerElevation = ElevationTokens.Level3
+    val ContainerHeight = 512.0.dp
+    val ContainerShape = ShapeKeyTokens.CornerExtraLarge
+    val ContainerSurfaceTintLayerColor = ColorSchemeKeyTokens.SurfaceTint
+    val ContainerWidth = 328.0.dp
+    val DateContainerHeight = 40.0.dp
+    val DateContainerShape = ShapeKeyTokens.CornerFull
+    val DateContainerWidth = 40.0.dp
+    val DateLabelTextFont = TypographyKeyTokens.BodySmall
+    val DateSelectedContainerColor = ColorSchemeKeyTokens.Primary
+    val DateSelectedLabelTextColor = ColorSchemeKeyTokens.OnPrimary
+    val DateStateLayerHeight = 40.0.dp
+    val DateStateLayerShape = ShapeKeyTokens.CornerFull
+    val DateStateLayerWidth = 40.0.dp
+    val DateTodayContainerOutlineColor = ColorSchemeKeyTokens.Primary
+    val DateTodayContainerOutlineWidth = 1.0.dp
+    val DateTodayLabelTextColor = ColorSchemeKeyTokens.Primary
+    val DateUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val HeaderContainerHeight = 120.0.dp
+    val HeaderContainerWidth = 328.0.dp
+    val HeaderHeadlineColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeaderHeadlineFont = TypographyKeyTokens.HeadlineLarge
+    val HeaderSupportingTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val HeaderSupportingTextFont = TypographyKeyTokens.LabelMedium
+    val RangeSelectionActiveIndicatorContainerColor = ColorSchemeKeyTokens.PrimaryContainer
+    val RangeSelectionActiveIndicatorContainerHeight = 40.0.dp
+    val RangeSelectionActiveIndicatorContainerShape = ShapeKeyTokens.CornerFull
+    val RangeSelectionContainerElevation = ElevationTokens.Level0
+    val RangeSelectionContainerShape = ShapeKeyTokens.CornerNone
+    val SelectionDateInRangeLabelTextColor = ColorSchemeKeyTokens.OnPrimaryContainer
+    val RangeSelectionHeaderContainerHeight = 128.0.dp
+    val RangeSelectionHeaderHeadlineFont = TypographyKeyTokens.TitleLarge
+    val RangeSelectionMonthSubheadColor = ColorSchemeKeyTokens.OnSurfaceVariant
+    val RangeSelectionMonthSubheadFont = TypographyKeyTokens.TitleSmall
+    val WeekdaysLabelTextColor = ColorSchemeKeyTokens.OnSurface
+    val WeekdaysLabelTextFont = TypographyKeyTokens.BodySmall
+    val SelectionYearContainerHeight = 36.0.dp
+    val SelectionYearContainerWidth = 72.0.dp
+    val SelectionYearLabelTextFont = TypographyKeyTokens.BodyLarge
+    val SelectionYearSelectedContainerColor = ColorSchemeKeyTokens.Primary
+    val SelectionYearSelectedLabelTextColor = ColorSchemeKeyTokens.OnPrimary
+    val SelectionYearStateLayerHeight = 36.0.dp
+    val SelectionYearStateLayerShape = ShapeKeyTokens.CornerFull
+    val SelectionYearStateLayerWidth = 72.0.dp
+    val SelectionYearUnselectedLabelTextColor = ColorSchemeKeyTokens.OnSurfaceVariant
+}
diff --git a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
index 33af974..67f4487 100644
--- a/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
+++ b/compose/ui/ui-graphics/benchmark/src/androidTest/java/androidx/compose/ui/graphics/benchmark/VectorBenchmarkWithTracing.kt
@@ -16,7 +16,8 @@
 
 package androidx.compose.ui.graphics.benchmark
 
-import androidx.benchmark.junit4.PerfettoRule
+import androidx.benchmark.junit4.PerfettoTraceRule
+import androidx.benchmark.perfetto.ExperimentalPerfettoCaptureApi
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.junit.Rule
@@ -24,13 +25,12 @@
 
 /**
  * Duplicate of [VectorBenchmark], but which adds tracing.
- *
- * Note: Per PerfettoRule, these benchmarks will be ignored < API 29
  */
 @Suppress("ClassName")
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 class VectorBenchmarkWithTracing : VectorBenchmark() {
+    @OptIn(ExperimentalPerfettoCaptureApi::class)
     @get:Rule
-    val perfettoRule = PerfettoRule()
+    val perfettoTraceRule = PerfettoTraceRule()
 }
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
index dc706b8..23c7e7a 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedCanvas.skiko.kt
@@ -52,8 +52,13 @@
 
 actual val Canvas.nativeCanvas: NativeCanvas get() = (this as SkiaBackedCanvas).skia
 
-internal class SkiaBackedCanvas(val skia: org.jetbrains.skia.Canvas) : Canvas {
-    private val Paint.skia get() = (this as SkiaBackedPaint).skia
+class SkiaBackedCanvas(val skia: org.jetbrains.skia.Canvas) : Canvas {
+
+    var alphaMultiplier: Float = 1.0f
+
+    private val Paint.skia get() = (this as SkiaBackedPaint).apply {
+        this.alphaMultiplier = this@SkiaBackedCanvas.alphaMultiplier
+    }.skia
 
     override fun save() {
         skia.save()
diff --git a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
index e9df393..08811b8 100644
--- a/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
+++ b/compose/ui/ui-graphics/src/skikoMain/kotlin/androidx/compose/ui/graphics/SkiaBackedPaint.skiko.kt
@@ -34,10 +34,26 @@
 ) : Paint {
     override fun asFrameworkPaint(): NativePaint = skia
 
-    override var alpha: Float
-        get() = Color(skia.color).alpha
+    private var mAlphaMultiplier = 1.0f
+    private var mColor: Color = Color.Black
+
+    var alphaMultiplier: Float
+        get() = mAlphaMultiplier
         set(value) {
-            skia.color = Color(skia.color).copy(alpha = value).toArgb()
+            val multiplier = value.coerceIn(0f, 1f)
+            updateAlpha(multiplier = multiplier)
+            mAlphaMultiplier = multiplier
+        }
+
+    private fun updateAlpha(alpha: Float = this.alpha, multiplier: Float = this.mAlphaMultiplier) {
+        skia.color = mColor.copy(alpha = alpha * multiplier).toArgb()
+    }
+
+    override var alpha: Float
+        get() = mColor.alpha
+        set(value) {
+            mColor = mColor.copy(alpha = value)
+            updateAlpha(alpha = value)
         }
 
     override var isAntiAlias: Boolean
@@ -47,8 +63,9 @@
         }
 
     override var color: Color
-        get() = Color(skia.color)
+        get() = mColor
         set(color) {
+            mColor = color
             skia.color = color.toArgb()
         }
 
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
index de73bcb..fac9dc9 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/TextStyle.kt
@@ -51,6 +51,8 @@
  * @see SpanStyle
  * @see ParagraphStyle
  */
+// Maintainer note: When adding a new constructor or copy parameter, make sure to add a test case to
+// TextStyleInvalidationTest to ensure the correct phase(s) get invalidated.
 @Immutable
 class TextStyle internal constructor(
     internal val spanStyle: SpanStyle,
diff --git a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
index 413e75ed..5f18bd8 100644
--- a/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
+++ b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
@@ -347,7 +347,7 @@
         assertEquals(
             "text, modifier, color, fontSize, fontStyle, fontWeight, fontFamily, " +
                 "letterSpacing, textDecoration, textAlign, lineHeight, overflow, softWrap, " +
-                "maxLines, onTextLayout, style",
+                "maxLines, minLines, onTextLayout, style",
             names.joinToString()
         )
     }
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
index e65ad4f..323824b 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClockTest.kt
@@ -81,6 +81,12 @@
         checkUpdatedState(clock, label = "DpAnimation",
             newInitialValue = 3.dp, newTargetValue = 4.dp,
             composeState = { state!!.value })
+        rule.runOnUiThread {
+            clock.setStateParameters(listOf(30f), listOf(40f))
+        }
+        checkUpdatedState(clock, label = "DpAnimation",
+            newInitialValue = 30.dp, newTargetValue = 40.dp,
+            composeState = { state!!.value })
         // Invalid parameters are ignored.
         rule.runOnUiThread {
             clock.setStateParameters(111.dp, 111)
@@ -91,7 +97,7 @@
         }
         // State hasn't changed.
         checkUpdatedState(clock, label = "DpAnimation",
-            newInitialValue = 3.dp, newTargetValue = 4.dp,
+            newInitialValue = 30.dp, newTargetValue = 40.dp,
             composeState = { state!!.value })
     }
 
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
index 790ecc0..897b603 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/AnimationSearch.kt
@@ -146,7 +146,8 @@
 
         @Suppress("UNCHECKED_CAST")
         private fun <T> findAnimationSpec(group: CallGroup): AnimationSpec<T>? {
-            return group.children.filter { it.name == REMEMBER_UPDATED_STATE }
+            val rememberStates = group.children.filter { it.name == REMEMBER_UPDATED_STATE }
+            return (rememberStates + rememberStates.flatMap { it.children })
                 .flatMap { it.data }
                 .filterIsInstance<State<T>>().map { it.value }
                 .filterIsInstance<AnimationSpec<T>>().firstOrNull()
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
index d80cf01..bbf646c 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/clock/AnimateXAsStateClock.kt
@@ -26,8 +26,10 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.tooling.animation.AnimateXAsStateComposeAnimation
 import androidx.compose.ui.tooling.animation.states.TargetState
+import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
 
 /**
  * [ComposeAnimationClock] for [AnimateXAsStateComposeAnimation].
@@ -126,6 +128,14 @@
                             par2[3] as Float
                         ),
                     )
+
+                    is Dp -> {
+                        if (parametersHasTheSameType(currentValue!!, par1[0]!!, par2[0]!!))
+                            TargetState(par1[0], par2[0]) else TargetState(
+                            (par1[0] as Float).dp, (par2[0] as Float).dp
+                        )
+                    }
+
                     else -> {
                         if (parametersAreValid(par1[0], par2[0]) &&
                             parametersHasTheSameType(currentValue!!, par1[0]!!, par2[0]!!)
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
new file mode 100644
index 0000000..42a92cf
--- /dev/null
+++ b/compose/ui/ui/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+InvalidNullConversion: androidx.compose.ui.graphics.GraphicsLayerModifierKt#graphicsLayer(androidx.compose.ui.Modifier, float, float, float, float, float, float, float, float, float, float, long, androidx.compose.ui.graphics.Shape, boolean, androidx.compose.ui.graphics.RenderEffect, long, long):
+    Attempted to remove @NonNull annotation from method androidx.compose.ui.graphics.GraphicsLayerModifierKt.graphicsLayer(androidx.compose.ui.Modifier,float,float,float,float,float,float,float,float,float,float,long,androidx.compose.ui.graphics.Shape,boolean,androidx.compose.ui.graphics.RenderEffect,long,long)
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index bcd7fd9..82c974c 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -435,11 +435,25 @@
 
 package androidx.compose.ui.graphics {
 
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class CompositingStrategy {
+    field public static final androidx.compose.ui.graphics.CompositingStrategy.Companion Companion;
+  }
+
+  public static final class CompositingStrategy.Companion {
+    method public int getAlways();
+    method public int getAuto();
+    method public int getModulateAlpha();
+    property public final int Always;
+    property public final int Auto;
+    property public final int ModulateAlpha;
+  }
+
   public final class GraphicsLayerModifierKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor, optional int compositingStrategy);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> block);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
@@ -448,6 +462,7 @@
     method public default long getAmbientShadowColor();
     method public float getCameraDistance();
     method public boolean getClip();
+    method public default int getCompositingStrategy();
     method public default androidx.compose.ui.graphics.RenderEffect? getRenderEffect();
     method public float getRotationX();
     method public float getRotationY();
@@ -456,6 +471,7 @@
     method public float getScaleY();
     method public float getShadowElevation();
     method public androidx.compose.ui.graphics.Shape getShape();
+    method public default long getSize();
     method public default long getSpotShadowColor();
     method public long getTransformOrigin();
     method public float getTranslationX();
@@ -464,6 +480,7 @@
     method public default void setAmbientShadowColor(long);
     method public void setCameraDistance(float);
     method public void setClip(boolean);
+    method public default void setCompositingStrategy(int);
     method public default void setRenderEffect(androidx.compose.ui.graphics.RenderEffect?);
     method public void setRotationX(float);
     method public void setRotationY(float);
@@ -480,6 +497,7 @@
     property public default long ambientShadowColor;
     property public abstract float cameraDistance;
     property public abstract boolean clip;
+    property public default int compositingStrategy;
     property public default androidx.compose.ui.graphics.RenderEffect? renderEffect;
     property public abstract float rotationX;
     property public abstract float rotationY;
@@ -488,6 +506,7 @@
     property public abstract float scaleY;
     property public abstract float shadowElevation;
     property public abstract androidx.compose.ui.graphics.Shape shape;
+    property public default long size;
     property public default long spotShadowColor;
     property public abstract long transformOrigin;
     property public abstract float translationX;
@@ -2241,6 +2260,9 @@
   public final class ObserverNodeKt {
   }
 
+  public final class ParentDataModifierNodeKt {
+  }
+
   public final class PointerInputModifierNodeKt {
   }
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 7dd6735..3651c6e 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -560,11 +560,25 @@
 
 package androidx.compose.ui.graphics {
 
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class CompositingStrategy {
+    field public static final androidx.compose.ui.graphics.CompositingStrategy.Companion Companion;
+  }
+
+  public static final class CompositingStrategy.Companion {
+    method public int getAlways();
+    method public int getAuto();
+    method public int getModulateAlpha();
+    property public final int Always;
+    property public final int Auto;
+    property public final int ModulateAlpha;
+  }
+
   public final class GraphicsLayerModifierKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor, optional int compositingStrategy);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> block);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
@@ -573,6 +587,7 @@
     method public default long getAmbientShadowColor();
     method public float getCameraDistance();
     method public boolean getClip();
+    method public default int getCompositingStrategy();
     method public default androidx.compose.ui.graphics.RenderEffect? getRenderEffect();
     method public float getRotationX();
     method public float getRotationY();
@@ -581,6 +596,7 @@
     method public float getScaleY();
     method public float getShadowElevation();
     method public androidx.compose.ui.graphics.Shape getShape();
+    method public default long getSize();
     method public default long getSpotShadowColor();
     method public long getTransformOrigin();
     method public float getTranslationX();
@@ -589,6 +605,7 @@
     method public default void setAmbientShadowColor(long);
     method public void setCameraDistance(float);
     method public void setClip(boolean);
+    method public default void setCompositingStrategy(int);
     method public default void setRenderEffect(androidx.compose.ui.graphics.RenderEffect?);
     method public void setRotationX(float);
     method public void setRotationY(float);
@@ -605,6 +622,7 @@
     property public default long ambientShadowColor;
     property public abstract float cameraDistance;
     property public abstract boolean clip;
+    property public default int compositingStrategy;
     property public default androidx.compose.ui.graphics.RenderEffect? renderEffect;
     property public abstract float rotationX;
     property public abstract float rotationY;
@@ -613,6 +631,7 @@
     property public abstract float scaleY;
     property public abstract float shadowElevation;
     property public abstract androidx.compose.ui.graphics.Shape shape;
+    property public default long size;
     property public default long spotShadowColor;
     property public abstract long transformOrigin;
     property public abstract float translationX;
@@ -2431,7 +2450,6 @@
   @androidx.compose.ui.ExperimentalComposeUiApi public abstract class DelegatingNode extends androidx.compose.ui.Modifier.Node {
     ctor public DelegatingNode();
     method public final <T extends androidx.compose.ui.Modifier.Node> T delegated(kotlin.jvm.functions.Function0<? extends T> fn);
-    method public final <T extends androidx.compose.ui.Modifier.Node> kotlin.Lazy<T> lazyDelegated(kotlin.jvm.functions.Function0<? extends T> fn);
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public interface DrawModifierNode extends androidx.compose.ui.node.DelegatableNode {
@@ -2440,6 +2458,7 @@
   }
 
   public final class DrawModifierNodeKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateDraw(androidx.compose.ui.node.DrawModifierNode);
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public interface GlobalPositionAwareModifierNode extends androidx.compose.ui.node.DelegatableNode {
@@ -2477,6 +2496,9 @@
   }
 
   public final class LayoutModifierNodeKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateLayer(androidx.compose.ui.node.LayoutModifierNode);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateLayout(androidx.compose.ui.node.LayoutModifierNode);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateMeasurements(androidx.compose.ui.node.LayoutModifierNode);
   }
 
   public final class LayoutNodeDrawScopeKt {
@@ -2492,13 +2514,13 @@
   }
 
   @androidx.compose.ui.ExperimentalComposeUiApi public abstract class ModifierNodeElement<N extends androidx.compose.ui.Modifier.Node> extends androidx.compose.ui.platform.InspectorValueInfo implements androidx.compose.ui.Modifier.Element {
-    ctor public ModifierNodeElement(optional Object? params, kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo);
+    ctor public ModifierNodeElement(optional Object? params, optional boolean autoInvalidate, kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,kotlin.Unit> inspectorInfo);
     method public abstract N create();
     method public abstract N update(N node);
   }
 
   public final class ModifierNodeElementKt {
-    method @androidx.compose.ui.ExperimentalComposeUiApi public static inline <reified T extends androidx.compose.ui.Modifier.Node> androidx.compose.ui.Modifier! modifierElementOf(Object? params, kotlin.jvm.functions.Function0<? extends T> create, kotlin.jvm.functions.Function1<? super T,? extends kotlin.Unit> update, kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,? extends kotlin.Unit> definitions);
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static inline <reified T extends androidx.compose.ui.Modifier.Node> androidx.compose.ui.Modifier! modifierElementOf(Object? key, kotlin.jvm.functions.Function0<? extends T> create, kotlin.jvm.functions.Function1<? super T,? extends kotlin.Unit> update, kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,? extends kotlin.Unit> definitions);
     method @androidx.compose.ui.ExperimentalComposeUiApi public static inline <reified T extends androidx.compose.ui.Modifier.Node> androidx.compose.ui.Modifier! modifierElementOf(kotlin.jvm.functions.Function0<? extends T> create, kotlin.jvm.functions.Function1<? super androidx.compose.ui.platform.InspectorInfo,? extends kotlin.Unit> definitions);
   }
 
@@ -2530,6 +2552,10 @@
     method public Object? modifyParentData(androidx.compose.ui.unit.Density, Object? parentData);
   }
 
+  public final class ParentDataModifierNodeKt {
+    method @androidx.compose.ui.ExperimentalComposeUiApi public static void invalidateParentData(androidx.compose.ui.node.ParentDataModifierNode);
+  }
+
   @androidx.compose.ui.ExperimentalComposeUiApi public interface PointerInputModifierNode extends androidx.compose.ui.node.DelegatableNode {
     method public default boolean interceptOutOfBoundsChildEvents();
     method public void onCancelPointerInput();
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
new file mode 100644
index 0000000..42a92cf
--- /dev/null
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+InvalidNullConversion: androidx.compose.ui.graphics.GraphicsLayerModifierKt#graphicsLayer(androidx.compose.ui.Modifier, float, float, float, float, float, float, float, float, float, float, long, androidx.compose.ui.graphics.Shape, boolean, androidx.compose.ui.graphics.RenderEffect, long, long):
+    Attempted to remove @NonNull annotation from method androidx.compose.ui.graphics.GraphicsLayerModifierKt.graphicsLayer(androidx.compose.ui.Modifier,float,float,float,float,float,float,float,float,float,float,long,androidx.compose.ui.graphics.Shape,boolean,androidx.compose.ui.graphics.RenderEffect,long,long)
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 63c91dc..9c494ff 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -435,11 +435,25 @@
 
 package androidx.compose.ui.graphics {
 
+  @androidx.compose.runtime.Immutable @kotlin.jvm.JvmInline public final value class CompositingStrategy {
+    field public static final androidx.compose.ui.graphics.CompositingStrategy.Companion Companion;
+  }
+
+  public static final class CompositingStrategy.Companion {
+    method public int getAlways();
+    method public int getAuto();
+    method public int getModulateAlpha();
+    property public final int Always;
+    property public final int Auto;
+    property public final int ModulateAlpha;
+  }
+
   public final class GraphicsLayerModifierKt {
-    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
+    method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor, optional int compositingStrategy);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.GraphicsLayerScope,kotlin.Unit> block);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip);
     method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect);
+    method @Deprecated @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier! graphicsLayer(androidx.compose.ui.Modifier, optional float scaleX, optional float scaleY, optional float alpha, optional float translationX, optional float translationY, optional float shadowElevation, optional float rotationX, optional float rotationY, optional float rotationZ, optional float cameraDistance, optional long transformOrigin, optional androidx.compose.ui.graphics.Shape shape, optional boolean clip, optional androidx.compose.ui.graphics.RenderEffect? renderEffect, optional long ambientShadowColor, optional long spotShadowColor);
     method @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
@@ -448,6 +462,7 @@
     method public default long getAmbientShadowColor();
     method public float getCameraDistance();
     method public boolean getClip();
+    method public default int getCompositingStrategy();
     method public default androidx.compose.ui.graphics.RenderEffect? getRenderEffect();
     method public float getRotationX();
     method public float getRotationY();
@@ -456,6 +471,7 @@
     method public float getScaleY();
     method public float getShadowElevation();
     method public androidx.compose.ui.graphics.Shape getShape();
+    method public default long getSize();
     method public default long getSpotShadowColor();
     method public long getTransformOrigin();
     method public float getTranslationX();
@@ -464,6 +480,7 @@
     method public default void setAmbientShadowColor(long);
     method public void setCameraDistance(float);
     method public void setClip(boolean);
+    method public default void setCompositingStrategy(int);
     method public default void setRenderEffect(androidx.compose.ui.graphics.RenderEffect?);
     method public void setRotationX(float);
     method public void setRotationY(float);
@@ -480,6 +497,7 @@
     property public default long ambientShadowColor;
     property public abstract float cameraDistance;
     property public abstract boolean clip;
+    property public default int compositingStrategy;
     property public default androidx.compose.ui.graphics.RenderEffect? renderEffect;
     property public abstract float rotationX;
     property public abstract float rotationY;
@@ -488,6 +506,7 @@
     property public abstract float scaleY;
     property public abstract float shadowElevation;
     property public abstract androidx.compose.ui.graphics.Shape shape;
+    property public default long size;
     property public default long spotShadowColor;
     property public abstract long transformOrigin;
     property public abstract float translationX;
@@ -2280,6 +2299,9 @@
   public final class ObserverNodeKt {
   }
 
+  public final class ParentDataModifierNodeKt {
+  }
+
   public final class PointerInputModifierNodeKt {
   }
 
diff --git a/compose/ui/ui/build.gradle b/compose/ui/ui/build.gradle
index ca9a3c7..8caae0c 100644
--- a/compose/ui/ui/build.gradle
+++ b/compose/ui/ui/build.gradle
@@ -30,6 +30,20 @@
 
 dependencies {
 
+    constraints {
+        // In 1.4.0-alpha02 there was a change made in :compose:ui:ui which fixed an issue where
+        // we were over-invalidating layout. This change caused a corresponding regression in
+        // foundation's CoreText, where it was expecting a layout to happen but with this change
+        // it would not. A corresponding fix for this was added in 1.4.0-alpha02 of
+        // :compose:foundation:foundation. By adding this constraint, we are ensuring that the
+        // if an app has this ui module _and_ the foundation module as a dependency, then the
+        // version of foundation will be at least this version. This will prevent the bug in
+        // foundation from occurring. This does _NOT_ require that the app have foundation as
+        // a dependency.
+        implementation(project(":compose:foundation:foundation")) {
+            because 'prevents a critical bug in Text'
+        }
+    }
     if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
         /*
          * When updating dependencies, make sure to make the an an analogous update in the
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
index 1badff9..8203b93 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
@@ -141,7 +141,7 @@
         }
     }
     fun Modifier.circle(color: Color) = this then modifierElementOf(
-        params = color,
+        key = color,
         create = { CircleNode(color) },
         update = { it.color = color },
         definitions = {
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayerModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayerModifierSamples.kt
index 42b00e9..21cf8c6 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayerModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayerModifierSamples.kt
@@ -18,12 +18,19 @@
 
 import androidx.annotation.Sampled
 import androidx.compose.animation.core.Animatable
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.size
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
+import androidx.compose.ui.graphics.drawscope.inset
 import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.unit.dp
 
 @Sampled
 @Composable
@@ -45,4 +52,30 @@
     LaunchedEffect(animatedAlpha) {
         animatedAlpha.animateTo(1f)
     }
-}
\ No newline at end of file
+}
+
+@Sampled
+@Composable
+fun CompositingStrategyModulateAlpha() {
+    Canvas(
+        modifier =
+            Modifier.size(100.dp)
+            .background(Color.Black)
+            .graphicsLayer(
+                alpha = 0.5f,
+                compositingStrategy = CompositingStrategy.ModulateAlpha
+            )
+    ) {
+        // Configuring an alpha less than 1.0 and specifying
+        // CompositingStrategy.ModulateAlpha ends up with the overlapping region
+        // of the 2 draw rect calls to blend transparent blue and transparent red
+        // against the black background instead of just transparent blue which is what would
+        // occur with CompositingStrategy.Auto or CompositingStrategy.Always
+        inset(0f, 0f, size.width / 3, size.height / 3) {
+            drawRect(color = Color.Red)
+        }
+        inset(size.width / 3, size.height / 3, 0f, 0f) {
+            drawRect(color = Color.Blue)
+        }
+    }
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
index 1cbdf4a..469b655 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/LayoutSample.kt
@@ -208,7 +208,7 @@
         }
     }
     fun Modifier.verticalPadding(padding: Dp) = this then modifierElementOf(
-        params = padding,
+        key = padding,
         create = { VerticalPadding(padding) },
         update = { it.padding = padding },
         definitions = {
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
index 157af91..56d110a 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/ModifierSamples.kt
@@ -163,7 +163,7 @@
         }
     }
     fun Modifier.circle(color: Color) = this then modifierElementOf(
-        params = color,
+        key = color,
         create = { Circle(color) },
         update = { it.color = color },
         definitions = {
@@ -213,7 +213,7 @@
     }
 
     fun Modifier.onPointerEvent(callback: (PointerEvent) -> Unit) = this then modifierElementOf(
-        params = callback,
+        key = callback,
         create = { OnPointerEventNode(callback) },
         update = { it.callback = callback },
         definitions = {
@@ -234,7 +234,7 @@
     }
 
     fun Modifier.logSize(id: String) = this then modifierElementOf(
-        params = id,
+        key = id,
         create = { SizeLoggerNode(id) },
         update = { it.id = id },
         definitions = {
@@ -264,7 +264,7 @@
     }
 
     fun Modifier.logPosition(id: String) = this then modifierElementOf(
-        params = id,
+        key = id,
         create = { PositionLoggerNode(id) },
         update = { it.id = id },
         definitions = {
@@ -294,7 +294,7 @@
     }
 
     fun Modifier.logSize(id: String) = this then modifierElementOf(
-        params = id,
+        key = id,
         create = { SizeLoggerNode(id) },
         update = { it.id = id },
         definitions = {
@@ -304,7 +304,7 @@
     )
 
     fun Modifier.provideLogger(logger: Logger) = this then modifierElementOf(
-        params = logger,
+        key = logger,
         create = { ProvideLoggerNode(logger) },
         update = { it.provide(loggerLocal, logger) },
         definitions = {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
index 4302c8b..738a1e2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidLayoutDrawTest.kt
@@ -57,6 +57,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.DefaultShadowColor
 import androidx.compose.ui.graphics.Outline
 import androidx.compose.ui.graphics.Path
@@ -186,7 +187,132 @@
         }
     }
 
-    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    @Test
+    fun testCompositingStrategyAuto() {
+        drawLatch = CountDownLatch(1)
+        var compositingApplied = false
+        activity.runOnUiThread {
+            compositingApplied = when (Build.VERSION.SDK_INT) {
+                // Use public RenderNode API
+                in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
+                    verifyRenderNode29CompositingStrategy(
+                        CompositingStrategy.Auto,
+                        expectedCompositing = false,
+                        expectedOverlappingRendering = true
+                    )
+                // Cannot access private APIs on P
+                Build.VERSION_CODES.P ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.Auto,
+                        View.LAYER_TYPE_NONE,
+                        true
+                    )
+                // Use stub access to framework RenderNode API
+                in Build.VERSION_CODES.M..Int.MAX_VALUE ->
+                    verifyRenderNode23CompositingStrategy(
+                        CompositingStrategy.Auto,
+                        expectedLayerType = View.LAYER_TYPE_NONE,
+                        expectedOverlappingRendering = true
+                    )
+                // No RenderNodes, use Views instead
+                else ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.Auto,
+                        View.LAYER_TYPE_NONE,
+                    true
+                    )
+            }
+            drawLatch.countDown()
+        }
+
+        drawLatch.await(1, TimeUnit.SECONDS)
+        assertTrue(compositingApplied)
+    }
+
+    @Test
+    fun testCompositingStrategyModulateAlpha() {
+        drawLatch = CountDownLatch(1)
+        var compositingApplied = false
+        activity.runOnUiThread {
+            compositingApplied = when (Build.VERSION.SDK_INT) {
+                // Use public RenderNode API
+                in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
+                    verifyRenderNode29CompositingStrategy(
+                        CompositingStrategy.ModulateAlpha,
+                        expectedCompositing = false,
+                        expectedOverlappingRendering = false
+                    )
+                // Cannot access private APIs on P
+                Build.VERSION_CODES.P ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.ModulateAlpha,
+                        View.LAYER_TYPE_NONE,
+                        false
+                    )
+                // Use stub access to framework RenderNode API
+                in Build.VERSION_CODES.M..Int.MAX_VALUE ->
+                    verifyRenderNode23CompositingStrategy(
+                        CompositingStrategy.ModulateAlpha,
+                        expectedLayerType = View.LAYER_TYPE_NONE,
+                        expectedOverlappingRendering = false
+                    )
+                // No RenderNodes, use Views instead
+                else ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.ModulateAlpha,
+                        View.LAYER_TYPE_NONE,
+                        false
+                    )
+            }
+            drawLatch.countDown()
+        }
+
+        drawLatch.await(1, TimeUnit.SECONDS)
+        assertTrue(compositingApplied)
+    }
+
+    @Test
+    fun testCompositingStrategyAlways() {
+        drawLatch = CountDownLatch(1)
+        var compositingApplied = false
+        activity.runOnUiThread {
+            compositingApplied = when (Build.VERSION.SDK_INT) {
+                // Use public RenderNode API
+                in Build.VERSION_CODES.Q..Int.MAX_VALUE ->
+                    verifyRenderNode29CompositingStrategy(
+                        CompositingStrategy.Always,
+                        expectedCompositing = true,
+                        expectedOverlappingRendering = true
+                    )
+                // Cannot access private APIs on P
+                Build.VERSION_CODES.P ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.Always,
+                        View.LAYER_TYPE_HARDWARE,
+                        true
+                    )
+                // Use stub access to framework RenderNode API
+                in Build.VERSION_CODES.M..Int.MAX_VALUE ->
+                    verifyRenderNode23CompositingStrategy(
+                        CompositingStrategy.Always,
+                        expectedLayerType = View.LAYER_TYPE_HARDWARE,
+                        expectedOverlappingRendering = true
+                    )
+                // No RenderNodes, use Views instead
+                else ->
+                    verifyViewLayerCompositingStrategy(
+                        CompositingStrategy.Always,
+                        View.LAYER_TYPE_HARDWARE,
+                        true
+                    )
+            }
+            drawLatch.countDown()
+        }
+
+        drawLatch.await(1, TimeUnit.SECONDS)
+        assertTrue(compositingApplied)
+    }
+
     @Test
     fun testLayerCameraDistance() {
         val targetCameraDistance = 15f
@@ -223,6 +349,68 @@
     }
 
     @RequiresApi(Build.VERSION_CODES.Q)
+    private fun verifyRenderNode29CompositingStrategy(
+        compositingStrategy: CompositingStrategy,
+        expectedCompositing: Boolean,
+        expectedOverlappingRendering: Boolean
+    ): Boolean {
+        val node = RenderNodeApi29(AndroidComposeView(activity)).apply {
+            this.compositingStrategy = compositingStrategy
+        }
+        return expectedCompositing == node.isUsingCompositingLayer() &&
+            expectedOverlappingRendering == node.hasOverlappingRendering()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.M)
+    private fun verifyRenderNode23CompositingStrategy(
+        compositingStrategy: CompositingStrategy,
+        expectedLayerType: Int,
+        expectedOverlappingRendering: Boolean
+    ): Boolean {
+        val node = RenderNodeApi23(AndroidComposeView(activity)).apply {
+            this.compositingStrategy = compositingStrategy
+        }
+        return expectedLayerType == node.getLayerType() &&
+            expectedOverlappingRendering == node.hasOverlappingRendering()
+    }
+
+    private fun verifyViewLayerCompositingStrategy(
+        compositingStrategy: CompositingStrategy,
+        expectedLayerType: Int,
+        expectedOverlappingRendering: Boolean
+    ): Boolean {
+        val view = ViewLayer(
+            AndroidComposeView(activity),
+            ViewLayerContainer(activity),
+            {},
+            {}).apply {
+            updateLayerProperties(
+                scaleX = 1f,
+                scaleY = 1f,
+                alpha = 1f,
+                translationX = 0f,
+                translationY = 0f,
+                shadowElevation = 0f,
+                rotationX = 0f,
+                rotationY = 0f,
+                rotationZ = 0f,
+                cameraDistance = cameraDistance,
+                transformOrigin = TransformOrigin.Center,
+                shape = RectangleShape,
+                clip = true,
+                layoutDirection = LayoutDirection.Ltr,
+                density = Density(1f),
+                renderEffect = null,
+                ambientShadowColor = DefaultShadowColor,
+                spotShadowColor = DefaultShadowColor,
+                compositingStrategy = compositingStrategy
+            )
+        }
+        return expectedLayerType == view.layerType &&
+            expectedOverlappingRendering == view.hasOverlappingRendering()
+    }
+
+    @RequiresApi(Build.VERSION_CODES.Q)
     private fun verifyRenderNode29CameraDistance(cameraDistance: Float): Boolean =
         // Verify that the internal render node has the camera distance property
         // given to the wrapper
@@ -263,7 +451,8 @@
                 density = Density(1f),
                 renderEffect = null,
                 ambientShadowColor = DefaultShadowColor,
-                spotShadowColor = DefaultShadowColor
+                spotShadowColor = DefaultShadowColor,
+                compositingStrategy = CompositingStrategy.Auto
             )
         }
         // Verify that the camera distance is applied properly even after accounting for
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
index ed22b91..618ceb7 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/draw/GraphicsLayerModifierTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.DefaultCameraDistance
 import androidx.compose.ui.graphics.DefaultShadowColor
@@ -65,7 +66,8 @@
             ValueElement("cameraDistance", DefaultCameraDistance),
             ValueElement("transformOrigin", TransformOrigin.Center),
             ValueElement("shape", RectangleShape),
-            ValueElement("clip", false)
+            ValueElement("clip", false),
+            ValueElement("compositingStrategy", CompositingStrategy.Auto)
         )
     }
 
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 eb06090..38e267e 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
@@ -18,6 +18,8 @@
 
 import android.os.Build
 import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
@@ -43,8 +45,10 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.RoundRect
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
 import androidx.compose.ui.graphics.BlurEffect
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.GraphicsLayerScope
 import androidx.compose.ui.graphics.OffsetEffect
 import androidx.compose.ui.graphics.Outline
@@ -86,6 +90,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
+import kotlin.math.ceil
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
 import org.junit.Assert.assertTrue
@@ -94,6 +99,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.math.roundToInt
+import org.junit.Assert.assertNotNull
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -1149,4 +1155,198 @@
             assertEquals(2f, valueReadInGraphicsLayer)
         }
     }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun testCompositingStrategyModulateAlpha() {
+        val tag = "testTag"
+        val dimen = 200
+        rule.setContent {
+            Canvas(
+                modifier =
+                Modifier.testTag(tag)
+                    .size((dimen / LocalDensity.current.density).dp)
+                    .background(Color.Black)
+                    .graphicsLayer(
+                        alpha = 0.5f,
+                        compositingStrategy = CompositingStrategy.ModulateAlpha
+                    )
+            ) {
+                inset(0f, 0f, size.width / 3, size.height / 3) {
+                    drawRect(color = Color.Red)
+                }
+                inset(size.width / 3, size.height / 3, 0f, 0f) {
+                    drawRect(color = Color.Blue)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag).captureToImage().apply {
+            with(toPixelMap()) {
+                val redWithAlpha = Color.Red.copy(alpha = 0.5f)
+                val blueWithAlpha = Color.Blue.copy(alpha = 0.5f)
+                val bg = Color.Black
+                val expectedTopLeft = redWithAlpha.compositeOver(bg)
+                val expectedBottomRight = blueWithAlpha.compositeOver(bg)
+                val expectedCenter = blueWithAlpha.compositeOver(redWithAlpha).compositeOver(bg)
+                assertPixelColor(expectedTopLeft, 0, 0)
+                assertPixelColor(Color.Black, width - 1, 0)
+                assertPixelColor(expectedBottomRight, width - 1, height - 1)
+                assertPixelColor(Color.Black, 0, height - 1)
+                assertPixelColor(expectedCenter, width / 2, height / 2)
+            }
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun testCompositingStrategyAlways() {
+        val tag = "testTag"
+        val dimen = 200
+        rule.setContent {
+            Canvas(
+                modifier =
+                Modifier.testTag(tag)
+                    .size((dimen / LocalDensity.current.density).dp)
+                    .background(Color.LightGray)
+                    .graphicsLayer(
+                        compositingStrategy = CompositingStrategy.Always
+                    )
+            ) {
+                inset(0f, 0f, size.width / 3, size.height / 3) {
+                    drawRect(color = Color.Red)
+                }
+                inset(size.width / 3, size.height / 3, 0f, 0f) {
+                    drawRect(color = Color.Blue, blendMode = BlendMode.Xor)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag).captureToImage().apply {
+            with(toPixelMap()) {
+                assertPixelColor(Color.Red, 0, 0)
+                assertPixelColor(Color.LightGray, width - 1, 0)
+                assertPixelColor(Color.Blue, width - 1, height - 1)
+                assertPixelColor(Color.LightGray, 0, height - 1)
+                assertPixelColor(Color.LightGray, width / 2, height / 2)
+            }
+        }
+    }
+
+    @RequiresApi(Build.VERSION_CODES.O)
+    @Test
+    fun testCompositingStrategyAuto() {
+        val tag = "testTag"
+        val dimen = 200
+        rule.setContent {
+            Canvas(
+                modifier =
+                Modifier.testTag(tag)
+                    .size((dimen / LocalDensity.current.density).dp)
+                    .background(Color.Black)
+                    .graphicsLayer(
+                        alpha = 0.5f,
+                        compositingStrategy = CompositingStrategy.Auto
+                    )
+            ) {
+                inset(0f, 0f, size.width / 3, size.height / 3) {
+                    drawRect(color = Color.Red)
+                }
+                inset(size.width / 3, size.height / 3, 0f, 0f) {
+                    drawRect(color = Color.Blue)
+                }
+            }
+        }
+
+        rule.onNodeWithTag(tag).captureToImage().apply {
+            with(toPixelMap()) {
+                val redWithAlpha = Color.Red.copy(alpha = 0.5f)
+                val blueWithAlpha = Color.Blue.copy(alpha = 0.5f)
+                val bg = Color.Black
+                val expectedTopLeft = redWithAlpha.compositeOver(bg)
+                val expectedBottomRight = blueWithAlpha.compositeOver(bg)
+                val expectedCenter = blueWithAlpha.compositeOver(bg)
+                assertPixelColor(expectedTopLeft, 0, 0)
+                assertPixelColor(Color.Black, width - 1, 0)
+                assertPixelColor(expectedBottomRight, width - 1, height - 1)
+                assertPixelColor(Color.Black, 0, height - 1)
+                assertPixelColor(expectedCenter, width / 2, height / 2)
+            }
+        }
+    }
+
+    @Test
+    fun testGraphicsLayerScopeSize() {
+        val widthDp = 200.dp
+        val heightDp = 500.dp
+
+        var graphicsLayerWidth = 0f
+        var graphicsLayerHeight = 0f
+        var drawScopeWidth = -1f
+        var drawScopeHeight = -1f
+        rule.setContent {
+            Box(
+                modifier = Modifier
+                    .size(widthDp, heightDp)
+                    .graphicsLayer {
+                        graphicsLayerWidth = size.width
+                        graphicsLayerHeight = size.height
+                    }
+                    .drawBehind {
+                        drawScopeWidth = size.width
+                        drawScopeHeight = size.height
+                    }
+            )
+        }
+        rule.runOnIdle {
+            assertEquals(drawScopeWidth, graphicsLayerWidth)
+            assertEquals(drawScopeHeight, graphicsLayerHeight)
+        }
+    }
+
+    @Test
+    fun testGraphicsLayerSizeAfterRelayout() {
+        var composableSize by mutableStateOf(20.dp)
+        var graphicsLayerWidth = -1f
+        var graphicsLayerHeight = -1f
+        var drawScopeWidth = 0f
+        var drawScopeHeight = 0f
+
+        var density: Density? = null
+
+        rule.setContent {
+            density = LocalDensity.current
+            Box(modifier = Modifier
+                .size(composableSize, composableSize)
+                .graphicsLayer {
+                    graphicsLayerWidth = size.width
+                    graphicsLayerHeight = size.height
+                }
+                .drawBehind {
+                    drawScopeWidth = size.width
+                    drawScopeHeight = size.height
+                }
+            )
+        }
+
+        rule.waitForIdle()
+
+        assertNotNull(density)
+
+        var sizePx = with(density!!) { ceil(composableSize.toPx()) }
+        assertEquals(sizePx, graphicsLayerWidth)
+        assertEquals(sizePx, graphicsLayerHeight)
+        assertEquals(sizePx, drawScopeWidth)
+        assertEquals(sizePx, drawScopeHeight)
+
+        composableSize = 40.dp
+
+        rule.waitForIdle()
+
+        sizePx = with(density!!) { ceil(composableSize.toPx()) }
+        assertEquals(sizePx, graphicsLayerWidth)
+        assertEquals(sizePx, graphicsLayerHeight)
+        assertEquals(sizePx, drawScopeWidth)
+        assertEquals(sizePx, drawScopeHeight)
+    }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index 182fb02..2d2932e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -29,6 +29,7 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.Shape
@@ -3747,6 +3748,7 @@
                 renderEffect: RenderEffect?,
                 ambientShadowColor: Color,
                 spotShadowColor: Color,
+                compositingStrategy: CompositingStrategy,
                 layoutDirection: LayoutDirection,
                 density: Density
             ) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
index 973fec2..40d0700 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/DelegatableNodeTest.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -277,6 +278,36 @@
     }
 
     @Test
+    fun visitAncestors_sameLayoutNode_calledDuringOnDetach() {
+        // Arrange.
+        val (node1, node2) = List(5) { object : Modifier.Node() {} }
+        val visitedAncestors = mutableListOf<Modifier.Node>()
+        val detachableNode = DetachableNode {
+            it.visitAncestors(Nodes.Any) { node ->
+                visitedAncestors.add(node)
+            }
+        }
+        val removeNode = mutableStateOf(false)
+        rule.setContent {
+            Box(
+                modifier = modifierElementOf { node1 }
+                    .then(modifierElementOf { node2 })
+                    .then(if (removeNode.value) Modifier else modifierElementOf { detachableNode })
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { removeNode.value = true }
+
+        // Assert.
+        rule.runOnIdle {
+            assertThat(visitedAncestors)
+                .containsAtLeastElementsIn(arrayOf(node2, node1))
+                .inOrder()
+        }
+    }
+
+    @Test
     fun nearestAncestorInDifferentLayoutNode_nonContiguousParentLayoutNode() {
         // Arrange.
         val (node1, node2) = List(2) { object : Modifier.Node() {} }
@@ -297,13 +328,42 @@
         assertThat(parent).isEqualTo(node1)
     }
 
+    @Test
+    fun delegatedNodeGetsCoordinator() {
+        val node = object : DelegatingNode() {
+            val inner = delegated {
+                object : Modifier.Node() { }
+            }
+        }
+
+        rule.setContent {
+            Box(modifier = modifierElementOf { node })
+        }
+
+        rule.runOnIdle {
+            assertThat(node.isAttached).isTrue()
+            assertThat(node.coordinator).isNotNull()
+            assertThat(node.inner.isAttached).isTrue()
+            assertThat(node.inner.coordinator).isNotNull()
+            assertThat(node.inner.coordinator).isEqualTo(node.coordinator)
+        }
+    }
+
     private fun Modifier.otherModifier(): Modifier = this.then(Modifier)
 }
 
 @ExperimentalComposeUiApi
 internal inline fun <reified T : Modifier.Node> modifierElementOf(
     crossinline create: () -> T,
-): Modifier = object : ModifierNodeElement<T>(null, {}) {
+): Modifier = object : ModifierNodeElement<T>(null, true, {}) {
     override fun create(): T = create()
     override fun update(node: T): T = node
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private class DetachableNode(val onDetach: (DetachableNode) -> Unit) : Modifier.Node() {
+    override fun onDetach() {
+        onDetach.invoke(this)
+        super.onDetach()
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
index 88a985f..5694bd1 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/node/NodeChainTester.kt
@@ -213,7 +213,7 @@
 }
 
 fun modifierA(params: Any? = null): Modifier.Element {
-    return object : ModifierNodeElement<A>(params, {}) {
+    return object : ModifierNodeElement<A>(params, true, {}) {
         override fun create(): A = A()
         override fun update(node: A): A = node
         override fun toString(): String = "a"
@@ -225,7 +225,7 @@
 }
 
 fun modifierB(params: Any? = null): Modifier.Element {
-    return object : ModifierNodeElement<B>(params, {}) {
+    return object : ModifierNodeElement<B>(params, true, {}) {
         override fun create(): B = B()
         override fun update(node: B): B = node
         override fun toString(): String = "b"
@@ -237,7 +237,7 @@
 }
 
 fun modifierC(params: Any? = null): Modifier.Element {
-    return object : ModifierNodeElement<C>(params, {}) {
+    return object : ModifierNodeElement<C>(params, true, {}) {
         override fun create(): C = C()
         override fun update(node: C): C = node
         override fun toString(): String = "c"
@@ -248,7 +248,7 @@
     class N : Modifier.Node() {
         override fun toString(): String = "d"
     }
-    return object : ModifierNodeElement<N>(params, {}) {
+    return object : ModifierNodeElement<N>(params, true, {}) {
         override fun create(): N = N()
         override fun update(node: N): N = node
         override fun toString(): String = "d"
@@ -258,7 +258,7 @@
 fun managedModifier(
     name: String,
     params: Any? = null
-): ModifierNodeElement<*> = object : ModifierNodeElement<Modifier.Node>(params, {}) {
+): ModifierNodeElement<*> = object : ModifierNodeElement<Modifier.Node>(params, true, {}) {
     override fun create(): Modifier.Node = object : Modifier.Node() {}
     override fun update(node: Modifier.Node): Modifier.Node = node
     override fun toString(): String = name
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.android.kt
index 1d4ad40..dd01402 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/DeviceRenderNode.android.kt
@@ -19,6 +19,7 @@
 import android.graphics.Outline
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RenderEffect
 
@@ -52,6 +53,7 @@
     var alpha: Float
     var renderEffect: RenderEffect?
     val hasDisplayList: Boolean
+    var compositingStrategy: CompositingStrategy
 
     fun setOutline(outline: Outline?)
     fun setPosition(left: Int, top: Int, right: Int, bottom: Int): Boolean
@@ -109,5 +111,6 @@
     var clipToOutline: Boolean,
     var clipToBounds: Boolean,
     var alpha: Float,
-    var renderEffect: RenderEffect?
+    var renderEffect: RenderEffect?,
+    var compositingStrategy: CompositingStrategy
 )
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.android.kt
index aaa068c..0958df1 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi23.android.kt
@@ -21,9 +21,11 @@
 import android.view.RenderNode
 import android.view.DisplayListCanvas
 import android.os.Build
+import android.view.View
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RenderEffect
 
@@ -37,6 +39,8 @@
 internal class RenderNodeApi23(val ownerView: AndroidComposeView) : DeviceRenderNode {
     private val renderNode = RenderNode.create("Compose", ownerView)
 
+    private var internalCompositingStrategy = CompositingStrategy.Auto
+
     init {
         if (needToValidateAccess) {
             // This is only to force loading the DisplayListCanvas class and causing the
@@ -68,6 +72,8 @@
             renderNode.offsetTopAndBottom(0)
             verifyShadowColorProperties(renderNode)
             discardDisplayListInternal()
+            renderNode.setLayerType(View.LAYER_TYPE_NONE)
+            renderNode.setHasOverlappingRendering(renderNode.hasOverlappingRendering())
             needToValidateAccess = false // only need to do this once
         }
         if (testFailCreateRenderNode) {
@@ -206,6 +212,33 @@
             renderNode.alpha = value
         }
 
+    override var compositingStrategy: CompositingStrategy
+        get() = internalCompositingStrategy
+        set(value) {
+            when (value) {
+                CompositingStrategy.Always -> {
+                    renderNode.setLayerType(View.LAYER_TYPE_HARDWARE)
+                    renderNode.setHasOverlappingRendering(true)
+                }
+                CompositingStrategy.ModulateAlpha -> {
+                    renderNode.setLayerType(View.LAYER_TYPE_NONE)
+                    renderNode.setHasOverlappingRendering(false)
+                }
+                else -> { // CompositingStrategy.Auto
+                    renderNode.setLayerType(View.LAYER_TYPE_NONE)
+                    renderNode.setHasOverlappingRendering(true)
+                }
+            }
+            internalCompositingStrategy = value
+        }
+
+    internal fun getLayerType(): Int = when (internalCompositingStrategy) {
+        CompositingStrategy.Always -> View.LAYER_TYPE_HARDWARE
+        else -> View.LAYER_TYPE_NONE
+    }
+
+    internal fun hasOverlappingRendering(): Boolean = renderNode.hasOverlappingRendering()
+
     override val hasDisplayList: Boolean
         get() = renderNode.isValid
 
@@ -295,7 +328,8 @@
             // on it since this is a write only field
             clipToBounds = clipToBounds,
             alpha = renderNode.alpha,
-            renderEffect = renderEffect
+            renderEffect = renderEffect,
+            compositingStrategy = internalCompositingStrategy
         )
 
     override fun discardDisplayList() {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.android.kt
index 7b0c7ab..fd1c8cc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeApi29.android.kt
@@ -22,6 +22,7 @@
 import androidx.annotation.RequiresApi
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RenderEffect
 
@@ -34,6 +35,12 @@
 
     private var internalRenderEffect: RenderEffect? = null
 
+    private var internalCompositingStrategy: CompositingStrategy = CompositingStrategy.Auto
+
+    internal fun isUsingCompositingLayer(): Boolean = renderNode.useCompositingLayer
+
+    internal fun hasOverlappingRendering(): Boolean = renderNode.hasOverlappingRendering()
+
     override val uniqueId: Long get() = renderNode.uniqueId
 
     override val left: Int get() = renderNode.left
@@ -148,6 +155,28 @@
             }
         }
 
+    override var compositingStrategy: CompositingStrategy
+        get() = internalCompositingStrategy
+        set(value) {
+            with(renderNode) {
+                when (value) {
+                    CompositingStrategy.Always -> {
+                        setUseCompositingLayer(true, null)
+                        setHasOverlappingRendering(true)
+                    }
+                    CompositingStrategy.ModulateAlpha -> {
+                        setUseCompositingLayer(false, null)
+                        setHasOverlappingRendering(false)
+                    }
+                    else -> { // CompositingStrategy.Auto
+                        setUseCompositingLayer(false, null)
+                        setHasOverlappingRendering(true)
+                    }
+                }
+            }
+            internalCompositingStrategy = value
+        }
+
     override val hasDisplayList: Boolean
         get() = renderNode.hasDisplayList()
 
@@ -225,7 +254,8 @@
             clipToOutline = renderNode.clipToOutline,
             clipToBounds = renderNode.clipToBounds,
             alpha = renderNode.alpha,
-            renderEffect = internalRenderEffect
+            renderEffect = internalRenderEffect,
+            compositingStrategy = internalCompositingStrategy
         )
 
     override fun discardDisplayList() {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index 07f4b2e..f718f7b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.RectangleShape
@@ -123,6 +124,7 @@
         renderEffect: RenderEffect?,
         ambientShadowColor: Color,
         spotShadowColor: Color,
+        compositingStrategy: CompositingStrategy,
         layoutDirection: LayoutDirection,
         density: Density
     ) {
@@ -145,6 +147,7 @@
         renderNode.clipToOutline = clip && shape !== RectangleShape
         renderNode.clipToBounds = clip && shape === RectangleShape
         renderNode.renderEffect = renderEffect
+        renderNode.compositingStrategy = compositingStrategy
         val shapeChanged = outlineResolver.update(
             shape,
             renderNode.alpha,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 55d1dcd..baadbdf 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RectangleShape
@@ -84,6 +85,8 @@
      */
     private var mTransformOrigin: TransformOrigin = TransformOrigin.Center
 
+    private var mHasOverlappingRendering = true
+
     init {
         setWillNotDraw(false) // we WILL draw
         id = generateViewId()
@@ -140,6 +143,7 @@
         renderEffect: RenderEffect?,
         ambientShadowColor: Color,
         spotShadowColor: Color,
+        compositingStrategy: CompositingStrategy,
         layoutDirection: LayoutDirection,
         density: Density
     ) {
@@ -187,6 +191,26 @@
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
             ViewLayerVerificationHelper31.setRenderEffect(this, renderEffect)
         }
+
+        mHasOverlappingRendering = when (compositingStrategy) {
+            CompositingStrategy.Always -> {
+                setLayerType(LAYER_TYPE_HARDWARE, null)
+                true
+            }
+
+            CompositingStrategy.ModulateAlpha -> {
+                setLayerType(LAYER_TYPE_NONE, null)
+                false
+            }
+            else -> { // CompositingStrategy.Auto
+                setLayerType(LAYER_TYPE_NONE, null)
+                true
+            }
+        }
+    }
+
+    override fun hasOverlappingRendering(): Boolean {
+        return mHasOverlappingRendering
     }
 
     override fun isInLayer(position: Offset): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
index 612a9c5..77848a3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.LayoutModifier
@@ -191,7 +192,8 @@
     transformOrigin = transformOrigin,
     shape = shape,
     clip = clip,
-    renderEffect = renderEffect
+    renderEffect = renderEffect,
+    compositingStrategy = CompositingStrategy.Auto
 )
 
 /**
@@ -237,6 +239,16 @@
  * @param ambientShadowColor see [GraphicsLayerScope.ambientShadowColor]
  * @param spotShadowColor see [GraphicsLayerScope.spotShadowColor]
  */
+@Deprecated(
+    "Replace with graphicsLayer that consumes a compositing strategy",
+    replaceWith = ReplaceWith(
+        "Modifier.graphicsLayer(scaleX, scaleY, alpha, translationX, translationY, " +
+            "shadowElevation, rotationX, rotationY, rotationZ, cameraDistance, transformOrigin, " +
+            "shape, clip, null, DefaultShadowColor, DefaultShadowColor, CompositingStrategy.Auto)",
+        "androidx.compose.ui.graphics"
+    ),
+    level = DeprecationLevel.HIDDEN
+)
 @Stable
 fun Modifier.graphicsLayer(
     scaleX: Float = 1f,
@@ -300,6 +312,120 @@
  * invalidated separately from parents. A [graphicsLayer] should be used when the content
  * updates independently from anything above it to minimize the invalidated content.
  *
+ * [graphicsLayer] can also be used to apply effects to content, such as scaling ([scaleX], [scaleY]),
+ * rotation ([rotationX], [rotationY], [rotationZ]), opacity ([alpha]), shadow
+ * ([shadowElevation], [shape]), clipping ([clip], [shape]), as well as altering the result of the
+ * layer with [RenderEffect]. Shadow color and ambient colors can be modified by configuring the
+ * [spotShadowColor] and [ambientShadowColor] respectively.
+ *
+ * [CompositingStrategy] determines whether or not the contents of this layer are rendered into
+ * an offscreen buffer. This is useful in order to optimize alpha usages with
+ * [CompositingStrategy.ModulateAlpha] which will skip the overhead of an offscreen buffer but can
+ * generate different rendering results depending on whether or not the contents of the layer are
+ * overlapping. Similarly leveraging [CompositingStrategy.Always] is useful in situations where
+ * creating an offscreen buffer is preferred usually in conjunction with [BlendMode] usage.
+ *
+ * Note that if you provide a non-zero [shadowElevation] and if the passed [shape] is concave the
+ * shadow will not be drawn on Android versions less than 10.
+ *
+ * Also note that alpha values less than 1.0f will have their contents implicitly clipped to their
+ * bounds unless [CompositingStrategy.ModulateAlpha] is specified.
+ * This is because an intermediate compositing layer is created to render contents into
+ * first before being drawn into the destination with the desired alpha.
+ * This layer is sized to the bounds of the composable this modifier is configured on, and contents
+ * outside of these bounds are omitted.
+ *
+ * If the layer parameters are backed by a [androidx.compose.runtime.State] or an animated value
+ * prefer an overload with a lambda block on [GraphicsLayerScope] as reading a state inside the block
+ * will only cause the layer properties update without triggering recomposition and relayout.
+ *
+ * @sample androidx.compose.ui.samples.ChangeOpacity
+ * @sample androidx.compose.ui.samples.CompositingStrategyModulateAlpha
+ *
+ * @param scaleX see [GraphicsLayerScope.scaleX]
+ * @param scaleY see [GraphicsLayerScope.scaleY]
+ * @param alpha see [GraphicsLayerScope.alpha]
+ * @param translationX see [GraphicsLayerScope.translationX]
+ * @param translationY see [GraphicsLayerScope.translationY]
+ * @param shadowElevation see [GraphicsLayerScope.shadowElevation]
+ * @param rotationX see [GraphicsLayerScope.rotationX]
+ * @param rotationY see [GraphicsLayerScope.rotationY]
+ * @param rotationZ see [GraphicsLayerScope.rotationZ]
+ * @param cameraDistance see [GraphicsLayerScope.cameraDistance]
+ * @param transformOrigin see [GraphicsLayerScope.transformOrigin]
+ * @param shape see [GraphicsLayerScope.shape]
+ * @param clip see [GraphicsLayerScope.clip]
+ * @param renderEffect see [GraphicsLayerScope.renderEffect]
+ * @param ambientShadowColor see [GraphicsLayerScope.ambientShadowColor]
+ * @param spotShadowColor see [GraphicsLayerScope.spotShadowColor]
+ * @param compositingStrategy see [GraphicsLayerScope.compositingStrategy]
+ */
+@Stable
+fun Modifier.graphicsLayer(
+    scaleX: Float = 1f,
+    scaleY: Float = 1f,
+    alpha: Float = 1f,
+    translationX: Float = 0f,
+    translationY: Float = 0f,
+    shadowElevation: Float = 0f,
+    rotationX: Float = 0f,
+    rotationY: Float = 0f,
+    rotationZ: Float = 0f,
+    cameraDistance: Float = DefaultCameraDistance,
+    transformOrigin: TransformOrigin = TransformOrigin.Center,
+    shape: Shape = RectangleShape,
+    clip: Boolean = false,
+    renderEffect: RenderEffect? = null,
+    ambientShadowColor: Color = DefaultShadowColor,
+    spotShadowColor: Color = DefaultShadowColor,
+    compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
+) = this.then(
+    SimpleGraphicsLayerModifier(
+        scaleX = scaleX,
+        scaleY = scaleY,
+        alpha = alpha,
+        translationX = translationX,
+        translationY = translationY,
+        shadowElevation = shadowElevation,
+        rotationX = rotationX,
+        rotationY = rotationY,
+        rotationZ = rotationZ,
+        cameraDistance = cameraDistance,
+        transformOrigin = transformOrigin,
+        shape = shape,
+        clip = clip,
+        renderEffect = renderEffect,
+        ambientShadowColor = ambientShadowColor,
+        spotShadowColor = spotShadowColor,
+        compositingStrategy = compositingStrategy,
+        inspectorInfo = debugInspectorInfo {
+            name = "graphicsLayer"
+            properties["scaleX"] = scaleX
+            properties["scaleY"] = scaleY
+            properties["alpha"] = alpha
+            properties["translationX"] = translationX
+            properties["translationY"] = translationY
+            properties["shadowElevation"] = shadowElevation
+            properties["rotationX"] = rotationX
+            properties["rotationY"] = rotationY
+            properties["rotationZ"] = rotationZ
+            properties["cameraDistance"] = cameraDistance
+            properties["transformOrigin"] = transformOrigin
+            properties["shape"] = shape
+            properties["clip"] = clip
+            properties["renderEffect"] = renderEffect
+            properties["ambientShadowColor"] = ambientShadowColor
+            properties["spotShadowColor"] = spotShadowColor
+            properties["compositingStrategy"] = compositingStrategy
+        }
+    )
+)
+
+/**
+ * A [Modifier.Element] that makes content draw into a draw layer. The draw layer can be
+ * invalidated separately from parents. A [graphicsLayer] should be used when the content
+ * updates independently from anything above it to minimize the invalidated content.
+ *
  * [graphicsLayer] can be used to apply effects to content, such as scaling, rotation, opacity,
  * shadow, and clipping.
  * Prefer this version when you have layer properties backed by a
@@ -323,6 +449,51 @@
     )
 
 /**
+ * Determines when to rendering the contents of the buffer into an offscreen layer before
+ * being drawn to the destination.
+ */
+@Immutable
+@kotlin.jvm.JvmInline
+value class CompositingStrategy internal constructor(
+    @Suppress("unused") private val value: Int
+) {
+
+    companion object {
+
+        /**
+         * Rendering to an offscreen buffer will be determined automatically by the rest of the
+         * graphicsLayer parameters. This is the default behavior.
+         * For example, whenever an alpha value less than 1.0f is provided on [Modifier.graphicsLayer],
+         * a compositing layer is created automatically to first render the contents fully opaque,
+         * then draw this offscreen buffer to the destination with the corresponding alpha. This is
+         * necessary for correctness otherwise alpha applied to individual drawing instructions that
+         * overlap will have a different result than expected
+         */
+        val Auto = CompositingStrategy(0)
+
+        /**
+         * Rendering of content will always be rendered into an offscreen buffer first then drawn to
+         * the destination regardless of the other parameters configured on the graphics
+         * layer. This is useful for leveraging different blending algorithms for masking content.
+         * For example, the contents can be drawn into this graphics layer and masked out by drawing
+         * additional shapes with [BlendMode.Clear]
+         */
+        val Always = CompositingStrategy(1)
+
+        /**
+         * Modulates alpha for each of the drawing instructions recorded within the graphicsLayer.
+         * This avoids usage of an offscreen buffer for purposes of alpha rendering.
+         * [ModulateAlpha] is more efficient than [Auto] in performance in scenarios where an alpha
+         * value less than 1.0f is provided. Otherwise the performance is similar to that of [Auto].
+         * However, this can provide different results than [Auto] if there is overlapping content
+         * within the layer and alpha is applied. This should only be used if the contents of the layer
+         * are known well in advance and are expected to not be overlapping.
+         */
+        val ModulateAlpha = CompositingStrategy(2)
+    }
+}
+
+/**
  * A [Modifier.Element] that adds a draw layer such that tooling can identify an element
  * in the drawn image.
  */
@@ -376,6 +547,7 @@
     private val renderEffect: RenderEffect?,
     private val ambientShadowColor: Color,
     private val spotShadowColor: Color,
+    private val compositingStrategy: CompositingStrategy = CompositingStrategy.Auto,
     inspectorInfo: InspectorInfo.() -> Unit
 ) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
 
@@ -396,6 +568,7 @@
         renderEffect = this@SimpleGraphicsLayerModifier.renderEffect
         ambientShadowColor = this@SimpleGraphicsLayerModifier.ambientShadowColor
         spotShadowColor = this@SimpleGraphicsLayerModifier.spotShadowColor
+        compositingStrategy = this@SimpleGraphicsLayerModifier.compositingStrategy
     }
 
     override fun MeasureScope.measure(
@@ -425,6 +598,7 @@
         result = 31 * result + renderEffect.hashCode()
         result = 31 * result + ambientShadowColor.hashCode()
         result = 31 * result + spotShadowColor.hashCode()
+        result = 31 * result + compositingStrategy.hashCode()
         return result
     }
 
@@ -445,7 +619,8 @@
             clip == otherModifier.clip &&
             renderEffect == otherModifier.renderEffect &&
             ambientShadowColor == otherModifier.ambientShadowColor &&
-            spotShadowColor == otherModifier.spotShadowColor
+            spotShadowColor == otherModifier.spotShadowColor &&
+            compositingStrategy == otherModifier.compositingStrategy
     }
 
     override fun toString(): String =
@@ -465,5 +640,6 @@
             "clip=$clip, " +
             "renderEffect=$renderEffect, " +
             "ambientShadowColor=$ambientShadowColor, " +
-            "spotShadowColor=$spotShadowColor)"
+            "spotShadowColor=$spotShadowColor, " +
+            "compositingStrategy=$compositingStrategy)"
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
index 2c063cb..5871e7f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerScope.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.graphics
 
+import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
 
@@ -196,6 +197,24 @@
     var renderEffect: RenderEffect?
         get() = null
         set(_) {}
+
+    /**
+     * Determines the [CompositingStrategy] used to render the contents of this graphicsLayer
+     * into an offscreen buffer first before rendering to the destination
+     */
+    var compositingStrategy: CompositingStrategy
+        get() = CompositingStrategy.Auto
+        // Keep the parameter name so current.txt maintains it for named parameter usage
+        @Suppress("UNUSED_PARAMETER")
+        set(compositingStrategy) {}
+
+    /**
+     * [Size] of the graphicsLayer represented in pixels. Drawing commands can extend beyond
+     * the size specified, however, if the graphicsLayer is promoted to an offscreen rasterization
+     * layer, any content rendered outside of the specified size will be clipped.
+     */
+    val size: Size
+        get() = Size.Zero
 }
 
 /**
@@ -219,6 +238,8 @@
     override var transformOrigin: TransformOrigin = TransformOrigin.Center
     override var shape: Shape = RectangleShape
     override var clip: Boolean = false
+    override var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
+    override var size: Size = Size.Zero
 
     internal var graphicsDensity: Density = Density(1.0f)
 
@@ -247,5 +268,7 @@
         shape = RectangleShape
         clip = false
         renderEffect = null
+        compositingStrategy = CompositingStrategy.Auto
+        size = Size.Zero
     }
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
index 4b48207..b18bbaa6 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/vector/ImageVector.kt
@@ -193,7 +193,7 @@
             false
         )
 
-        private val nodes = Stack<GroupParams>()
+        private val nodes = ArrayList<GroupParams>()
 
         private var root = GroupParams()
         private var isConsumed = false
@@ -760,11 +760,8 @@
     clearGroup()
 }
 
-@kotlin.jvm.JvmInline
-private value class Stack<T>(private val backing: ArrayList<T> = ArrayList<T>()) {
-    val size: Int get() = backing.size
+private fun <T> ArrayList<T>.push(value: T): Boolean = add(value)
 
-    fun push(value: T): Boolean = backing.add(value)
-    fun pop(): T = backing.removeAt(size - 1)
-    fun peek(): T = backing[size - 1]
-}
+private fun <T> ArrayList<T>.pop(): T = this.removeAt(size - 1)
+
+private fun <T> ArrayList<T>.peek(): T = this[size - 1]
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
index 3842944..a449536 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierLocalManager.kt
@@ -41,9 +41,10 @@
  */
 @OptIn(ExperimentalComposeUiApi::class)
 internal class ModifierLocalManager(val owner: Owner) {
-    private val inserted = mutableVectorOf<Pair<BackwardsCompatNode, ModifierLocal<*>>>()
-    private val updated = mutableVectorOf<Pair<BackwardsCompatNode, ModifierLocal<*>>>()
-    private val removed = mutableVectorOf<Pair<LayoutNode, ModifierLocal<*>>>()
+    private val inserted = mutableVectorOf<BackwardsCompatNode>()
+    private val insertedLocal = mutableVectorOf<ModifierLocal<*>>()
+    private val removed = mutableVectorOf<LayoutNode>()
+    private val removedLocal = mutableVectorOf<ModifierLocal<*>>()
     private var invalidated: Boolean = false
 
     fun invalidate() {
@@ -60,7 +61,8 @@
         // both the rmoved node and the inserted one, so we store all of the consumers we want to
         // update in a set and call update on them at the end.
         val toUpdate = hashSetOf<BackwardsCompatNode>()
-        removed.forEach { (layout, key) ->
+        removed.forEachIndexed { i, layout ->
+            val key = removedLocal[i]
             if (layout.isAttached) {
                 // if the layout is still attached, that means that this provider got removed and
                 // there's possible some consumers below it that need to be updated
@@ -68,21 +70,18 @@
             }
         }
         removed.clear()
+        removedLocal.clear()
         // TODO(lmr): we could potentially opt for a more sophisticated strategy here where we
         //  start from the higher up nodes, and invalidate in a way where during traversal if we
         //  happen upon other inserted nodes we can remove them from the inserted set
-        inserted.forEach { (node, key) ->
+        inserted.forEachIndexed { i, node ->
+            val key = insertedLocal[i]
             if (node.isAttached) {
                 invalidateConsumersOfNodeForKey(node, key, toUpdate)
             }
         }
         inserted.clear()
-        updated.forEach { (node, key) ->
-            if (node.isAttached) {
-                invalidateConsumersOfNodeForKey(node, key, toUpdate)
-            }
-        }
-        updated.clear()
+        insertedLocal.clear()
         toUpdate.forEach { it.updateModifierLocalConsumer() }
     }
 
@@ -103,17 +102,20 @@
     }
 
     fun updatedProvider(node: BackwardsCompatNode, key: ModifierLocal<*>) {
-        updated += node to key
+        inserted += node
+        insertedLocal += key
         invalidate()
     }
 
     fun insertedProvider(node: BackwardsCompatNode, key: ModifierLocal<*>) {
-        inserted += node to key
+        inserted += node
+        insertedLocal += key
         invalidate()
     }
 
     fun removedProvider(node: BackwardsCompatNode, key: ModifierLocal<*>) {
-        removed += node.requireLayoutNode() to key
+        removed += node.requireLayoutNode()
+        removedLocal += key
         invalidate()
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
index ba36ff7..6fb14bb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/BackwardsCompatNode.kt
@@ -242,7 +242,7 @@
 
     override fun onMeasureResultChanged() {
         invalidateCache = true
-        requestDraw()
+        invalidateDraw()
     }
 
     private fun updateDrawCache() {
@@ -259,7 +259,7 @@
 
     internal fun onDrawCacheReadsChanged() {
         invalidateCache = true
-        requestDraw()
+        invalidateDraw()
     }
 
     private var focusOrderElement: FocusPropertiesModifier? = null
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
index 2a0a74a..3b5ac23 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatingNode.kt
@@ -41,7 +41,7 @@
 
     /**
      * In order to properly delegate work to another [Modifier.Node], the delegated instance must
-     * be created and returned inside of a [delegated] or [lazyDelegated] call. Doing this will
+     * be created and returned inside of a [delegated] call. Doing this will
      * ensure that the created node instance follows all of the right lifecycles and is properly
      * discoverable in this position of the node tree.
      *
@@ -51,8 +51,6 @@
      * This method can be called from within an `init` block, however the returned delegated node
      * will not be attached until the delegating node is attached. If [delegated] is called after
      * the delegating node is already attached, the returned delegated node will be attached.
-     *
-     * @see lazyDelegated
      */
     fun <T : Modifier.Node> delegated(fn: () -> T): T {
         val owner = node
@@ -74,26 +72,6 @@
         delegate = node
     }
 
-    /**
-     * In order to properly delegate work to another [Modifier.Node], the delegated instance must
-     * be created and returned inside of a [delegated] or [lazyDelegated] call. Doing this will
-     * ensure that the created node instance follows all of the right lifecycles and is properly
-     * discoverable in this position of the node tree.
-     *
-     * By using [lazyDelegated], the [fn] parameter is executed asynchronously, and the result is
-     * returned is a [Lazy] instance whose value is equivalent to the result of calling [delegated]
-     * with the provided [fn].
-     *
-     * This method can be useful if you want to delegate to another [Modifier.Node], but want to
-     * lazily allocate it only once it is needed.
-     *
-     * @see delegated
-     * @see Lazy
-     */
-    fun <T : Modifier.Node> lazyDelegated(fn: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE) {
-        delegated(fn)
-    }
-
     private inline fun forEachDelegate(block: (Modifier.Node) -> Unit) {
         var node: Modifier.Node? = delegate
         while (node != null) {
@@ -105,7 +83,7 @@
     override fun onAttach() {
         super.onAttach()
         forEachDelegate {
-            updateCoordinator(coordinator)
+            it.updateCoordinator(coordinator)
             it.attach()
         }
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
index 69c46bb..e633f82 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DrawModifierNode.kt
@@ -34,8 +34,12 @@
     fun onMeasureResultChanged() {}
 }
 
+/**
+ * Invalidates this modifier's draw layer, ensuring that a draw pass will
+ * be run on the next frame.
+ */
 @ExperimentalComposeUiApi
-internal fun DrawModifierNode.requestDraw() {
+fun DrawModifierNode.invalidateDraw() {
     if (node.isAttached) {
         requireCoordinator(Nodes.Any).invalidateLayer()
     }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
index 456405d..a68a547 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutModifierNode.kt
@@ -128,12 +128,27 @@
     )
 }
 
+/**
+ * This will invalidate the current node's layer, and ensure that the layer is redrawn for the next
+ * frame.
+ */
 @ExperimentalComposeUiApi
-internal fun LayoutModifierNode.invalidateLayer() =
+fun LayoutModifierNode.invalidateLayer() =
     requireCoordinator(Nodes.Layout).invalidateLayer()
 
+/**
+ * This will invalidate the current node's layout pass, and ensure that relayout of this node will
+ * happen for the next frame.
+ */
 @ExperimentalComposeUiApi
-internal fun LayoutModifierNode.requestRelayout() = requireLayoutNode().requestRelayout()
+fun LayoutModifierNode.invalidateLayout() = requireLayoutNode().requestRelayout()
+
+/**
+ * This invalidates the current node's measure result, and ensures that a remeasurement of this node
+ * will happen for the next frame.
+ */
+@ExperimentalComposeUiApi
+fun LayoutModifierNode.invalidateMeasurements() = requireLayoutNode().invalidateMeasurements()
 
 @ExperimentalComposeUiApi
 internal fun LayoutModifierNode.requestRemeasure() = requireLayoutNode().requestRemeasure()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 85db8a6..5dcc5f8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -384,7 +384,7 @@
         mLookaheadScope =
             parent?.mLookaheadScope ?: if (isLookaheadRoot) LookaheadScope(this) else null
 
-        nodes.attach()
+        nodes.attach(performInvalidations = false)
         _foldedChildren.forEach { child ->
             child.attach(owner)
         }
@@ -732,38 +732,21 @@
                 "Modifiers are not supported on virtual LayoutNodes"
             }
             field = value
-            val oldShouldInvalidateParentLayer = shouldInvalidateParentLayer()
-            val oldOuterCoordinator = outerCoordinator
-
             nodes.updateFrom(value)
 
             // TODO(lmr): we don't need to do this every time and should attempt to avoid it
             //  whenever possible!
             forEachCoordinatorIncludingInner {
-                it.onInitialize()
                 it.updateLookaheadScope(mLookaheadScope)
             }
 
-            // TODO(lmr): lets move this to the responsibility of the nodes
             layoutDelegate.updateParentData()
-
-            // TODO(lmr): lets move this to the responsibility of the nodes
-            if (oldShouldInvalidateParentLayer || shouldInvalidateParentLayer())
-                parent?.invalidateLayer()
-
-            // TODO(lmr): this logic is not clear to me, but we want to move all invalidate* calls
-            //  to the responsibility of the nodes to avoid unnecessary work. Let's try to include
-            //  this one as well since it looks like it will be hit quite a bit
-            // Optimize the case where the layout itself is not modified. A common reason for
-            // this is if no wrapping actually occurs above because no LayoutModifiers are
-            // present in the modifier chain.
-            if (oldOuterCoordinator != innerCoordinator ||
-                outerCoordinator != innerCoordinator
-            ) {
-                invalidateMeasurements()
-            }
         }
 
+    internal fun invalidateParentData() {
+        layoutDelegate.invalidateParentData()
+    }
+
     /**
      * Coordinates of just the contents of the [LayoutNode], after being affected by all modifiers.
      */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index a35e1e0..2c4d5cb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -223,6 +223,7 @@
         private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
         private var lastZIndex: Float = 0f
 
+        private var parentDataDirty: Boolean = true
         override var parentData: Any? = null
             private set
         override val isPlaced: Boolean
@@ -510,7 +511,12 @@
             }
         }
 
+        fun invalidateParentData() {
+            parentDataDirty = true
+        }
         fun updateParentData(): Boolean {
+            if (!parentDataDirty) return false
+            parentDataDirty = false
             val changed = parentData != outerCoordinator.parentData
             parentData = outerCoordinator.parentData
             return changed
@@ -847,6 +853,7 @@
             }
         }
 
+        private var parentDataDirty: Boolean = true
         override var parentData: Any? = measurePassDelegate.parentData
             private set
 
@@ -988,7 +995,13 @@
             }
         }
 
+        fun invalidateParentData() {
+            parentDataDirty = true
+        }
+
         fun updateParentData(): Boolean {
+            if (!parentDataDirty) return false
+            parentDataDirty = false
             val changed = parentData != outerCoordinator.lookaheadDelegate!!.parentData
             parentData = outerCoordinator.lookaheadDelegate!!.parentData
             return changed
@@ -1125,6 +1138,11 @@
         }
     }
 
+    fun invalidateParentData() {
+        measurePassDelegate.invalidateParentData()
+        lookaheadPassDelegate?.invalidateParentData()
+    }
+
     fun resetAlignmentLines() {
         measurePassDelegate.alignmentLines.reset()
         lookaheadPassDelegate?.alignmentLines?.reset()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
index 8266a53..6116c55 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt
@@ -46,6 +46,7 @@
      * [ModifierNodeElement] while still having a reasonable equals implementation.
      */
     internal val params: Any? = null,
+    internal val autoInvalidate: Boolean = true,
     /**
      * This lambda will construct a debug-only set of information for use with tooling.
      *
@@ -82,11 +83,11 @@
  * A helpful API for constructing a [ModifierNodeElement] corresponding to a particular
  * [Modifier.Node] implementation.
  *
- * @param params An object used to determine whether or not the created node should be updated or not.
+ * @param key An object used to determine whether or not the created node should be updated or not.
  * @param create The initial creation of the node. This will be called the first time the modifier
  *  is applied to the Layout and it should construct the correspoding [Modifier.Node] instance,
  *  referencing any captured inputs necessary.
- * @param update Called when a modifier is applied to a Layout whose [params] have changed from the
+ * @param update Called when a modifier is applied to a Layout whose [key] have changed from the
  *  previous application. This lambda will have the current node instance passed in as a parameter,
  *  and it is expected that the node will be brought up to date.
  * @param definitions This lambda will construct a debug-only set of information for use with
@@ -106,11 +107,11 @@
 @Suppress("MissingNullability", "ModifierFactoryExtensionFunction")
 @ExperimentalComposeUiApi
 inline fun <reified T : Modifier.Node> modifierElementOf(
-    params: Any?,
+    key: Any?,
     crossinline create: () -> T,
     crossinline update: (T) -> Unit,
     crossinline definitions: InspectorInfo.() -> Unit
-): Modifier = object : ModifierNodeElement<T>(params, debugInspectorInfo(definitions)) {
+): Modifier = object : ModifierNodeElement<T>(key, true, debugInspectorInfo(definitions)) {
     override fun create(): T = create()
     override fun update(node: T): T = node.also(update)
 }
@@ -137,7 +138,7 @@
 inline fun <reified T : Modifier.Node> modifierElementOf(
     crossinline create: () -> T,
     crossinline definitions: InspectorInfo.() -> Unit
-): Modifier = object : ModifierNodeElement<T>(null, debugInspectorInfo(definitions)) {
+): Modifier = object : ModifierNodeElement<T>(null, true, debugInspectorInfo(definitions)) {
     override fun create(): T = create()
     override fun update(node: T): T = node
 }
\ No newline at end of file
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 aca43ad..9d0bed1 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
@@ -27,6 +27,8 @@
 
 private val SentinelHead = object : Modifier.Node() {
     override fun toString() = "<Head>"
+}.apply {
+    aggregateChildKindSet = 0.inv()
 }
 
 internal class NodeChain(val layoutNode: LayoutNode) {
@@ -196,7 +198,7 @@
             syncCoordinators()
         }
         if (attachNeeded && layoutNode.isAttached) {
-            attach()
+            attach(performInvalidations = true)
         }
     }
 
@@ -228,9 +230,12 @@
         outerCoordinator = coordinator
     }
 
-    fun attach() {
+    fun attach(performInvalidations: Boolean) {
         headToTail {
-            if (!it.isAttached) it.attach()
+            if (!it.isAttached) {
+                it.attach()
+                if (performInvalidations) autoInvalidateNode(it)
+            }
         }
     }
 
@@ -423,7 +428,13 @@
     }
 
     private fun disposeAndRemoveNode(node: Modifier.Node): Modifier.Node {
-        if (node.isAttached) node.detach()
+        if (node.isAttached) {
+            // 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)
+            node.detach()
+        }
         return removeNode(node)
     }
 
@@ -498,6 +509,8 @@
         if (prev !is ModifierNodeElement<*> || next !is ModifierNodeElement<*>) {
             check(node is BackwardsCompatNode)
             node.element = next
+            // we always autoInvalidate BackwardsCompatNode
+            autoInvalidateNode(node)
             return node
         }
         val updated = next.updateUnsafe(node)
@@ -509,6 +522,11 @@
             // 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
+            autoInvalidateNode(result)
+        }
         return result
     }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index 2d7d547..e3d85a2 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -240,6 +240,7 @@
         }
         layoutNode.owner?.onLayoutChange(layoutNode)
         measuredSize = IntSize(width, height)
+        graphicsLayerScope.size = measuredSize.toSize()
         visitNodes(Nodes.Draw) {
             it.onMeasureResultChanged()
         }
@@ -255,11 +256,13 @@
         get() {
             var data: Any? = null
             val thisNode = tail
-            with(layoutNode.density) {
-                layoutNode.nodes.tailToHead {
-                    if (it === thisNode) return@tailToHead
-                    if (it.isKind(Nodes.ParentData) && it is ParentDataModifierNode) {
-                        data = with(it) { modifyParentData(data) }
+            if (layoutNode.nodes.has(Nodes.ParentData)) {
+                with(layoutNode.density) {
+                    layoutNode.nodes.tailToHead {
+                        if (it === thisNode) return@tailToHead
+                        if (it.isKind(Nodes.ParentData) && it is ParentDataModifierNode) {
+                            data = with(it) { modifyParentData(data) }
+                        }
                     }
                 }
             }
@@ -312,15 +315,6 @@
     }
 
     /**
-     * An initialization function that is called when the [NodeCoordinator] is initially created,
-     * and also called when the [NodeCoordinator] is re-used.
-     */
-    // TODO(lmr): we should try and get rid of this since it isn't always needed!
-    fun onInitialize() {
-        layer?.invalidate()
-    }
-
-    /**
      * Places the modified child.
      */
     /*@CallSuper*/
@@ -446,6 +440,7 @@
             val layerBlock = requireNotNull(layerBlock)
             graphicsLayerScope.reset()
             graphicsLayerScope.graphicsDensity = layoutNode.density
+            graphicsLayerScope.size = size.toSize()
             snapshotObserver.observeReads(this, onCommitAffectingLayerParams) {
                 layerBlock.invoke(graphicsLayerScope)
             }
@@ -469,6 +464,7 @@
                 shape = graphicsLayerScope.shape,
                 clip = graphicsLayerScope.clip,
                 renderEffect = graphicsLayerScope.renderEffect,
+                compositingStrategy = graphicsLayerScope.compositingStrategy,
                 layoutDirection = layoutNode.layoutDirection,
                 density = layoutNode.density
             )
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 ae0ffee..4f8022f 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
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:Suppress("DEPRECATION")
+@file:Suppress("DEPRECATION", "NOTHING_TO_INLINE")
 
 package androidx.compose.ui.node
 
@@ -37,10 +37,10 @@
 
 @JvmInline
 internal value class NodeKind<T>(val mask: Int) {
-    infix fun or(other: NodeKind<*>): Int = mask or other.mask
-    infix fun or(other: Int): Int = mask or other
+    inline infix fun or(other: NodeKind<*>): Int = mask or other.mask
+    inline infix fun or(other: Int): Int = mask or other
 }
-internal infix fun Int.or(other: NodeKind<*>): Int = this or other.mask
+internal inline infix fun Int.or(other: NodeKind<*>): Int = this or other.mask
 
 // For a given NodeCoordinator, the "LayoutAware" nodes that it is concerned with should include
 // its own measureNode if the measureNode happens to implement LayoutAware. If the measureNode
@@ -56,16 +56,26 @@
 
 @OptIn(ExperimentalComposeUiApi::class)
 internal object Nodes {
-    val Any = NodeKind<Modifier.Node>(0b1)
-    val Layout = NodeKind<LayoutModifierNode>(0b1 shl 1)
-    val Draw = NodeKind<DrawModifierNode>(0b1 shl 2)
-    val Semantics = NodeKind<SemanticsModifierNode>(0b1 shl 3)
-    val PointerInput = NodeKind<PointerInputModifierNode>(0b1 shl 4)
-    val Locals = NodeKind<ModifierLocalNode>(0b1 shl 5)
-    val ParentData = NodeKind<ParentDataModifierNode>(0b1 shl 6)
-    val LayoutAware = NodeKind<LayoutAwareModifierNode>(0b1 shl 7)
-    val GlobalPositionAware = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8)
-    val IntermediateMeasure = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 9)
+    @JvmStatic
+    inline val Any get() = NodeKind<Modifier.Node>(0b1 shl 0)
+    @JvmStatic
+    inline val Layout get() = NodeKind<LayoutModifierNode>(0b1 shl 1)
+    @JvmStatic
+    inline val Draw get() = NodeKind<DrawModifierNode>(0b1 shl 2)
+    @JvmStatic
+    inline val Semantics get() = NodeKind<SemanticsModifierNode>(0b1 shl 3)
+    @JvmStatic
+    inline val PointerInput get() = NodeKind<PointerInputModifierNode>(0b1 shl 4)
+    @JvmStatic
+    inline val Locals get() = NodeKind<ModifierLocalNode>(0b1 shl 5)
+    @JvmStatic
+    inline val ParentData get() = NodeKind<ParentDataModifierNode>(0b1 shl 6)
+    @JvmStatic
+    inline val LayoutAware get() = NodeKind<LayoutAwareModifierNode>(0b1 shl 7)
+    @JvmStatic
+    inline val GlobalPositionAware get() = NodeKind<GlobalPositionAwareModifierNode>(0b1 shl 8)
+    @JvmStatic
+    inline val IntermediateMeasure get() = NodeKind<IntermediateLayoutModifierNode>(0b1 shl 9)
     // ...
 }
 
@@ -75,6 +85,7 @@
     if (element is LayoutModifier) {
         mask = mask or Nodes.Layout
     }
+    @OptIn(ExperimentalComposeUiApi::class)
     if (element is IntermediateLayoutModifier) {
         mask = mask or Nodes.IntermediateMeasure
     }
@@ -144,3 +155,22 @@
     }
     return mask
 }
+
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun autoInvalidateNode(node: Modifier.Node) {
+    if (node.isKind(Nodes.Layout) && node is LayoutModifierNode) {
+        node.invalidateMeasurements()
+    }
+    if (node.isKind(Nodes.GlobalPositionAware) && node is GlobalPositionAwareModifierNode) {
+        node.requireLayoutNode().invalidateMeasurements()
+    }
+    if (node.isKind(Nodes.Draw) && node is DrawModifierNode) {
+        node.invalidateDraw()
+    }
+    if (node.isKind(Nodes.Semantics) && node is SemanticsModifierNode) {
+        node.invalidateSemantics()
+    }
+    if (node.isKind(Nodes.ParentData) && node is ParentDataModifierNode) {
+        node.invalidateParentData()
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index a12e478..79a754b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -20,6 +20,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.Shape
@@ -54,6 +55,7 @@
         renderEffect: RenderEffect?,
         ambientShadowColor: Color,
         spotShadowColor: Color,
+        compositingStrategy: CompositingStrategy,
         layoutDirection: LayoutDirection,
         density: Density
     )
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
index 21f23e8..22eec13 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ParentDataModifierNode.kt
@@ -37,4 +37,11 @@
      * Provides a parentData, given the [parentData] already provided through the modifier's chain.
      */
     fun Density.modifyParentData(parentData: Any?): Any?
-}
\ No newline at end of file
+}
+
+/**
+ * This invalidates the current node's parent data, and ensures that layouts that utilize it will be
+ * scheduled to relayout for the next frame.
+ */
+@ExperimentalComposeUiApi
+fun ParentDataModifierNode.invalidateParentData() = requireLayoutNode().invalidateParentData()
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt
index fd7fd12..af4ba78 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkiaLayerTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.DefaultShadowColor
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.RenderEffect
@@ -391,12 +392,14 @@
         transformOrigin: TransformOrigin = TransformOrigin.Center,
         shape: Shape = RectangleShape,
         clip: Boolean = false,
-        renderEffect: RenderEffect? = null
+        renderEffect: RenderEffect? = null,
+        compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
     ) {
         updateLayerProperties(
             scaleX, scaleY, alpha, translationX, translationY, shadowElevation, rotationX,
             rotationY, rotationZ, cameraDistance, transformOrigin, shape, clip, renderEffect,
-            ambientShadowColor, spotShadowColor, LayoutDirection.Ltr, Density(1f, 1f)
+            ambientShadowColor, spotShadowColor, compositingStrategy, LayoutDirection.Ltr,
+            Density(1f, 1f)
         )
     }
 }
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
index 49c7b5d..3b07d10 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
@@ -24,6 +24,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.ClipOp
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.DefaultShadowColor
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Outline
@@ -34,6 +35,7 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.asComposeCanvas
+import androidx.compose.ui.graphics.SkiaBackedCanvas
 import androidx.compose.ui.graphics.asSkiaPath
 import androidx.compose.ui.graphics.nativeCanvas
 import androidx.compose.ui.graphics.toArgb
@@ -82,6 +84,7 @@
     private var shadowElevation: Float = 0f
     private var ambientShadowColor: Color = DefaultShadowColor
     private var spotShadowColor: Color = DefaultShadowColor
+    private var compositingStrategy: CompositingStrategy = CompositingStrategy.Auto
 
     override fun destroy() {
         picture?.close()
@@ -160,6 +163,7 @@
         renderEffect: RenderEffect?,
         ambientShadowColor: Color,
         spotShadowColor: Color,
+        compositingStrategy: CompositingStrategy,
         layoutDirection: LayoutDirection,
         density: Density
     ) {
@@ -178,6 +182,7 @@
         this.renderEffect = renderEffect
         this.ambientShadowColor = ambientShadowColor
         this.spotShadowColor = spotShadowColor
+        this.compositingStrategy = compositingStrategy
         outlineCache.shape = shape
         outlineCache.layoutDirection = layoutDirection
         outlineCache.density = density
@@ -256,7 +261,11 @@
             }
 
             val currentRenderEffect = renderEffect
-            if (alpha < 1 || currentRenderEffect != null) {
+            val requiresLayer =
+                (alpha < 1 && compositingStrategy != CompositingStrategy.ModulateAlpha) ||
+                currentRenderEffect != null ||
+                compositingStrategy == CompositingStrategy.Always
+            if (requiresLayer) {
                 canvas.saveLayer(
                     bounds,
                     Paint().apply {
@@ -267,6 +276,12 @@
             } else {
                 canvas.save()
             }
+            val skiaCanvas = canvas as SkiaBackedCanvas
+            if (compositingStrategy == CompositingStrategy.ModulateAlpha) {
+                skiaCanvas.alphaMultiplier = alpha
+            } else {
+                skiaCanvas.alphaMultiplier = 1.0f
+            }
 
             drawBlock(canvas)
             canvas.restore()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
index 370030a..3aa9cbc 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/graphics/GraphicsLayerScopeTest.kt
@@ -57,6 +57,7 @@
             ) = Outline.Rectangle(size.toRect())
         }
         scope.clip = true
+        scope.size = Size(100f, 200f)
         scope.reset()
         scope.assertCorrectDefaultValuesAreCorrect()
     }
@@ -71,6 +72,16 @@
         }
     }
 
+    @Test
+    fun testGraphicsLayerSize() {
+        val scope = GraphicsLayerScope() as ReusableGraphicsLayerScope
+        scope.size = Size(2560f, 1400f)
+        with(scope) {
+            assertEquals(2560f, size.width)
+            assertEquals(1400f, size.height)
+        }
+    }
+
     fun GraphicsLayerScope.assertCorrectDefaultValuesAreCorrect() {
         assertThat(scaleX).isEqualTo(1f)
         assertThat(scaleY).isEqualTo(1f)
@@ -85,5 +96,6 @@
         assertThat(transformOrigin).isEqualTo(TransformOrigin.Center)
         assertThat(shape).isEqualTo(RectangleShape)
         assertThat(clip).isEqualTo(false)
+        assertThat(size).isEqualTo(Size.Zero)
     }
 }
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index f65314a..0d2d1c1 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.CompositingStrategy
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.Shape
@@ -2609,6 +2610,7 @@
                 renderEffect: RenderEffect?,
                 ambientShadowColor: Color,
                 spotShadowColor: Color,
+                compositingStrategy: CompositingStrategy,
                 layoutDirection: LayoutDirection,
                 density: Density
             ) {
diff --git a/core/core-location-altitude/api/current.txt b/core/core-location-altitude/api/current.txt
index a333f75..e6f50d0 100644
--- a/core/core-location-altitude/api/current.txt
+++ b/core/core-location-altitude/api/current.txt
@@ -1,9 +1 @@
 // Signature format: 4.0
-package androidx.core.location.altitude {
-
-  public final class AltitudeConverterCompat {
-    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
-  }
-
-}
-
diff --git a/core/core-location-altitude/api/public_plus_experimental_current.txt b/core/core-location-altitude/api/public_plus_experimental_current.txt
index a333f75..e6f50d0 100644
--- a/core/core-location-altitude/api/public_plus_experimental_current.txt
+++ b/core/core-location-altitude/api/public_plus_experimental_current.txt
@@ -1,9 +1 @@
 // Signature format: 4.0
-package androidx.core.location.altitude {
-
-  public final class AltitudeConverterCompat {
-    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
-  }
-
-}
-
diff --git a/core/core-location-altitude/api/restricted_current.txt b/core/core-location-altitude/api/restricted_current.txt
index a333f75..e6f50d0 100644
--- a/core/core-location-altitude/api/restricted_current.txt
+++ b/core/core-location-altitude/api/restricted_current.txt
@@ -1,9 +1 @@
 // Signature format: 4.0
-package androidx.core.location.altitude {
-
-  public final class AltitudeConverterCompat {
-    method public static com.google.common.util.concurrent.ListenableFuture<android.location.Location!> addMslAltitudeAsync(android.content.Context, android.location.Location);
-  }
-
-}
-
diff --git a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
index ce18128..37e448c 100644
--- a/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
+++ b/core/core-location-altitude/src/main/java/androidx/core/location/altitude/AltitudeConverterCompat.java
@@ -29,6 +29,8 @@
 /**
  * Converts altitudes reported above the World Geodetic System 1984 (WGS84) reference ellipsoid
  * into ones above Mean Sea Level.
+ *
+ * @hide
  */
 public final class AltitudeConverterCompat {
 
@@ -53,6 +55,8 @@
      * </ul>
      *
      * <p>NOTE: Currently throws {@link UnsupportedOperationException} for a valid {@code location}.
+     *
+     * @hide
      */
     @NonNull
     public static ListenableFuture<Location> addMslAltitudeAsync(
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
index aa0aa6a..c8d6133 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerJavaTest.java
@@ -79,4 +79,14 @@
                         })
         );
     }
+
+    @Test
+    public void testClearCredentialSessionAsync() {
+        assertThrows(UnsupportedOperationException.class,
+                () -> mCredentialManager.clearCredentialSessionAsync(
+                        null,
+                        Runnable::run,
+                        result -> {})
+        );
+    }
 }
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
index b573302..4b9dcea 100644
--- a/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/CredentialManagerTest.kt
@@ -57,6 +57,13 @@
     }
 
     @Test
+    fun testClearCredentialSession() = runBlocking<Unit> {
+        assertThrows<UnsupportedOperationException> {
+            credentialManager.clearCredentialSession()
+        }
+    }
+
+    @Test
     fun testCreateCredentialAsyc() {
         assertThrows<UnsupportedOperationException> {
             credentialManager.executeCreateCredentialAsync(
@@ -87,4 +94,17 @@
             )
         }
     }
+
+    @Test
+    fun testClearCredentialSessionAsync() {
+        assertThrows<UnsupportedOperationException> {
+            credentialManager.clearCredentialSessionAsync(
+                cancellationSignal = null,
+                executor = Runnable::run,
+                callback = object : CredentialManagerCallback<Void> {
+                    override fun onResult(result: Void) {}
+                }
+            )
+        }
+    }
 }
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
index 17a2831..8023aa4 100644
--- a/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
+++ b/credentials/credentials/src/main/java/androidx/credentials/CredentialManager.kt
@@ -125,6 +125,38 @@
     }
 
     /**
+     * Clears the current user credential session from all credential providers.
+     *
+     * Usually invoked after your user signs out of your app so that they will not be
+     * automatically signed in the next time.
+     *
+     * @hide
+     */
+    suspend fun clearCredentialSession(): Unit = suspendCancellableCoroutine { continuation ->
+        // Any Android API that supports cancellation should be configured to propagate
+        // coroutine cancellation as follows:
+        val canceller = CancellationSignal()
+        continuation.invokeOnCancellation { canceller.cancel() }
+
+        val callback = object : CredentialManagerCallback<Void> {
+            override fun onResult(result: Void) {
+                continuation.resume(Unit)
+            }
+
+            override fun onError(e: CredentialManagerException) {
+                continuation.resumeWithException(e)
+            }
+        }
+
+        clearCredentialSessionAsync(
+            canceller,
+            // Use a direct executor to avoid extra dispatch. Resuming the continuation will
+            // handle getting to the right thread or pool via the ContinuationInterceptor.
+            Runnable::run,
+            callback)
+    }
+
+    /**
      * Java API for requesting a credential from the user.
      *
      * The execution potentially launches framework UI flows for a user to view available
@@ -176,4 +208,25 @@
     ) {
         throw UnsupportedOperationException("Unimplemented")
     }
+
+    /**
+     * Clears the current user credential session from all credential providers.
+     *
+     * Usually invoked after your user signs out of your app so that they will not be
+     * automatically signed in the next time.
+     *
+     * @param cancellationSignal an optional signal that allows for cancelling this call
+     * @param executor the callback will take place on this executor
+     * @param callback the callback invoked when the request succeeds or fails
+     * @throws UnsupportedOperationException Since the api is unimplemented
+     *
+     * @hide
+     */
+    fun clearCredentialSessionAsync(
+        cancellationSignal: CancellationSignal?,
+        executor: Executor,
+        callback: CredentialManagerCallback<Void>,
+    ) {
+        throw UnsupportedOperationException("Unimplemented")
+    }
 }
\ No newline at end of file
diff --git a/datastore/datastore-core-okio/build.gradle b/datastore/datastore-core-okio/build.gradle
index 3f9c98a..44bdc0f 100644
--- a/datastore/datastore-core-okio/build.gradle
+++ b/datastore/datastore-core-okio/build.gradle
@@ -100,7 +100,7 @@
 
 androidx {
     name = "Android DataStore Core Okio"
-    type = LibraryType.KMP_LIBRARY
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.DATASTORE
     inceptionYear = "2020"
     description = "Android DataStore Core Okio- contains APIs to use datastore-core in multiplatform via okio"
diff --git a/datastore/datastore-core/api/current.txt b/datastore/datastore-core/api/current.txt
index 515b0a9..3fc872f 100644
--- a/datastore/datastore-core/api/current.txt
+++ b/datastore/datastore-core/api/current.txt
@@ -34,6 +34,10 @@
     field public static final androidx.datastore.core.DataStoreFactory INSTANCE;
   }
 
+  public final class MultiProcessDataStoreFactory {
+    field public static final androidx.datastore.core.MultiProcessDataStoreFactory INSTANCE;
+  }
+
   public interface ReadScope<T> extends androidx.datastore.core.Closeable {
     method public suspend Object? readData(kotlin.coroutines.Continuation<? super T>);
   }
diff --git a/datastore/datastore-core/api/public_plus_experimental_current.txt b/datastore/datastore-core/api/public_plus_experimental_current.txt
index 515b0a9..4821c84 100644
--- a/datastore/datastore-core/api/public_plus_experimental_current.txt
+++ b/datastore/datastore-core/api/public_plus_experimental_current.txt
@@ -34,6 +34,21 @@
     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 {
+  }
+
+  public final class MultiProcessDataStoreFactory {
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Storage<T> storage, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, optional kotlinx.coroutines.CoroutineScope scope, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Storage<T> storage, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Storage<T> storage, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Storage<T> storage, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, optional kotlinx.coroutines.CoroutineScope scope, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    method @androidx.datastore.core.ExperimentalMultiProcessDataStore public <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
+    field public static final androidx.datastore.core.MultiProcessDataStoreFactory INSTANCE;
+  }
+
   public interface ReadScope<T> extends androidx.datastore.core.Closeable {
     method public suspend Object? readData(kotlin.coroutines.Continuation<? super T>);
   }
diff --git a/datastore/datastore-multiprocess/api/res-current.txt b/datastore/datastore-core/api/res-1.0.0-beta01.txt
similarity index 100%
rename from datastore/datastore-multiprocess/api/res-current.txt
rename to datastore/datastore-core/api/res-1.0.0-beta01.txt
diff --git a/datastore/datastore-multiprocess/api/res-current.txt b/datastore/datastore-core/api/res-1.0.0-beta02.txt
similarity index 100%
copy from datastore/datastore-multiprocess/api/res-current.txt
copy to datastore/datastore-core/api/res-1.0.0-beta02.txt
diff --git a/datastore/datastore-multiprocess/api/res-current.txt b/datastore/datastore-core/api/res-1.0.0-beta03.txt
similarity index 100%
copy from datastore/datastore-multiprocess/api/res-current.txt
copy to datastore/datastore-core/api/res-1.0.0-beta03.txt
diff --git a/datastore/datastore-core/api/restricted_current.txt b/datastore/datastore-core/api/restricted_current.txt
index 515b0a9..3fc872f 100644
--- a/datastore/datastore-core/api/restricted_current.txt
+++ b/datastore/datastore-core/api/restricted_current.txt
@@ -34,6 +34,10 @@
     field public static final androidx.datastore.core.DataStoreFactory INSTANCE;
   }
 
+  public final class MultiProcessDataStoreFactory {
+    field public static final androidx.datastore.core.MultiProcessDataStoreFactory INSTANCE;
+  }
+
   public interface ReadScope<T> extends androidx.datastore.core.Closeable {
     method public suspend Object? readData(kotlin.coroutines.Continuation<? super T>);
   }
diff --git a/datastore/datastore-core/build.gradle b/datastore/datastore-core/build.gradle
index b5194bd..ea3988a 100644
--- a/datastore/datastore-core/build.gradle
+++ b/datastore/datastore-core/build.gradle
@@ -22,17 +22,46 @@
 
 plugins {
     id("AndroidXPlugin")
+    id("com.android.library")
+    id("com.google.protobuf")
 }
 
 def enableNative = KmpPlatformsKt.enableNative(project)
 
-androidXMultiplatform {
-    jvm() {
-        withJava()
+android {
+    externalNativeBuild {
+        cmake {
+            path "src/androidMain/cpp/CMakeLists.txt"
+            version libs.versions.cmake.get()
+        }
     }
+    namespace "androidx.datastore.core"
+}
+
+protobuf {
+    protoc {
+        artifact = libs.protobufCompiler.get()
+    }
+    // Generates the java proto-lite code for the protos in this project. See
+    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
+    // for more information.
+    generateProtoTasks {
+        all().each { task ->
+            task.builtins {
+                java {
+                    option "lite"
+                }
+            }
+        }
+    }
+}
+
+androidXMultiplatform {
+    jvm()
     mac()
     linux()
     ios()
+    android()
 
     sourceSets {
         all {
@@ -50,6 +79,9 @@
                 api("androidx.annotation:annotation:1.3.0")
             }
         }
+        androidMain {
+            dependsOn(jvmMain)
+        }
 
         commonTest {
             dependencies {
@@ -72,6 +104,17 @@
             }
         }
 
+        androidTest {
+            dependsOn(jvmTest)
+            dependencies {
+                implementation(libs.protobufLite)
+                implementation(libs.truth)
+                implementation(project(":internal-testutils-truth"))
+                implementation(libs.testRunner)
+                implementation(libs.testCore)
+            }
+        }
+
         if (enableNative) {
             nativeMain {
                 dependsOn(commonMain)
@@ -104,7 +147,7 @@
 
 androidx {
     name = "Android DataStore Core"
-    type = LibraryType.KMP_LIBRARY
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.DATASTORE
     inceptionYear = "2020"
     description = "Android DataStore Core - contains the underlying store used by each serialization method"
diff --git a/datastore/datastore-multiprocess/src/androidTest/AndroidManifest.xml b/datastore/datastore-core/src/androidAndroidTest/AndroidManifest.xml
similarity index 67%
rename from datastore/datastore-multiprocess/src/androidTest/AndroidManifest.xml
rename to datastore/datastore-core/src/androidAndroidTest/AndroidManifest.xml
index 8d722f2..1fd5182 100644
--- a/datastore/datastore-multiprocess/src/androidTest/AndroidManifest.xml
+++ b/datastore/datastore-core/src/androidAndroidTest/AndroidManifest.xml
@@ -18,42 +18,42 @@
 
     <application>
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$SimpleUpdateService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$SimpleUpdateService"
             android:enabled="true"
             android:exported="false"
             android:process=":SimpleUpdateService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$ConcurrentReadUpdateWriterService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$ConcurrentReadUpdateWriterService"
             android:enabled="true"
             android:exported="false"
             android:process=":ConcurrentReadUpdateWriterService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$ConcurrentReadUpdateReaderService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$ConcurrentReadUpdateReaderService"
             android:enabled="true"
             android:exported="false"
             android:process=":ConcurrentReadUpdateReaderService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$InterleavedUpdateDataService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$InterleavedUpdateDataService"
             android:enabled="true"
             android:exported="false"
             android:process=":InterleavedUpdateDataService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$InterleavedUpdateDataWithReadService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$InterleavedUpdateDataWithReadService"
             android:enabled="true"
             android:exported="false"
             android:process=":InterleavedUpdateDataWithReadService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$FailedUpdateDataService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$FailedUpdateDataService"
             android:enabled="true"
             android:exported="false"
             android:process=":FailedUpdateDataService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$CancelledUpdateDataService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$CancelledUpdateDataService"
             android:enabled="true"
             android:exported="false"
             android:process=":CancelledUpdateDataService" />
         <service
-            android:name="androidx.datastore.multiprocess.MultiProcessDataStoreMultiProcessTest$InterleavedHandlerUpdateDataService"
+            android:name="androidx.datastore.core.MultiProcessDataStoreMultiProcessTest$InterleavedHandlerUpdateDataService"
             android:enabled="true"
             android:exported="false"
             android:process=":InterleavedHandlerUpdateDataService" />
diff --git a/datastore/datastore-multiprocess/src/main/cpp/CMakeLists.txt b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
similarity index 96%
rename from datastore/datastore-multiprocess/src/main/cpp/CMakeLists.txt
rename to datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
index cee8f56..f7cba81 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/CMakeLists.txt
+++ b/datastore/datastore-core/src/androidMain/cpp/CMakeLists.txt
@@ -22,6 +22,6 @@
 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror")
 
 add_library(shared_counter STATIC shared_counter.cc)
-add_library(datastore_shared_counter SHARED jni/androidx_datastore_multiprocess_SharedCounter.cc)
+add_library(datastore_shared_counter SHARED jni/androidx_datastore_core_SharedCounter.cc)
 
 target_link_libraries(datastore_shared_counter shared_counter)
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc b/datastore/datastore-core/src/androidMain/cpp/jni/androidx_datastore_core_SharedCounter.cc
similarity index 85%
rename from datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
rename to datastore/datastore-core/src/androidMain/cpp/jni/androidx_datastore_core_SharedCounter.cc
index 61cddca..f7ff144 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
+++ b/datastore/datastore-core/src/androidMain/cpp/jni/androidx_datastore_core_SharedCounter.cc
@@ -35,7 +35,7 @@
 extern "C" {
 
 JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeTruncateFile(
+Java_androidx_datastore_core_NativeSharedCounter_nativeTruncateFile(
         JNIEnv *env, jclass clazz, jint fd) {
     if (int errNum = datastore::TruncateFile(fd)) {
         return ThrowIoException(env, strerror(errNum));
@@ -44,7 +44,7 @@
 }
 
 JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+Java_androidx_datastore_core_NativeSharedCounter_nativeCreateSharedCounter(
         JNIEnv *env, jclass clazz, jint fd, jboolean enable_mlock) {
     void* address = nullptr;
     if (int errNum = datastore::CreateSharedCounter(fd, &address, enable_mlock)) {
@@ -54,14 +54,14 @@
 }
 
 JNIEXPORT jint JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeGetCounterValue(
+Java_androidx_datastore_core_NativeSharedCounter_nativeGetCounterValue(
         JNIEnv *env, jclass clazz, jlong address) {
     return static_cast<jint>(
         datastore::GetCounterValue(reinterpret_cast<std::atomic<uint32_t>*>(address)));
 }
 
 JNIEXPORT jint JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeIncrementAndGetCounterValue(
+Java_androidx_datastore_core_NativeSharedCounter_nativeIncrementAndGetCounterValue(
         JNIEnv *env, jclass clazz, jlong address) {
     return static_cast<jint>(
         datastore::IncrementAndGetCounterValue(reinterpret_cast<std::atomic<uint32_t>*>(address)));
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc b/datastore/datastore-core/src/androidMain/cpp/shared_counter.cc
similarity index 100%
rename from datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
rename to datastore/datastore-core/src/androidMain/cpp/shared_counter.cc
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h b/datastore/datastore-core/src/androidMain/cpp/shared_counter.h
similarity index 100%
rename from datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
rename to datastore/datastore-core/src/androidMain/cpp/shared_counter.h
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
similarity index 95%
rename from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
rename to datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
index 07fb357..5546222 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/ExperimentalMultiProcessDataStore.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 @RequiresOptIn(
     level = RequiresOptIn.Level.WARNING,
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
similarity index 83%
rename from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
rename to datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
index 99af5fe..6c1d228 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStore.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStore.kt
@@ -14,21 +14,15 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import android.os.FileObserver
-import androidx.annotation.GuardedBy
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.DataStore
-import androidx.datastore.core.Serializer
-import androidx.datastore.multiprocess.handlers.NoOpCorruptionHandler
+import androidx.datastore.core.handlers.NoOpCorruptionHandler
 import java.io.File
 import java.io.FileInputStream
-import java.io.FileNotFoundException
 import java.io.FileOutputStream
 import java.io.IOException
 import java.nio.channels.FileLock
-import java.util.concurrent.Semaphore
 import kotlin.coroutines.CoroutineContext
 import kotlin.coroutines.coroutineContext
 import kotlinx.coroutines.completeWith
@@ -51,8 +45,7 @@
  * Multi process implementation of DataStore. It is multi-process safe.
  */
 internal class MultiProcessDataStore<T>(
-    private val produceFile: () -> File,
-    private val serializer: Serializer<T>,
+    private val storage: Storage<T>,
     /**
      * The list of initialization tasks to perform. These tasks will be completed before any data
      * is published to the data and before any read-modify-writes execute in updateData.  If
@@ -62,7 +55,8 @@
      */
     initTasksList: List<suspend (api: InitializerApi<T>) -> Unit> = emptyList(),
     private val corruptionHandler: CorruptionHandler<T> = NoOpCorruptionHandler<T>(),
-    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+    private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+    private val produceFile: () -> File
 ) : DataStore<T> {
     override val data: Flow<T> = flow {
         /**
@@ -135,7 +129,6 @@
         return ack.await()
     }
 
-    private val SCRATCH_SUFFIX = ".tmp"
     private val LOCK_SUFFIX = ".lock"
     private val VERSION_SUFFIX = ".version"
     private val BUG_MESSAGE = "This is a bug in DataStore. Please file a bug at: " +
@@ -154,26 +147,15 @@
             versionFile
         }
     }
-    private val threadLockSemaphore = Semaphore(1)
+    private val threadLock = Mutex()
+    private val storageConnection: StorageConnection<T> by lazy {
+        storage.createConnection()
+    }
 
     // file is protected rather than private to avoid requiring synthetic accessor for its usage in
     // the definition of FileObserver
     protected val file: File by lazy {
-        val file = produceFile()
-
-        file.absolutePath.let {
-            synchronized(activeFilesLock) {
-                check(!activeFiles.contains(it)) {
-                    "There are multiple DataStores in the same process active for the same file: " +
-                        "$file. You should either maintain your DataStore as a singleton or " +
-                        "confirm that there is no two DataStore's active on the same file in the " +
-                        "same process(by confirming that the scope is cancelled)."
-                }
-                activeFiles.add(it)
-            }
-        }
-
-        file
+        produceFile()
     }
 
     private val lockFile: File by lazy {
@@ -207,9 +189,7 @@
             // We expect it to always be non-null but we will leave the alternative as a no-op
             // just in case.
 
-            synchronized(activeFilesLock) {
-                activeFiles.remove(file.absolutePath)
-            }
+            storageConnection.close()
         },
         onUndeliveredElement = { msg, ex ->
             msg.ack.completeExceptionally(
@@ -432,21 +412,7 @@
     // Caller is responsible for (try to) getting file lock. It reads from the file directly without
     // checking shared counter version and returns serializer default value if file is not found.
     private suspend fun readDataFromFileOrDefault(): T {
-        try {
-            return FileInputStream(file).use { stream ->
-                serializer.readFrom(stream)
-            }
-        } catch (ex: FileNotFoundException) {
-            if (file.exists()) {
-                // Re-read to prevent throwing from a race condition where the file is created by
-                // another process before `file.exists()` is called. Otherwise file exists but we
-                // can't read it; throw FileNotFoundException because something is wrong.
-                return FileInputStream(file).use { stream ->
-                    serializer.readFrom(stream)
-                }
-            }
-            return serializer.defaultValue
-        }
+        return storageConnection.readData()
     }
 
     private suspend fun transformAndWrite(
@@ -468,39 +434,22 @@
 
     // Write data to disk and return the corresponding version if succeed.
     internal suspend fun writeData(newData: T, updateCache: Boolean = true): Int {
-        file.createParentDirectories()
+        var newVersion: Int = 0
 
-        val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
-        try {
-            FileOutputStream(scratchFile).use { stream ->
-                serializer.writeTo(newData, UncloseableOutputStream(stream))
-                stream.fd.sync()
-                // TODO(b/151635324): fsync the directory, otherwise a badly timed crash could
-                // result in reverting to a previous state.
-            }
-
-            val newVersion = sharedCounter.incrementAndGetValue()
-
-            if (!scratchFile.renameTo(file)) {
-                throw IOException(
-                    "Unable to rename $scratchFile." +
-                        "This likely means that there are multiple instances of DataStore " +
-                        "for this file. Ensure that you are only creating a single instance of " +
-                        "datastore for this file."
-                )
-            }
-
+        // The code in `writeScope` is run synchronously, i.e. the newVersion isn't returned until
+        // the code in `writeScope` completes.
+        storageConnection.writeScope {
+            // TODO(b/256242862): decide the long term solution to increment version after write to
+            // scratch file. We used to increment version after scratch file write for performance
+            // optimization for concurrent reads and failed writes.
+            newVersion = sharedCounter.incrementAndGetValue()
+            writeData(newData)
             if (updateCache) {
                 downstreamFlow.value = Data(newData, newData.hashCode(), newVersion)
             }
-
-            return newVersion
-        } catch (ex: IOException) {
-            if (scratchFile.exists()) {
-                scratchFile.delete() // Swallow failure to delete
-            }
-            throw ex
         }
+
+        return newVersion
     }
 
     private fun fileWithSuffix(suffix: String): File {
@@ -526,9 +475,7 @@
     }
 
     private suspend fun getWriteFileLock(block: suspend () -> T): Data<T> {
-        // TODO(b/239970979): use kotlinx.coroutines.sync.Mutex instead of adhoc ThreadLock with
-        // semaphore to make the best use of coroutine
-        ThreadLock.acquire(threadLockSemaphore).use {
+        threadLock.withLock {
             FileOutputStream(lockFile).use { lockFileStream ->
                 var lock: FileLock? = null
                 try {
@@ -545,8 +492,8 @@
     private suspend fun tryGetReadFileLock(
         block: suspend (Boolean) -> Data<T>
     ): Data<T> {
-        ThreadLock.tryAcquire(threadLockSemaphore).use {
-            if (it.acquired() == false) {
+        return threadLock.withTryLock<Data<T>> {
+            if (it == false) {
                 return block(false)
             }
             FileInputStream(lockFile).use { lockFileStream ->
@@ -578,18 +525,23 @@
         }
     }
 
+    /**
+     * Provide similar functionality of {@link kotlinx.coroutines.sync.Mutex#withLock} but don't
+     * wait for the lock if unavailable, instead it passes a Boolean into the {@link action} lambda
+     * to indicate if it is able to get the lock and run {@link action} immediately.
+     */
+    private inline fun <R> Mutex.withTryLock(owner: Any? = null, action: (Boolean) -> R): R {
+        val locked: Boolean = tryLock(owner)
+        try {
+            return action(locked)
+        } finally {
+            if (locked) {
+                unlock(owner)
+            }
+        }
+    }
+
     internal companion object {
-        /**
-         * Active files should contain the absolute path for which there are currently active
-         * DataStores. A DataStore is active until the scope it was created with has been
-         * cancelled. Files aren't added to this list until the first read/write because the file
-         * path is computed asynchronously.
-         */
-        @GuardedBy("activeFilesLock")
-        internal val activeFiles = mutableSetOf<String>()
-
-        internal val activeFilesLock = Any()
-
         internal val initTaskLock = Mutex()
     }
 }
\ No newline at end of file
diff --git a/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStoreFactory.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStoreFactory.kt
new file mode 100644
index 0000000..66628d9
--- /dev/null
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/MultiProcessDataStoreFactory.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.datastore.core
+
+import androidx.datastore.core.handlers.NoOpCorruptionHandler
+import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
+import java.io.File
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+public object MultiProcessDataStoreFactory {
+    /**
+     * Create an instance of MultiProcessDataStore, which provides cross-process eventual
+     * consistency. Never create more than one instance of DataStore for a given file in the same
+     * process; doing so can break all DataStore functionality. You should consider managing your
+     * DataStore instance for each file as a singleton. If there are multiple DataStores active for
+     * a given file in the same process, DataStore will throw IllegalStateException when reading or
+     * updating data. A DataStore is considered active as long as its scope is active. Having
+     * multiple instances, each for a different file, in the same process is OK.
+     *
+     * T is the type DataStore acts on. The type T must be immutable. Mutating a type used in
+     * DataStore invalidates any guarantees that DataStore provides and will result in
+     * potentially serious, hard-to-catch bugs. We strongly recommend using protocol buffers:
+     * https://developers.google.com/protocol-buffers/docs/javatutorial - which provides
+     * immutability guarantees, a simple API and efficient serialization.
+     *
+     * @param storage Storage to handle file reads and writes used with DataStore. The type T must
+     * be immutable. The storage must operate on the same file as the one passed in
+     * {@link produceFile}.
+     * @param corruptionHandler The [ReplaceFileCorruptionHandler] is invoked if DataStore
+     * encounters a [CorruptionException] when attempting to read data. CorruptionExceptions are
+     * thrown by serializers when data can not be de-serialized.
+     * @param migrations Migrations are run before any access to data can occur. Migrations must
+     * be idempotent.
+     * @param scope The scope in which IO operations and transform functions will execute.
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time in the same process.
+     *
+     * @return a new DataStore instance with the provided configuration
+     */
+    @ExperimentalMultiProcessDataStore
+    @JvmOverloads // Generate constructors for default params for java users.
+    public fun <T> create(
+        storage: Storage<T>,
+        corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
+        migrations: List<DataMigration<T>> = listOf(),
+        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+        produceFile: () -> File
+    ): DataStore<T> = MultiProcessDataStore<T>(
+        storage = storage,
+        produceFile = produceFile,
+        initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
+        corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
+        scope = scope
+    )
+
+    /**
+     * Create an instance of MultiProcessDataStore, which provides cross-process eventual
+     * consistency. Never create more than one instance of DataStore for a given file in the same
+     * process; doing so can break all DataStore functionality. You should consider managing your
+     * DataStore instance for each file as a singleton. If there are multiple DataStores active for
+     * a given file in the same process, DataStore will throw IllegalStateException when reading or
+     * updating data. A DataStore is considered active as long as its scope is active. Having
+     * multiple instances, each for a different file, in the same process is OK.
+     *
+     * T is the type DataStore acts on. The type T must be immutable. Mutating a type used in
+     * DataStore invalidates any guarantees that DataStore provides and will result in
+     * potentially serious, hard-to-catch bugs. We strongly recommend using protocol buffers:
+     * https://developers.google.com/protocol-buffers/docs/javatutorial - which provides
+     * immutability guarantees, a simple API and efficient serialization.
+     *
+     * @param serializer Serializer for the type T used with DataStore. The type T must be immutable.
+     * @param corruptionHandler The {@link androidx.datastore.core.handlers.ReplaceFileCorruptionHandler}
+     * is invoked if DataStore encounters a [CorruptionException] when attempting to read data.
+     * CorruptionExceptions are thrown by serializers when data can not be de-serialized.
+     * @param migrations Migrations are run before any access to data can occur. Migrations must
+     * be idempotent.
+     * @param scope The scope in which IO operations and transform functions will execute.
+     * @param produceFile Function which returns the file that the new DataStore will act on. The
+     * function must return the same path every time. No two instances of DataStore should act on
+     * the same file at the same time in the same process.
+     *
+     * @return a new DataStore instance with the provided configuration
+     */
+    @ExperimentalMultiProcessDataStore
+    @JvmOverloads // Generate constructors for default params for java users.
+    public fun <T> create(
+        serializer: Serializer<T>,
+        corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
+        migrations: List<DataMigration<T>> = listOf(),
+        scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+        produceFile: () -> File
+    ): DataStore<T> = MultiProcessDataStore<T>(
+        storage = FileStorage(serializer, produceFile),
+        initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
+        corruptionHandler = corruptionHandler ?: NoOpCorruptionHandler(),
+        scope = scope,
+        produceFile = produceFile
+    )
+}
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/SharedCounter.kt
similarity index 98%
rename from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
rename to datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/SharedCounter.kt
index 1442640..2365756 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-core/src/androidMain/kotlin/androidx/datastore/core/SharedCounter.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import android.annotation.SuppressLint
 import android.os.ParcelFileDescriptor
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/DirectTestService.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/DirectTestService.kt
similarity index 99%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/DirectTestService.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/DirectTestService.kt
index 733f9eb..a0d2f75 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/DirectTestService.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/DirectTestService.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import android.app.Service
 import android.content.ComponentName
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactoryTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
similarity index 91%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactoryTest.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
index 5dfb797..55952fa 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactoryTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreFactoryTest.kt
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
-import androidx.datastore.core.DataMigration
+import androidx.datastore.TestingSerializerConfig
 import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
 import com.google.common.truth.Truth.assertThat
 import java.io.File
@@ -58,7 +58,7 @@
     @ExperimentalMultiProcessDataStore
     @Test
     fun testNewInstance() = runTest {
-        val store = create(
+        val store = MultiProcessDataStoreFactory.create(
             serializer = TestingSerializer(),
             scope = dataStoreScope
         ) { testFile }
@@ -78,9 +78,9 @@
     fun testCorruptionHandlerInstalled() = runTest {
         val valueToReplace = 123.toByte()
 
-        val store = create(
+        val store = MultiProcessDataStoreFactory.create(
             serializer = TestingSerializer(
-                failReadWithCorruptionException = true
+                TestingSerializerConfig(failReadWithCorruptionException = true)
             ),
             corruptionHandler = ReplaceFileCorruptionHandler<Byte> {
                 valueToReplace
@@ -109,7 +109,7 @@
             override suspend fun cleanUp() {}
         }
 
-        val store = create(
+        val store = MultiProcessDataStoreFactory.create(
             serializer = TestingSerializer(),
             migrations = listOf(migratePlus2, migrateMinus1),
             scope = dataStoreScope
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
similarity index 96%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
index 699976c..0ec2645 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreMultiProcessTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreMultiProcessTest.kt
@@ -14,13 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import android.content.Context
 import android.os.Bundle
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.Serializer
-import androidx.datastore.multiprocess.handlers.NoOpCorruptionHandler
+import androidx.datastore.core.handlers.NoOpCorruptionHandler
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.FlakyTest
 import androidx.testing.TestMessageProto.FooProto
@@ -79,11 +77,12 @@
     scope: TestScope,
     corruptionHandler: CorruptionHandler<FooProto> = NoOpCorruptionHandler<FooProto>()
 ): MultiProcessDataStore<FooProto> {
+    val produceFile = { File(bundle.getString(PATH_BUNDLE_KEY)!!) }
     return MultiProcessDataStore<FooProto>(
-        { File(bundle.getString(PATH_BUNDLE_KEY)!!) },
-        PROTO_SERIALIZER,
+        storage = FileStorage(PROTO_SERIALIZER, produceFile),
         scope = scope,
-        corruptionHandler = corruptionHandler
+        corruptionHandler = corruptionHandler,
+        produceFile = produceFile
     )
 }
 
@@ -115,10 +114,11 @@
         bundle: Bundle,
         scope: TestScope
     ): MultiProcessDataStore<FooProto> {
+        val produceFile = { File(bundle.getString(PATH_BUNDLE_KEY)!!) }
         return MultiProcessDataStore<FooProto>(
-            { File(bundle.getString(PATH_BUNDLE_KEY)!!) },
-            protoSerializer,
-            scope = scope
+            storage = FileStorage(protoSerializer, produceFile),
+            scope = scope,
+            produceFile = produceFile
         )
     }
 
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
similarity index 94%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
index f8b2f2e..f735ace 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/MultiProcessDataStoreTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/MultiProcessDataStoreTest.kt
@@ -14,13 +14,11 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import android.os.StrictMode
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.DataStore
-import androidx.datastore.core.Serializer
-import androidx.datastore.multiprocess.handlers.NoOpCorruptionHandler
+import androidx.datastore.TestingSerializerConfig
+import androidx.datastore.core.handlers.NoOpCorruptionHandler
 import androidx.test.filters.FlakyTest
 import androidx.test.filters.LargeTest
 import androidx.testutils.assertThrows
@@ -80,6 +78,7 @@
     val tempFolder = TemporaryFolder()
 
     private lateinit var store: DataStore<Byte>
+    private lateinit var serializerConfig: TestingSerializerConfig
     private lateinit var testingSerializer: TestingSerializer
     private lateinit var testFile: File
     private lateinit var dataStoreScope: TestScope
@@ -92,22 +91,22 @@
         corruptionHandler: CorruptionHandler<Byte> = NoOpCorruptionHandler<Byte>()
     ): DataStore<Byte> {
         return MultiProcessDataStore(
-            { file },
-            serializer = serializer,
+            storage = FileStorage(serializer) { file },
             scope = scope,
             initTasksList = initTasksList,
             corruptionHandler = corruptionHandler
-        )
+        ) { file }
     }
 
     @Before
     fun setUp() {
-        testingSerializer = TestingSerializer()
+        serializerConfig = TestingSerializerConfig()
+        testingSerializer = TestingSerializer(serializerConfig)
         testFile = tempFolder.newFile()
         dataStoreScope = TestScope(UnconfinedTestDispatcher() + Job())
         store =
-            MultiProcessDataStore<Byte>(
-                { testFile },
+            newDataStore(
+                testFile,
                 testingSerializer,
                 scope = dataStoreScope
             )
@@ -217,7 +216,7 @@
         coroutineScope {
             val store = newDataStore(file, scope = this)
             store.updateData { 1 }
-            testingSerializer.failingWrite = true
+            serializerConfig.failingWrite = true
             assertThrows<IOException> { store.updateData { 2 } }
         }
 
@@ -277,10 +276,10 @@
         }
 
         val newStore = MultiProcessDataStore(
-            fileProducer,
-            serializer = testingSerializer,
+            storage = FileStorage(testingSerializer, fileProducer),
             scope = dataStoreScope,
-            initTasksList = listOf()
+            initTasksList = listOf(),
+            produceFile = fileProducer
         )
 
         assertThrows<IOException> { newStore.data.first() }.hasMessageThat().isEqualTo(
@@ -311,11 +310,11 @@
 
     @Test
     fun testWriteAfterTransientBadRead() = runTest {
-        testingSerializer.failingRead = true
+        serializerConfig.failingRead = true
 
         assertThrows<IOException> { store.data.first() }
 
-        testingSerializer.failingRead = false
+        serializerConfig.failingRead = false
 
         store.updateData { 1 }
         assertThat(store.data.first()).isEqualTo(1)
@@ -323,7 +322,7 @@
 
     @Test
     fun testWriteWithBadReadFails() = runTest {
-        testingSerializer.failingRead = true
+        serializerConfig.failingRead = true
 
         assertThrows<IOException> { store.updateData { 1 } }
     }
@@ -402,10 +401,10 @@
     fun testInitTaskFailsFirstTimeDueToReadFail() = runTest {
         store = newDataStore(initTasksList = listOf { api -> api.updateData { 1 } })
 
-        testingSerializer.failingRead = true
+        serializerConfig.failingRead = true
         assertThrows<IOException> { store.updateData { 2 } }
 
-        testingSerializer.failingRead = false
+        serializerConfig.failingRead = false
         store.updateData { it.inc().inc() }
 
         assertThat(store.data.first()).isEqualTo(3)
@@ -575,10 +574,10 @@
 
         val store = newDataStore(initTasksList = listOf(initializer))
 
-        testingSerializer.failingWrite = true
+        serializerConfig.failingWrite = true
         assertThrows<IOException> { store.data.first() }
 
-        testingSerializer.failingWrite = false
+        serializerConfig.failingWrite = false
         assertThat(store.data.first()).isEqualTo(123)
     }
 
@@ -719,7 +718,7 @@
 
         coroutineScope {
             val testingHandler = TestingCorruptionHandler()
-            testingSerializer.failingRead = true
+            serializerConfig.failingRead = true
             val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
 
             assertThrows<IOException> { newStore.updateData { 2 } }
@@ -738,7 +737,7 @@
 
         coroutineScope {
             val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
-            testingSerializer.failReadWithCorruptionException = true
+            serializerConfig.failReadWithCorruptionException = true
             val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
 
             assertThrows<IOException> { newStore.data.first() }.hasMessageThat().contains(
@@ -758,7 +757,7 @@
 
         coroutineScope {
             val testingHandler: TestingCorruptionHandler = TestingCorruptionHandler()
-            testingSerializer.failReadWithCorruptionException = true
+            serializerConfig.failReadWithCorruptionException = true
             val newStore = newDataStore(corruptionHandler = testingHandler, file = testFile)
 
             assertThrows<IOException> { newStore.updateData { 1 } }.hasMessageThat().contains(
@@ -778,7 +777,7 @@
         coroutineScope {
             val testingHandler: TestingCorruptionHandler =
                 TestingCorruptionHandler(replaceWith = 10)
-            testingSerializer.failReadWithCorruptionException = true
+            serializerConfig.failReadWithCorruptionException = true
             val newStore = newDataStore(
                 corruptionHandler = testingHandler, file = testFile,
                 scope = this
@@ -792,10 +791,9 @@
     fun testMutatingDataStoreFails() = runTest {
 
         val dataStore = MultiProcessDataStore(
-            { testFile },
-            ByteWrapper.ByteWrapperSerializer(),
+            storage = FileStorage(ByteWrapper.ByteWrapperSerializer()) { testFile },
             scope = dataStoreScope,
-        )
+        ) { testFile }
 
         assertThrows<IllegalStateException> {
             dataStore.updateData { input: ByteWrapper ->
@@ -809,10 +807,11 @@
     @Test
     fun testDefaultValueUsedWhenNoDataOnDisk() = runTest {
         val dataStore = MultiProcessDataStore(
-            { testFile },
-            TestingSerializer(defaultValue = 99),
+            storage = FileStorage(TestingSerializer(TestingSerializerConfig(defaultValue = 99))) {
+                testFile
+            },
             scope = dataStoreScope
-        )
+        ) { testFile }
 
         assertThat(testFile.delete()).isTrue()
 
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/ProtoSerializer.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoSerializer.kt
similarity index 92%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/ProtoSerializer.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoSerializer.kt
index 3a6f92a..4df5bcf 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/ProtoSerializer.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/ProtoSerializer.kt
@@ -14,10 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.Serializer
 import com.google.protobuf.ExtensionRegistryLite
 import com.google.protobuf.InvalidProtocolBufferException
 import com.google.protobuf.MessageLite
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/SharedCounterTest.kt
similarity index 98%
rename from datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
rename to datastore/datastore-core/src/androidTest/java/androidx/datastore/core/SharedCounterTest.kt
index 707ae1b..6e338e2 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
+++ b/datastore/datastore-core/src/androidTest/java/androidx/datastore/core/SharedCounterTest.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.datastore.core
 
 import androidx.test.filters.MediumTest
 import androidx.testutils.assertThrows
diff --git a/datastore/datastore-multiprocess/src/androidTest/proto/test.proto b/datastore/datastore-core/src/androidTest/proto/test.proto
similarity index 100%
rename from datastore/datastore-multiprocess/src/androidTest/proto/test.proto
rename to datastore/datastore-core/src/androidTest/proto/test.proto
diff --git a/datastore/datastore-core/src/jvmMain/kotlin/androidx/datastore/core/FileStorage.kt b/datastore/datastore-core/src/jvmMain/kotlin/androidx/datastore/core/FileStorage.kt
index 1bf4505..5172a45 100644
--- a/datastore/datastore-core/src/jvmMain/kotlin/androidx/datastore/core/FileStorage.kt
+++ b/datastore/datastore-core/src/jvmMain/kotlin/androidx/datastore/core/FileStorage.kt
@@ -165,9 +165,15 @@
             }
         } catch (ex: FileNotFoundException) {
             if (file.exists()) {
-                throw ex
+                // Re-read to prevent throwing from a race condition where the file is created by
+                // another process after the initial read attempt but before `file.exists()` is
+                // called. Otherwise file exists but we can't read it; throw FileNotFoundException
+                // because something is wrong.
+                return FileInputStream(file).use { stream ->
+                    serializer.readFrom(stream)
+                }
             }
-            serializer.defaultValue
+            return serializer.defaultValue
         }
     }
 
diff --git a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/FileStorageTest.kt b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/FileStorageTest.kt
index aebafd7..c04a81c 100644
--- a/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/FileStorageTest.kt
+++ b/datastore/datastore-core/src/jvmTest/kotlin/androidx/datastore/core/FileStorageTest.kt
@@ -333,6 +333,6 @@
     }
 
     private fun getTempFolder(): File {
-        return File.createTempFile("test", "test").parentFile
+        return File.createTempFile("test", "test").parentFile!!
     }
 }
diff --git a/datastore/datastore-multiprocess/api/current.txt b/datastore/datastore-multiprocess/api/current.txt
deleted file mode 100644
index 1cf3f2a..0000000
--- a/datastore/datastore-multiprocess/api/current.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-// Signature format: 4.0
-package androidx.datastore.multiprocess {
-
-  public final class MultiProcessDataStoreFactory {
-  }
-
-}
-
diff --git a/datastore/datastore-multiprocess/api/public_plus_experimental_current.txt b/datastore/datastore-multiprocess/api/public_plus_experimental_current.txt
deleted file mode 100644
index 0a26b66..0000000
--- a/datastore/datastore-multiprocess/api/public_plus_experimental_current.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-// Signature format: 4.0
-package androidx.datastore.multiprocess {
-
-  @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 {
-  }
-
-  public final class MultiProcessDataStoreFactory {
-    method @androidx.datastore.multiprocess.ExperimentalMultiProcessDataStore public static <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, optional kotlinx.coroutines.CoroutineScope scope, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
-    method @androidx.datastore.multiprocess.ExperimentalMultiProcessDataStore public static <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, optional java.util.List<? extends androidx.datastore.core.DataMigration<T>> migrations, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
-    method @androidx.datastore.multiprocess.ExperimentalMultiProcessDataStore public static <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, optional androidx.datastore.core.handlers.ReplaceFileCorruptionHandler<T>? corruptionHandler, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
-    method @androidx.datastore.multiprocess.ExperimentalMultiProcessDataStore public static <T> androidx.datastore.core.DataStore<T> create(androidx.datastore.core.Serializer<T> serializer, kotlin.jvm.functions.Function0<? extends java.io.File> produceFile);
-  }
-
-}
-
diff --git a/datastore/datastore-multiprocess/api/restricted_current.txt b/datastore/datastore-multiprocess/api/restricted_current.txt
deleted file mode 100644
index 1cf3f2a..0000000
--- a/datastore/datastore-multiprocess/api/restricted_current.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-// Signature format: 4.0
-package androidx.datastore.multiprocess {
-
-  public final class MultiProcessDataStoreFactory {
-  }
-
-}
-
diff --git a/datastore/datastore-multiprocess/build.gradle b/datastore/datastore-multiprocess/build.gradle
deleted file mode 100644
index e59a2cf..0000000
--- a/datastore/datastore-multiprocess/build.gradle
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import androidx.build.LibraryType
-
-plugins {
-    id("AndroidXPlugin")
-    id("com.android.library")
-    id("com.google.protobuf")
-    id("java-test-fixtures")
-    id("org.jetbrains.kotlin.android")
-}
-
-dependencies {
-    api(libs.kotlinStdlib)
-    api(libs.kotlinCoroutinesCore)
-    api("androidx.annotation:annotation:1.2.0")
-    api(project(":datastore:datastore-core"))
-
-    androidTestImplementation(libs.junit)
-    androidTestImplementation(libs.kotlinCoroutinesTest)
-    androidTestImplementation(libs.protobufLite)
-    androidTestImplementation(libs.truth)
-    androidTestImplementation(project(":internal-testutils-truth"))
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testCore)
-    androidTestImplementation(project(":datastore:datastore-core"))
-}
-
-protobuf {
-    protoc {
-        artifact = libs.protobufCompiler.get()
-    }
-    // Generates the java proto-lite code for the protos in this project. See
-    // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
-    // for more information.
-    generateProtoTasks {
-        all().each { task ->
-            task.builtins {
-                java {
-                    option "lite"
-                }
-            }
-        }
-    }
-}
-
-android {
-    externalNativeBuild {
-        cmake {
-            path "src/main/cpp/CMakeLists.txt"
-            version libs.versions.cmake.get()
-        }
-    }
-    namespace "androidx.datastore.multiprocess"
-}
-
-androidx {
-    name = "androidx.datastore:datastore-multiprocess"
-    type = LibraryType.PUBLISHED_LIBRARY
-    mavenGroup = LibraryGroups.DATASTORE
-    inceptionYear = "2022"
-    description = "Android DataStore MultiProcess - contains the underlying store used by " +
-            "multiple process use cases"
-}
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/TestingSerializer.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/TestingSerializer.kt
deleted file mode 100644
index 86d7763..0000000
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/TestingSerializer.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.datastore.multiprocess
-
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.Serializer
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-
-internal class TestingSerializer(
-    @Volatile var failReadWithCorruptionException: Boolean = false,
-    @Volatile var failingRead: Boolean = false,
-    @Volatile var failingWrite: Boolean = false,
-    override val defaultValue: Byte = 0
-) : Serializer<Byte> {
-    override suspend fun readFrom(input: InputStream): Byte {
-        if (failReadWithCorruptionException) {
-            throw CorruptionException(
-                "CorruptionException",
-                IOException()
-            )
-        }
-
-        if (failingRead) {
-            throw IOException("I was asked to fail on reads")
-        }
-
-        val read = input.read()
-        if (read == -1) {
-            return 0
-        }
-        return read.toByte()
-    }
-
-    override suspend fun writeTo(t: Byte, output: OutputStream) {
-        if (failingWrite) {
-            throw IOException("I was asked to fail on writes")
-        }
-        output.write(t.toInt())
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactory.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactory.kt
deleted file mode 100644
index 4c8075f..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/MultiProcessDataStoreFactory.kt
+++ /dev/null
@@ -1,74 +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.
- */
-
-@file:JvmName("MultiProcessDataStoreFactory")
-
-package androidx.datastore.multiprocess
-
-import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
-import androidx.datastore.core.DataStore
-import androidx.datastore.core.DataMigration
-import androidx.datastore.core.Serializer
-import androidx.datastore.multiprocess.handlers.NoOpCorruptionHandler
-import java.io.File
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-
-/**
- * Create an instance of MultiProcessDataStore, which provides cross-process eventual
- * consistency. Never create more than one instance of DataStore for a given file in the same
- * process; doing so can break all DataStore functionality. You should consider managing your
- * DataStore instance for each file as a singleton. If there are multiple DataStores active for
- * a given file in the same process, DataStore will throw IllegalStateException when reading or
- * updating data. A DataStore is considered active as long as its scope is active. Having
- * multiple instances, each for a different file, in the same process is OK.
- *
- * T is the type DataStore acts on. The type T must be immutable. Mutating a type used in
- * DataStore invalidates any guarantees that DataStore provides and will result in
- * potentially serious, hard-to-catch bugs. We strongly recommend using protocol buffers:
- * https://developers.google.com/protocol-buffers/docs/javatutorial - which provides
- * immutability guarantees, a simple API and efficient serialization.
- *
- * @param serializer Serializer for the type T used with DataStore. The type T must be immutable.
- * @param corruptionHandler The {@link androidx.datastore.core.handlers.ReplaceFileCorruptionHandler}
- * is invoked if DataStore encounters a [CorruptionException] when attempting to read data.
- * CorruptionExceptions are thrown by serializers when data can not be de-serialized.
- * @param migrations Migrations are run before any access to data can occur. Migrations must
- * be idempotent.
- * @param scope The scope in which IO operations and transform functions will execute.
- * @param produceFile Function which returns the file that the new DataStore will act on. The
- * function must return the same path every time. No two instances of DataStore should act on
- * the same file at the same time.
- *
- * @return a new DataStore instance with the provided configuration
- */
-@ExperimentalMultiProcessDataStore
-@JvmOverloads // Generate constructors for default params for java users.
-public fun <T> create(
-    serializer: Serializer<T>,
-    corruptionHandler: ReplaceFileCorruptionHandler<T>? = null,
-    migrations: List<DataMigration<T>> = listOf(),
-    scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
-    produceFile: () -> File
-): DataStore<T> = MultiProcessDataStore<T>(
-    produceFile = produceFile,
-    serializer = serializer,
-    initTasksList = listOf(DataMigrationInitializer.getInitializer(migrations)),
-    corruptionHandler = adapterCorruptionHandlerOrNull(corruptionHandler)
-        ?: NoOpCorruptionHandler(),
-    scope = scope
-)
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ThreadLock.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ThreadLock.kt
deleted file mode 100644
index 27ef8fa..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ThreadLock.kt
+++ /dev/null
@@ -1,51 +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.datastore.multiprocess
-
-import java.io.Closeable
-import java.io.InterruptedIOException
-import java.util.concurrent.Semaphore
-
-internal class ThreadLock(
-    private val semaphore: Semaphore?
-) : Closeable {
-    fun acquired(): Boolean {
-        return semaphore != null
-    }
-
-    override fun close() {
-        if (acquired()) {
-            semaphore!!.release()
-        }
-    }
-
-    internal companion object {
-        suspend fun tryAcquire(semaphore: Semaphore): ThreadLock {
-            val acquired = semaphore.tryAcquire()
-            return if (acquired) ThreadLock(semaphore) else ThreadLock(null)
-        }
-
-        suspend fun acquire(semaphore: Semaphore): ThreadLock {
-            try {
-                semaphore.acquire()
-            } catch (ex: InterruptedException) {
-                throw InterruptedIOException("semaphore not acquired: $ex")
-            }
-            return ThreadLock(semaphore)
-        }
-    }
-}
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt
deleted file mode 100644
index c92e910..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/handlers/NoOpCorruptionHandler.kt
+++ /dev/null
@@ -1,30 +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.datastore.multiprocess.handlers
-
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.multiprocess.CorruptionHandler
-
-/**
- * Default corruption handler which does nothing but rethrow the exception.
- */
-internal class NoOpCorruptionHandler<T> : CorruptionHandler<T> {
-
-    @Throws(CorruptionException::class)
-    override suspend fun handleCorruption(ex: CorruptionException): T {
-        throw ex
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt
deleted file mode 100644
index 5ec99ac..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/CorruptionHandler.kt
+++ /dev/null
@@ -1,58 +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.
- */
-@file:RestrictTo(RestrictTo.Scope.LIBRARY)
-
-package androidx.datastore.multiprocess
-
-import androidx.annotation.RestrictTo
-import androidx.datastore.core.CorruptionException
-import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
-
-/**
- * CorruptionHandlers allow recovery from corruption that prevents reading data from the file (as
- * indicated by a CorruptionException).
- *
- * This is a duplicate of {@link androidx.datastore.core.CorruptionHandler}.
- */
-internal interface CorruptionHandler<T> {
-    /**
-     * This function will be called by DataStore when it encounters corruption. If the
-     * implementation of this function throws an exception, it will be propagated to the original
-     * call to DataStore. Otherwise, the returned data will be written to disk.
-     *
-     * This function should not interact with any DataStore API - doing so can result in a deadlock.
-     *
-     * @param ex is the exception encountered when attempting to deserialize data from disk.
-     * @return The value that DataStore should attempt to write to disk.
-     **/
-    public suspend fun handleCorruption(ex: CorruptionException): T
-}
-
-/**
- * Create a `CorruptionHandler` instance to adapt the `ReplaceFileCorruptionHandler` instance. As
- * MPDSFactory takes in `ReplaceFileCorruptionHandler` in public API, it needs to be adapted to the
- * local definition of `CorruptionHandler` as it is a duplicated class.
- */
-// TODO(b/242906637): remove this method when interface definition is deduped
-internal fun <T> adapterCorruptionHandlerOrNull(
-    handler: ReplaceFileCorruptionHandler<T>?
-): CorruptionHandler<T>? {
-    return if (handler != null) object : CorruptionHandler<T> {
-        override suspend fun handleCorruption(ex: CorruptionException): T {
-            return handler.handleCorruption(ex)
-        }
-    } else null
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/DataMigrationInitializer.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/DataMigrationInitializer.kt
deleted file mode 100644
index ce0ba85..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/DataMigrationInitializer.kt
+++ /dev/null
@@ -1,76 +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.datastore.multiprocess
-
-import androidx.datastore.core.DataMigration
-
-/**
- * Returns an initializer function created from a list of DataMigrations.
- *
- * This is a duplicate of {@link androidx.datastore.core.DataMigrationInitializer}.
- */
-// TODO(b/242906637): remove duplicated definitions and reuse existing ones
-internal class DataMigrationInitializer<T>() {
-    companion object {
-        /**
-         * Creates an initializer from DataMigrations for use with DataStore.
-         *
-         * @param migrations A list of migrations that will be included in the initializer.
-         * @return The initializer which includes the data migrations returned from the factory
-         * functions.
-         */
-        fun <T> getInitializer(migrations: List<DataMigration<T>>):
-            suspend (api: InitializerApi<T>) -> Unit = { api ->
-            runMigrations(migrations, api)
-        }
-
-        private suspend fun <T> runMigrations(
-            migrations: List<DataMigration<T>>,
-            api: InitializerApi<T>
-        ) {
-            val cleanUps = mutableListOf<suspend () -> Unit>()
-
-            api.updateData { startingData ->
-                migrations.fold(startingData) { data, migration ->
-                    if (migration.shouldMigrate(data)) {
-                        cleanUps.add { migration.cleanUp() }
-                        migration.migrate(data)
-                    } else {
-                        data
-                    }
-                }
-            }
-
-            var cleanUpFailure: Throwable? = null
-
-            cleanUps.forEach { cleanUp ->
-                try {
-                    cleanUp()
-                } catch (exception: Throwable) {
-                    if (cleanUpFailure == null) {
-                        cleanUpFailure = exception
-                    } else {
-                        cleanUpFailure!!.addSuppressed(exception)
-                    }
-                }
-            }
-
-            // If we encountered a failure on cleanup, throw it.
-            cleanUpFailure?.let { throw it }
-        }
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt
deleted file mode 100644
index c7eaa410..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/InitializerApi.kt
+++ /dev/null
@@ -1,31 +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.datastore.multiprocess
-
-/**
- * The initializer API allows changes to be made to store before data is accessed through
- * data or updateData.
- *
- * Initializers are executed in the order in which they are added. They must be idempotent
- * since they are run each time the DataStore starts, and they may be run multiple times by a
- * single instance if a downstream initializer fails.
- *
- * Note: Initializers are internal only. Instead, see [DataMigration].
- */
-internal interface InitializerApi<T> {
-    suspend fun updateData(transform: suspend (t: T) -> T): T
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt
deleted file mode 100644
index 0b8ff0c..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/Message.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.datastore.multiprocess
-
-import kotlin.coroutines.CoroutineContext
-import kotlinx.coroutines.CompletableDeferred
-
-/** The actions for the actor. */
-internal sealed class Message<T> {
-    abstract val lastState: State<T>?
-
-    /**
-     * Represents a read operation. If the data is already cached, this is a no-op. If data
-     * has not been cached, it triggers a new read to the specified dataChannel.
-     */
-    class Read<T>(
-        override val lastState: State<T>?
-    ) : Message<T>()
-
-    /** Represents an update operation. */
-    class Update<T>(
-        val transform: suspend (t: T) -> T,
-        /**
-         * Used to signal (un)successful completion of the update to the caller.
-         */
-        val ack: CompletableDeferred<T>,
-        override val lastState: State<T>?,
-        val callerContext: CoroutineContext
-    ) : Message<T>()
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt
deleted file mode 100644
index 39f3a38..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/SimpleActor.kt
+++ /dev/null
@@ -1,127 +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.datastore.multiprocess
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED
-import kotlinx.coroutines.channels.ClosedSendChannelException
-import kotlinx.coroutines.channels.onClosed
-import kotlinx.coroutines.ensureActive
-import kotlinx.coroutines.launch
-import java.util.concurrent.atomic.AtomicInteger
-
-internal class SimpleActor<T>(
-    /**
-     * The scope in which to consume messages.
-     */
-    private val scope: CoroutineScope,
-    /**
-     * Function that will be called when scope is cancelled. Should *not* throw exceptions.
-     */
-    onComplete: (Throwable?) -> Unit,
-    /**
-     * Function that will be called for each element when the scope is cancelled. Should *not*
-     * throw exceptions.
-     */
-    onUndeliveredElement: (T, Throwable?) -> Unit,
-    /**
-     * Function that will be called once for each message.
-     *
-     * Must *not* throw an exception (other than CancellationException if scope is cancelled).
-     */
-    private val consumeMessage: suspend (T) -> Unit
-) {
-    private val messageQueue = Channel<T>(capacity = UNLIMITED)
-
-    /**
-     * Count of the number of remaining messages to process. When the messageQueue is closed,
-     * this is no longer used.
-     */
-    private val remainingMessages = AtomicInteger(0)
-
-    init {
-        // If the scope doesn't have a job, it won't be cancelled, so we don't need to register a
-        // callback.
-        scope.coroutineContext[Job]?.invokeOnCompletion { ex ->
-            onComplete(ex)
-
-            // TODO(rohitsat): replace this with Channel(onUndeliveredElement) when it
-            // is fixed: https://github.com/Kotlin/kotlinx.coroutines/issues/2435
-
-            messageQueue.close(ex)
-
-            while (true) {
-                messageQueue.tryReceive().getOrNull()?.let { msg ->
-                    onUndeliveredElement(msg, ex)
-                } ?: break
-            }
-        }
-    }
-
-    /**
-     * Sends a message to a message queue to be processed by [consumeMessage] in [scope].
-     *
-     * If [offer] completes successfully, the msg *will* be processed either by
-     * consumeMessage or
-     * onUndeliveredElement. If [offer] throws an exception, the message may or may not be
-     * processed.
-     */
-    fun offer(msg: T) {
-        /**
-         * Possible states:
-         * 1) remainingMessages = 0
-         *   All messages have been consumed, so there is no active consumer
-         * 2) remainingMessages > 0, no active consumer
-         *   One of the senders is responsible for triggering the consumer
-         * 3) remainingMessages > 0, active consumer
-         *   Consumer will continue to consume until remainingMessages is 0
-         * 4) messageQueue is closed, there are remaining messages to consume
-         *   Attempts to offer messages will fail, onComplete() will consume remaining messages
-         *   with onUndelivered. The Consumer has already completed since close() is called by
-         *   onComplete().
-         * 5) messageQueue is closed, there are no remaining messages to consume
-         *   Attempts to offer messages will fail.
-         */
-
-        // should never return false bc the channel capacity is unlimited
-        check(
-            messageQueue.trySend(msg)
-                .onClosed { throw it ?: ClosedSendChannelException("Channel was closed normally") }
-                .isSuccess
-        )
-
-        // If the number of remaining messages was 0, there is no active consumer, since it quits
-        // consuming once remaining messages hits 0. We must kick off a new consumer.
-        if (remainingMessages.getAndIncrement() == 0) {
-            scope.launch {
-                // We shouldn't have started a new consumer unless there are remaining messages...
-                check(remainingMessages.get() > 0)
-
-                do {
-                    // We don't want to try to consume a new message unless we are still active.
-                    // If ensureActive throws, the scope is no longer active, so it doesn't
-                    // matter that we have remaining messages.
-                    scope.ensureActive()
-
-                    consumeMessage(messageQueue.receive())
-                } while (remainingMessages.decrementAndGet() != 0)
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt
deleted file mode 100644
index c83819b..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/State.kt
+++ /dev/null
@@ -1,45 +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.datastore.multiprocess
-
-/**
- * Represents the current state of the DataStore.
- */
-internal sealed class State<T>
-
-internal object UnInitialized : State<Any>()
-
-/**
- * A read from disk has succeeded, value represents the current on disk state.
- */
-internal class Data<T>(val value: T, val hashCode: Int, val version: Int) : State<T>() {
-    fun checkHashCode() {
-        check(value.hashCode() == hashCode) {
-            "Data in DataStore was mutated but DataStore is only compatible with Immutable types."
-        }
-    }
-}
-
-/**
- * A read from disk has failed. ReadException is the exception that was thrown.
- */
-internal class ReadException<T>(val readException: Throwable) : State<T>()
-
-/**
- * The scope has been cancelled. This DataStore cannot process any new reads or writes.
- */
-internal class Final<T>(val finalException: Throwable) : State<T>()
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt
deleted file mode 100644
index 45b1c81..0000000
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/internal/UncloseableOutputStream.kt
+++ /dev/null
@@ -1,47 +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.datastore.multiprocess
-
-import java.io.FileOutputStream
-import java.io.OutputStream
-
-/**
- * Wrapper on FileOutputStream to prevent closing the underlying OutputStream.
- */
-internal class UncloseableOutputStream(val fileOutputStream: FileOutputStream) : OutputStream() {
-
-    override fun write(b: Int) {
-        fileOutputStream.write(b)
-    }
-
-    override fun write(b: ByteArray) {
-        fileOutputStream.write(b)
-    }
-
-    override fun write(bytes: ByteArray, off: Int, len: Int) {
-        fileOutputStream.write(bytes, off, len)
-    }
-
-    override fun close() {
-        // We will not close the underlying FileOutputStream until after we're done syncing
-        // the fd. This is useful for things like b/173037611.
-    }
-
-    override fun flush() {
-        fileOutputStream.flush()
-    }
-}
\ No newline at end of file
diff --git a/datastore/datastore-preferences-core/build.gradle b/datastore/datastore-preferences-core/build.gradle
index 20f30bf..a32be22 100644
--- a/datastore/datastore-preferences-core/build.gradle
+++ b/datastore/datastore-preferences-core/build.gradle
@@ -123,7 +123,7 @@
 
 androidx {
     name = "Android Preferences DataStore Core"
-    type = LibraryType.KMP_LIBRARY
+    type = LibraryType.PUBLISHED_LIBRARY
     mavenGroup = LibraryGroups.DATASTORE
     inceptionYear = "2020"
     description = "Android Preferences DataStore without the Android Dependencies"
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index 81afb45..add01af 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -81,7 +81,7 @@
 # https://youtrack.jetbrains.com/issue/KT-54627
 \- Task \`\:commonizeNativeDistribution\` of type \`org\.jetbrains\.kotlin\.gradle\.targets\.native\.internal\.NativeDistributionCommonizerTask\`\: error writing value of type \'java\.util\.concurrent\.locks\.ReentrantLock\'
 See https://docs\.gradle\.org/[0-9]+\.[0-9]+.*/userguide/configuration_cache\.html\#config_cache:requirements:use_project_during_execution
-See https\:\/\/docs\.gradle\.org\/[0-9]+\.[0-9]+\/userguide\/configuration_cache\.html\#config_cache\:requirements\:disallowed_types
+See https\:\/\/docs\.gradle\.org\/[0-9]+\.[0-9]+.*\/userguide\/configuration_cache\.html\#config_cache\:requirements\:disallowed_types
 See the complete report at file://\$SUPPORT/build/reports/configuration\-cache/[^/]*/[^/]*/configuration\-cache\-report\.html
 See the complete report at file://\$OUT_DIR/androidx/build/reports/configuration\-cache/[^ ]*/[^ ]*/configuration\-cache\-report\.html
 # > Task :compose:ui:ui:processDebugAndroidTestManifest
@@ -185,8 +185,8 @@
 WARN: Missing @param tag for parameter `context` of function android\.support\.v[0-9]+\.media\.session/MediaControllerCompat/MediaControllerCompat/\#android\.content\.Context\#android\.support\.v[0-9]+\.media\.session\.MediaSessionCompat/PointingToDeclaration/
 WARN: Missing @param tag for parameter `context` of function android\.support\.v[0-9]+\.media\.session/MediaControllerCompat/MediaControllerCompat/\#android\.content\.Context\#android\.support\.v[0-9]+\.media\.session\.MediaSessionCompat\.Token/PointingToDeclaration/
 in DClass ComplicationSlot
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of ComplicationSlot in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
 @param id,
 @param canvasComplicationFactory,
 @param supportedTypes,
@@ -196,8 +196,8 @@
 \@param defaultPolicy
 @param complicationTapFilter
 in DClass UserStyle
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of UserStyle in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/style\/CurrentUserStyleRepository\.kt at line [0-9]+\.
 WARN: Missing @param tag for parameter `matchHeightConstraintsFirst` of function androidx\.compose\.ui/Modifier/aspectRatio/androidx\.compose\.ui\.Modifier\#kotlin\.Float\#kotlin\.Boolean/PointingToDeclaration/
 WARN: Missing @param tag for parameter `painter` of function androidx\.compose\.ui/Modifier/paint/androidx\.compose\.ui\.Modifier\#androidx\.compose\.ui\.graphics\.painter\.Painter\#kotlin\.Boolean\#androidx\.compose\.ui\.Alignment\#androidx\.compose\.ui\.layout\.ContentScale\#kotlin\.Float\#androidx\.compose\.ui\.graphics\.ColorFilter\?/PointingToDeclaration/
 WARN: Missing @param tag for parameter `cookieManager` of function androidx\.webkit/CookieManagerCompat/getCookieInfo/\#android\.webkit\.CookieManager\#java\.lang\.String/PointingToDeclaration/
@@ -214,8 +214,8 @@
 WARN: Missing @param tag for parameter `spotColor` of function androidx\.compose\.ui/Modifier/shadow/androidx\.compose\.ui\.Modifier\#androidx\.compose\.ui\.unit\.Dp\#androidx\.compose\.ui\.graphics\.Shape\#kotlin\.Boolean\#androidx\.compose\.ui\.graphics\.Color\#androidx\.compose\.ui\.graphics\.Color/PointingToDeclaration/
 @param copySelectedOptions
 in DClass Builder
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
-Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedSourcesForDackka\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-public\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
+Did you make a typo\? Are you trying to refer to something not visible to users\? in declaration of Builder in file \$OUT_DIR\/androidx\/docs\-tip\-of\-tree\/build\/unzippedJvmSources\/androidx\/wear\/watchface\/ComplicationSlot\.kt at line [0-9]+\.
 Did you make a typo\? Are you trying to refer to something not visible to users\?
 WARNING: do not use 'an' before the exception type in an @throws statement\. This is against jdoc spec, will be an error in the next version of dackka, and your exception is not being linked and looks bad\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[DocumentationLink\(dri=java\.lang/IllegalArgumentException///PointingToDeclaration/, children=\[Text\(body=IllegalArgumentException, children=\[\], params=\{\}\)\], params=\{\}\), Text\(body= if the property name or index is invalid\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=an, exceptionAddress=null\)\.
 WARNING: do not use 'an' before the exception type in an @throws statement\. This is against jdoc spec, will be an error in the next version of dackka, and your exception is not being linked and looks bad\. This was observed in Throws\(root=CustomDocTag\(children=\[P\(children=\[DocumentationLink\(dri=kotlin/IllegalArgumentException///PointingToDeclaration/, children=\[Text\(body=IllegalArgumentException, children=\[\], params=\{\}\)\], params=\{href=\[IllegalArgumentException\]\}\), Text\(body= if address is invalid\., children=\[\], params=\{\}\)\], params=\{\}\)\], params=\{\}, name=MARKDOWN_FILE\), name=an, exceptionAddress=null\)\.
diff --git a/development/offlinifyDocs/offlinify_dackka_docs.py b/development/offlinifyDocs/offlinify_dackka_docs.py
index a042fd6..932d03c 100644
--- a/development/offlinifyDocs/offlinify_dackka_docs.py
+++ b/development/offlinifyDocs/offlinify_dackka_docs.py
@@ -14,8 +14,8 @@
 
 SCRIPT_PATH = Path(__file__).parent.absolute()
 REL_PATH_TO_DOCS = '../../../../out/androidx/docs-tip-of-tree/build'
-DEFAULT_INPUT  = os.path.abspath(os.path.join(SCRIPT_PATH, REL_PATH_TO_DOCS, 'dackkaDocs'))
-DEFAULT_OUTPUT = os.path.abspath(os.path.join(SCRIPT_PATH, REL_PATH_TO_DOCS, 'offlineDackkaDocs'))
+DEFAULT_INPUT  = os.path.abspath(os.path.join(SCRIPT_PATH, REL_PATH_TO_DOCS, 'docs'))
+DEFAULT_OUTPUT = os.path.abspath(os.path.join(SCRIPT_PATH, REL_PATH_TO_DOCS, 'offlineDocs'))
 REL_PATH_TO_LIBRARIES = 'reference/kotlin/androidx'
 STYLE_FILENAME = 'style.css'
 CSS_SOURCE_PATH = os.path.join(SCRIPT_PATH, STYLE_FILENAME)
@@ -78,7 +78,7 @@
   if path is None:
     if not os.path.exists(DEFAULT_INPUT):
       print(f'ERROR: Default input path `{DEFAULT_INPUT}` does not exist. Generate docs by running')
-      print('    ./gradlew dackkaDocs')
+      print('    ./gradlew docs')
       exit(-1)
     return DEFAULT_INPUT
 
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index f88da60..3e82bc1 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -62,10 +62,10 @@
 
   if (( FLAGS_useToT )); then
     printf "Downloading docs-tip-of-tree zip files \n"
-    androidxDackkaZip="dackka-tip-of-tree-docs-${FLAGS_buildId}.zip"
+    androidxDackkaZip="docs-tip-of-tree-${FLAGS_buildId}.zip"
   else
     printf "Downloading docs-public zip files \n"
-    androidxDackkaZip="dackka-public-docs-${FLAGS_buildId}.zip"
+    androidxDackkaZip="docs-public-${FLAGS_buildId}.zip"
   fi
 
   if (( "${FLAGS_buildId::1}" == "P" )); then
@@ -95,7 +95,7 @@
   printf "== Copying doc sources from local directory $FLAGS_sourceDir \n"
   printf "=================================================================== \n"
 
-  cp -r "$FLAGS_sourceDir/dackkaDocs/." $newDir
+  cp -r "$FLAGS_sourceDir/docs/." $newDir
 
 fi
 
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index cbb96f5..90f7a93 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -134,7 +134,6 @@
     docs(project(":customview:customview-poolingcontainer"))
     docs(project(":datastore:datastore"))
     docs(project(":datastore:datastore-core"))
-    docs(project(":datastore:datastore-multiprocess"))
     docs(project(":datastore:datastore-preferences"))
     docs(project(":datastore:datastore-preferences-core"))
     docs(project(":datastore:datastore-preferences-proto"))
diff --git a/emoji2/emoji2-emojipicker/api/current.txt b/emoji2/emoji2-emojipicker/api/current.txt
index e6f50d0..aab9e2c 100644
--- a/emoji2/emoji2-emojipicker/api/current.txt
+++ b/emoji2/emoji2-emojipicker/api/current.txt
@@ -1 +1,16 @@
 // Signature format: 4.0
+package androidx.emoji2.emojipicker {
+
+  public final class EmojiPickerView extends android.widget.FrameLayout {
+    ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs);
+    ctor public EmojiPickerView(android.content.Context context);
+    method public int getEmojiGridColumns();
+    method public float getEmojiGridRows();
+    method public void setEmojiGridColumns(int);
+    method public void setEmojiGridRows(float);
+    property public final int emojiGridColumns;
+    property public final float emojiGridRows;
+  }
+
+}
+
diff --git a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
index e6f50d0..aab9e2c 100644
--- a/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
+++ b/emoji2/emoji2-emojipicker/api/public_plus_experimental_current.txt
@@ -1 +1,16 @@
 // Signature format: 4.0
+package androidx.emoji2.emojipicker {
+
+  public final class EmojiPickerView extends android.widget.FrameLayout {
+    ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs);
+    ctor public EmojiPickerView(android.content.Context context);
+    method public int getEmojiGridColumns();
+    method public float getEmojiGridRows();
+    method public void setEmojiGridColumns(int);
+    method public void setEmojiGridRows(float);
+    property public final int emojiGridColumns;
+    property public final float emojiGridRows;
+  }
+
+}
+
diff --git a/emoji2/emoji2-emojipicker/api/restricted_current.txt b/emoji2/emoji2-emojipicker/api/restricted_current.txt
index e6f50d0..aab9e2c 100644
--- a/emoji2/emoji2-emojipicker/api/restricted_current.txt
+++ b/emoji2/emoji2-emojipicker/api/restricted_current.txt
@@ -1 +1,16 @@
 // Signature format: 4.0
+package androidx.emoji2.emojipicker {
+
+  public final class EmojiPickerView extends android.widget.FrameLayout {
+    ctor public EmojiPickerView(android.content.Context context, optional android.util.AttributeSet? attrs);
+    ctor public EmojiPickerView(android.content.Context context);
+    method public int getEmojiGridColumns();
+    method public float getEmojiGridRows();
+    method public void setEmojiGridColumns(int);
+    method public void setEmojiGridRows(float);
+    property public final int emojiGridColumns;
+    property public final float emojiGridRows;
+  }
+
+}
+
diff --git a/emoji2/emoji2-emojipicker/build.gradle b/emoji2/emoji2-emojipicker/build.gradle
index 9156e71..4c98c732 100644
--- a/emoji2/emoji2-emojipicker/build.gradle
+++ b/emoji2/emoji2-emojipicker/build.gradle
@@ -27,13 +27,22 @@
     implementation("androidx.core:core-ktx:1.8.0")
     implementation project(path: ':emoji2:emoji2')
     implementation project(path: ':core:core')
+    implementation 'androidx.recyclerview:recyclerview:1.2.1'
+    implementation 'androidx.appcompat:appcompat:1.5.1'
+    implementation 'androidx.tracing:tracing-ktx:1.0.0'
+    implementation 'androidx.test.ext:junit-ktx:1.1.3'
 
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testRules)
     androidTestImplementation project(path: ':test:screenshot:screenshot')
+    androidTestImplementation project(path: ':internal-testutils-runtime')
+
+    samples(project(":emoji2:emoji2-emojipicker:emoji2-emojipicker-samples"))
 }
 
+
 android {
     defaultConfig {
         minSdkVersion 21
diff --git a/emoji2/emoji2-emojipicker/samples/build.gradle b/emoji2/emoji2-emojipicker/samples/build.gradle
new file mode 100644
index 0000000..a8bf5b4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/build.gradle
@@ -0,0 +1,40 @@
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import androidx.build.LibraryType
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("org.jetbrains.kotlin.android")
+}
+dependencies {
+    api(libs.kotlinStdlib)
+    implementation("androidx.appcompat:appcompat:1.5.1")
+    implementation project(path: ':emoji2:emoji2-emojipicker')
+}
+android {
+    defaultConfig {
+        minSdkVersion 21
+    }
+    namespace "androidx.emoji2.emojipicker.samples"
+}
+androidx {
+    name = "androidx.emoji2:emoji2-emojipicker-samples"
+    type = LibraryType.SAMPLES
+    mavenGroup = LibraryGroups.EMOJI2
+    inceptionYear = "2022"
+    description = "Contains the sample code for the APIs in the androidx.emoji2:emoji2-emojipicker library"
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..141611d
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+    <uses-sdk android:minSdkVersion="21"/>
+    <application>
+        <activity android:name=".MainActivity" android:exported="true"
+            android:theme="@style/Theme.AppCompat.DayNight">
+            <!-- Handle Google app icon launch. -->
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
similarity index 66%
copy from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
copy to emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
index 07fb357..8a8ebeb 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
+++ b/emoji2/emoji2-emojipicker/samples/src/main/java/androidx/emoji2/emojipicker/samples/MainActivity.kt
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.emoji2.emojipicker.samples
 
-@RequiresOptIn(
-    level = RequiresOptIn.Level.WARNING,
-    message = "This API is experimental and is likely to change in the future."
-)
-@Target(AnnotationTarget.FUNCTION)
-annotation class ExperimentalMultiProcessDataStore
\ No newline at end of file
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+
+class MainActivity : AppCompatActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.main)
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
new file mode 100644
index 0000000..3b15586
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/samples/src/main/res/layout/main.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.emoji2.emojipicker.EmojiPickerView
+        android:id="@+id/emoji_picker"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</LinearLayout>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
index 45dad90..3d68e16 100644
--- a/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
+++ b/emoji2/emoji2-emojipicker/src/androidTest/AndroidManifest.xml
@@ -16,7 +16,8 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android">
 
     <application>
-        <uses-library android:name="android.test.runner" />
+        <uses-library android:name="android.test.runner"/>
+        <activity android:name="androidx.emoji2.emojipicker.EmojiPickerViewTestActivity"/>
         <activity android:name="androidx.emoji2.emojipicker.EmojiViewTestActivity" />
     </application>
 </manifest>
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
new file mode 100644
index 0000000..a63f725
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/java/androidx/emoji2/emojipicker/EmojiPickerViewTest.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import androidx.core.view.isVisible
+import androidx.emoji2.emojipicker.test.R
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+class EmojiPickerViewTestActivity : Activity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.inflation_test)
+    }
+}
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class EmojiPickerViewTest {
+    private lateinit var mContext: Context
+
+    @get:Rule
+    val mActivityTestRule = ActivityScenarioRule(
+        EmojiPickerViewTestActivity::class.java
+    )
+
+    @Before
+    fun setUp() {
+        mContext = ApplicationProvider.getApplicationContext()
+    }
+
+    @Test
+    fun testCustomEmojiPickerView_rendered() {
+        mActivityTestRule.scenario.onActivity {
+            val mEmojiPickerView = it.findViewById<EmojiPickerView>(R.id.emojiPickerTest)
+            assert(mEmojiPickerView.isVisible)
+            assertEquals(mEmojiPickerView.emojiGridColumns, 10)
+        }
+    }
+}
diff --git a/emoji2/emoji2-emojipicker/src/androidTest/res/layout/inflation_test.xml b/emoji2/emoji2-emojipicker/src/androidTest/res/layout/inflation_test.xml
new file mode 100644
index 0000000..db9c41e
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/androidTest/res/layout/inflation_test.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.emoji2.emojipicker.EmojiPickerView
+        android:id="@+id/emojiPickerTest"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:emojiGridColumns="10"
+        app:emojiGridRows="8.5"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
new file mode 100644
index 0000000..43c70e4
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import androidx.annotation.UiThread
+import androidx.appcompat.widget.AppCompatTextView
+import androidx.recyclerview.widget.RecyclerView
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+import androidx.tracing.Trace
+
+/** RecyclerView adapter for emoji body.  */
+internal class EmojiPickerBodyAdapter(
+    context: Context,
+    private val emojiGridColumns: Int,
+    private val emojiGridRows: Float
+) : RecyclerView.Adapter<ViewHolder>() {
+    private val layoutInflater: LayoutInflater = LayoutInflater.from(context)
+    private val context = context
+
+    @UiThread
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        Trace.beginSection("EmojiPickerBodyAdapter.onCreateViewHolder")
+        try {
+            // TODO: Load real emoji data in the next change
+            val view: View = layoutInflater.inflate(
+                R.layout.emoji_picker_empty_category_text_view, parent,
+                /*attachToRoot= */ false
+            )
+            view.layoutParams = LayoutParams(
+                parent.width / emojiGridColumns, (parent.measuredHeight / emojiGridRows).toInt()
+            )
+            view.minimumHeight = (parent.measuredHeight / emojiGridRows).toInt()
+            return object : ViewHolder(view) {}
+        } finally {
+            Trace.endSection()
+        }
+    }
+
+    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
+        val emptyCategoryView: AppCompatTextView =
+            viewHolder.itemView.findViewById(R.id.emoji_picker_empty_category_view)
+        emptyCategoryView.setText(R.string.emoji_empty_non_recent_category)
+    }
+
+    override fun getItemCount(): Int {
+        return (emojiGridColumns * emojiGridRows).toInt()
+    }
+}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
similarity index 65%
copy from datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
copy to emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
index 07fb357..7108b68 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/ExperimentalMultiProcessDataStore.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerConstants.kt
@@ -14,11 +14,14 @@
  * limitations under the License.
  */
 
-package androidx.datastore.multiprocess
+package androidx.emoji2.emojipicker
 
-@RequiresOptIn(
-    level = RequiresOptIn.Level.WARNING,
-    message = "This API is experimental and is likely to change in the future."
-)
-@Target(AnnotationTarget.FUNCTION)
-annotation class ExperimentalMultiProcessDataStore
\ No newline at end of file
+/** A utility class to hold various constants used by the Emoji Picker library.  */
+internal object EmojiPickerConstants {
+
+    // The default number of body columns.
+    const val DEFAULT_BODY_COLUMNS = 9
+
+    // The default number of body rows.
+    const val DEFAULT_BODY_ROWS = 7.5f
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
new file mode 100644
index 0000000..225eba7
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerHeaderAdapter.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.emoji2.emojipicker
+
+import android.content.Context
+import android.content.res.TypedArray
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.annotation.DrawableRes
+import androidx.recyclerview.widget.RecyclerView.Adapter
+import androidx.recyclerview.widget.RecyclerView.ViewHolder
+
+/** RecyclerView adapter for emoji header.  */
+internal class EmojiPickerHeaderAdapter(
+    context: Context
+) : Adapter<ViewHolder>() {
+    @DrawableRes
+    private val categoryIconIds: IntArray
+    private val layoutInflater: LayoutInflater
+    private val context: Context
+
+    init {
+        this.context = context
+        this.categoryIconIds = getEmojiCategoryIconIds(context)
+        layoutInflater = LayoutInflater.from(context)
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+        return object : ViewHolder(
+            layoutInflater.inflate(
+                R.layout.header_icon_holder, parent,
+                /* attachToRoot= */ false
+            )
+        ) {}
+    }
+
+    override fun onBindViewHolder(viewHolder: ViewHolder, i: Int) {
+        val headerIconView: ImageView =
+            viewHolder.itemView.findViewById(R.id.emoji_picker_header_icon)
+        headerIconView.setImageDrawable(context.getDrawable(categoryIconIds[i]))
+    }
+
+    override fun getItemCount(): Int {
+        return categoryIconIds.size
+    }
+
+    @DrawableRes
+    private fun getEmojiCategoryIconIds(
+        context: Context
+    ): IntArray {
+        val typedArray: TypedArray =
+            context.resources.obtainTypedArray(R.array.emoji_categories_icons)
+        @DrawableRes val iconIds = IntArray(typedArray.length())
+        for (i in 0 until typedArray.length()) {
+            iconIds[i] = typedArray.getResourceId(i, 0)
+        }
+        typedArray.recycle()
+        return iconIds
+    }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 70efdef..2dfc5ff 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -16,12 +16,85 @@
 
 package androidx.emoji2.emojipicker
 
+import android.content.Context
+import android.content.res.TypedArray
+import android.util.AttributeSet
+import android.widget.FrameLayout
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+
 /**
  * The emoji picker view that provides up-to-date emojis in a vertical scrollable view with a
  * clickable horizontal header.
  */
-// TODO(scduan): API surface and implementation
-internal class EmojiPickerView {
+class EmojiPickerView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null
+) :
+    FrameLayout(context, attrs) {
+    /**
+     * This is the number of rows displayed in emoji picker. Some apps like Gboard have their
+     * default values(e.g. 7.5f which means there are height of 7.5 rows displayed on the UI).
+     * Clients could specify this value by their own. The default value will be used if
+     * emojiGridRows is set to non-positive value. Float value indicates that we will display
+     * partial of the last row and have content down, so the users get the idea that they can scroll
+     * down for more contents.
+     */
+    var emojiGridRows: Float = EmojiPickerConstants.DEFAULT_BODY_ROWS
+        set(value) {
+            field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_ROWS
+        }
+
+    /**
+     * This is the number of columns of emoji picker. Some apps like Gboard have their default
+     * values(e.g. 9). Clients could specify this value by their own. The default value will be used
+     * if emojiGridColumns is set to non-positive value.
+     */
+    var emojiGridColumns: Int = EmojiPickerConstants.DEFAULT_BODY_COLUMNS
+        set(value) {
+            field = if (value > 0) value else EmojiPickerConstants.DEFAULT_BODY_COLUMNS
+        }
+
+    private lateinit var headerView: RecyclerView
+    private lateinit var bodyView: RecyclerView
+
+    init {
+        initialize(context, attrs)
+    }
+
+    private fun initialize(context: Context, attrs: AttributeSet?) {
+        val typedArray: TypedArray =
+            context.obtainStyledAttributes(attrs, R.styleable.EmojiPickerView, 0, 0)
+        emojiGridRows = typedArray.getFloat(
+            R.styleable.EmojiPickerView_emojiGridRows,
+            EmojiPickerConstants.DEFAULT_BODY_ROWS
+        )
+        emojiGridColumns = typedArray.getInt(
+            R.styleable.EmojiPickerView_emojiGridColumns,
+            EmojiPickerConstants.DEFAULT_BODY_COLUMNS
+        )
+
+        // get emoji picker
+        val emojiPicker = inflate(context, R.layout.emoji_picker, this)
+
+        // set headerView
+        headerView = emojiPicker.findViewById(R.id.emoji_picker_header)
+        headerView.layoutManager =
+            LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, /* reverseLayout= */false)
+        headerView.adapter = EmojiPickerHeaderAdapter(context)
+
+        // set bodyView
+        bodyView = emojiPicker.findViewById(R.id.emoji_picker_body)
+        bodyView.layoutManager = GridLayoutManager(
+            context, emojiGridColumns, LinearLayoutManager.VERTICAL, /* reverseLayout= */
+            false
+        )
+        bodyView.adapter = EmojiPickerBodyAdapter(context, emojiGridColumns, emojiGridRows)
+
+        // recycle the typed array
+        typedArray.recycle()
+    }
 
     /**
      * MetaVersion will be null if EmojiCompat is not enabled.
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_emotions_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_emotions_vd_theme_24.xml
new file mode 100644
index 0000000..15f01d9
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_emotions_vd_theme_24.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM15.5,8C16.33,8 17,8.67 17,9.5S16.33,11 15.5,11S14,10.33 14,9.5S14.67,8 15.5,8zM8.5,8C9.33,8 10,8.67 10,9.5S9.33,11 8.5,11S7,10.33 7,9.5S7.67,8 8.5,8zM12,17.5c-2.33,0 -4.31,-1.46 -5.11,-3.5h10.22C16.31,16.04 14.33,17.5 12,17.5z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_events_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_events_vd_theme_24.xml
new file mode 100644
index 0000000..c915853
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_events_vd_theme_24.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19,5h-2V3H7v2H5C3.9,5 3,5.9 3,7v1c0,2.55 1.92,4.63 4.39,4.94c0.63,1.5 1.98,2.63 3.61,2.96V19H7v2h10v-2h-4v-3.1c1.63,-0.33 2.98,-1.46 3.61,-2.96C19.08,12.63 21,10.55 21,8V7C21,5.9 20.1,5 19,5zM7,10.82C5.84,10.4 5,9.3 5,8V7h2V10.82zM19,8c0,1.3 -0.84,2.4 -2,2.82V7h2V8z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_food_beverage_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_food_beverage_vd_theme_24.xml
new file mode 100644
index 0000000..6b140ac
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_food_beverage_vd_theme_24.xml
@@ -0,0 +1,29 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20,3H10v2.4l1.81,1.45C11.93,6.94 12,7.09 12,7.24v4.26c0,0.28 -0.22,0.5 -0.5,0.5h-4C7.22,12 7,11.78 7,11.5V7.24c0,-0.15 0.07,-0.3 0.19,-0.39L9,5.4V3H4v10c0,2.21 1.79,4 4,4h6c2.21,0 4,-1.79 4,-4v-3h2c1.11,0 2,-0.9 2,-2V5C22,3.89 21.11,3 20,3zM20,8h-2V5h2V8z" />
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M4,19h16v2h-16z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_nature_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_nature_vd_theme_24.xml
new file mode 100644
index 0000000..28b1e8f
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_nature_vd_theme_24.xml
@@ -0,0 +1,29 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M21.94,4.88C21.75,4.33 21.19,3.96 20.58,4H19.6l-0.31,-0.97C19.15,2.43 18.61,2 18,2h0c-0.61,0 -1.15,0.43 -1.29,1.04L16.4,4h-0.98c-0.61,-0.04 -1.16,0.32 -1.35,0.88c-0.19,0.56 0.04,1.17 0.56,1.48l0.87,0.52L15.1,8.12c-0.23,0.58 -0.04,1.25 0.45,1.62c0.5,0.37 1.17,0.35 1.64,-0.06L18,8.98l0.81,0.7c0.47,0.4 1.15,0.43 1.64,0.06c0.5,-0.37 0.68,-1.04 0.45,-1.62l-0.39,-1.24l0.87,-0.52C21.89,6.05 22.12,5.44 21.94,4.88zM18,7c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1s1,0.45 1,1S18.55,7 18,7z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M14.78,11.75c-0.12,-0.38 -0.65,-1.91 -2.53,-2.53V7h-1.5v2.02c-0.82,0.05 -2.19,0.4 -3.1,1.79c-0.57,-0.23 -1,-0.4 -1.57,-0.62c-0.79,-0.3 -1.63,-0.23 -2.34,0.11s-1.29,0.98 -1.57,1.81c-0.25,0.73 -0.21,1.49 0.04,2.16s0.72,1.25 1.35,1.62c-0.13,0.83 -0.06,2.26 1.11,3.43c1.25,1.25 2.72,1.22 3.42,1.11c0.11,0.19 0.5,0.71 1.14,1.1c0.64,0.39 1.54,0.65 2.65,0.27c0.82,-0.28 1.46,-0.85 1.81,-1.56c0.35,-0.71 0.42,-1.54 0.11,-2.36c-0.22,-0.56 -0.39,-0.99 -0.62,-1.55c1.38,-0.91 1.74,-2.27 1.79,-3.1H17v-1.5H14.78zM6.8,14.26c-0.7,0.19 -1.43,0.24 -2.08,-0.01c-0.51,-0.19 -0.87,-0.82 -0.65,-1.49C4.22,12.28 4.63,12 5.05,12c0.24,0 0.02,-0.06 3.7,1.4C8.18,13.73 7.5,14.07 6.8,14.26zM11.25,19.94c-0.65,0.22 -1.3,-0.12 -1.49,-0.65c-0.23,-0.63 -0.2,-1.34 -0.01,-2.04c0.18,-0.7 0.51,-1.4 0.86,-2c0.48,1.22 0.85,2.15 1.34,3.37C12.07,18.96 12.02,19.67 11.25,19.94zM12.42,14.41c-0.28,-0.7 -0.86,-1.94 -0.86,-1.94s-1.56,-0.72 -1.97,-0.88c0.43,-0.43 1.75,-1.07 2.82,0C12.84,12.01 13.49,13.33 12.42,14.41z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_objects_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_objects_vd_theme_24.xml
new file mode 100644
index 0000000..ab926c3
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_objects_vd_theme_24.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,2C8,2 4.75,5.25 4.75,9.25c0,2.52 1.29,4.75 3.25,6.04v3.56c0,1.1 0.9,2 2,2h0.01C10.42,21.54 11.15,22 12,22s1.58,-0.46 1.99,-1.14H14c1.1,0 2,-0.9 2,-2v-3.56c1.96,-1.3 3.25,-3.52 3.25,-6.04C19.25,5.25 16,2 12,2zM14,18.86h-4v-0.93h4V18.86zM14,16.93h-4V16h4V16.93zM12.75,11.31v2.81h-1.5v-2.81l-2.2,-2.2l1.06,-1.06L12,9.94l1.89,-1.89l1.06,1.06L12.75,11.31z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_people_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_people_vd_theme_24.xml
new file mode 100644
index 0000000..31a9d5d
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_people_vd_theme_24.xml
@@ -0,0 +1,29 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12,4m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15.89,8.11C15.5,7.72 14.83,7 13.53,7c-0.21,0 -1.42,0 -2.54,0C8.24,6.99 6,4.75 6,2H4c0,3.16 2.11,5.84 5,6.71V22h2v-6h2v6h2V10.05L18.95,14l1.41,-1.41L15.89,8.11z"/>
+</vector>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_symbols_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_symbols_vd_theme_24.xml
new file mode 100644
index 0000000..72e9457
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_symbols_vd_theme_24.xml
@@ -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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M3,2h8v2h-8z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,11l2,0l0,-4l3,0l0,-2l-8,0l0,2l3,0z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M12.3047,20.1846l7.7781,-7.7781l1.4142,1.4142l-7.7781,7.7781z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M14.5,14.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M19.5,19.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15.88,10.97C17.12,10.79 18,9.64 18,8.38L18,4h3V2h-3.5c-0.55,0 -1,0.45 -1,1v3.22c-0.4,-0.18 -0.84,-0.27 -1.33,-0.2c-1.08,0.15 -1.99,1.03 -2.14,2.11C12.79,9.8 14.21,11.22 15.88,10.97z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M9.76,15.96l-1.41,1.41l-0.71,-0.71l0.35,-0.35c0.98,-0.98 0.98,-2.56 0,-3.54c-0.49,-0.49 -1.13,-0.73 -1.77,-0.73c-0.64,0 -1.28,0.24 -1.77,0.73c-0.98,0.98 -0.98,2.56 0,3.54l0.35,0.35l-1.06,1.06c-0.98,0.98 -0.98,2.56 0,3.54C4.23,21.76 4.88,22 5.52,22s1.28,-0.24 1.77,-0.73l1.06,-1.06l1.41,1.41l1.41,-1.41L9.76,18.8l1.41,-1.41L9.76,15.96zM5.86,14.2c0.12,-0.12 0.26,-0.15 0.35,-0.15c0.09,0 0.23,0.03 0.35,0.15c0.19,0.2 0.19,0.51 0,0.71l-0.35,0.35L5.86,14.9C5.67,14.71 5.67,14.39 5.86,14.2zM5.86,19.85C5.74,19.97 5.6,20 5.52,20c-0.09,0 -0.23,-0.03 -0.35,-0.15c-0.19,-0.19 -0.19,-0.51 0,-0.71l1.06,-1.06l0.71,0.71L5.86,19.85z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_transportation_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_transportation_vd_theme_24.xml
new file mode 100644
index 0000000..252e656
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_emoji_transportation_vd_theme_24.xml
@@ -0,0 +1,41 @@
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M20.57,11.66C20.43,11.26 20.05,11 19.6,11h-7.19c-0.46,0 -0.83,0.26 -0.98,0.66L10,15.77l0.01,5.51c0,0.38 0.31,0.72 0.69,0.72h0.62C11.7,22 12,21.62 12,21.24V20.5h8v0.74c0,0.38 0.31,0.76 0.69,0.76h0.61c0.38,0 0.69,-0.34 0.69,-0.72L22,19.91v-4.14L20.57,11.66zM13.5,18.75c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1s1,0.45 1,1S14.05,18.75 13.5,18.75zM18.5,18.75c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1s1,0.45 1,1S19.05,18.75 18.5,18.75zM11.86,15l0.87,-2.5h6.55l0.87,2.5H11.86z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,12h2v2h-2z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M11,6h2v2h-2z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,16h2v2h-2z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M6,20h2v2h-2z"/>
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M15,9l2,0l0,-7l-10,0l0,6l-5,0l0,14l2,0l0,-12l5,0l0,-6l6,0z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_flag_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_flag_vd_theme_24.xml
new file mode 100644
index 0000000..d544dd3
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/gm_filled_flag_vd_theme_24.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24"
+    android:viewportWidth="24">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6H14.4z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/icon_tint_selector.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/icon_tint_selector.xml
new file mode 100644
index 0000000..bb387de
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/icon_tint_selector.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:color="?attr/EmojiPickerColorHeaderIconSelected" android:state_selected="true"/>
+    <item android:color="?attr/EmojiPickerColorHeaderIcon"/>
+</selector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/drawable/quantum_gm_ic_access_time_filled_vd_theme_24.xml b/emoji2/emoji2-emojipicker/src/main/res/drawable/quantum_gm_ic_access_time_filled_vd_theme_24.xml
new file mode 100644
index 0000000..a879376
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/drawable/quantum_gm_ic_access_time_filled_vd_theme_24.xml
@@ -0,0 +1,26 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:tint="?attr/colorControlNormal"
+    android:viewportHeight="24.0"
+    android:viewportWidth="24.0">
+    <path
+        android:fillColor="@android:color/white"
+        android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM16.49,16.36L11,13.07L11,6.42h2v5.51l4.51,2.71 -1.02,1.72z"/>
+</vector>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
new file mode 100644
index 0000000..0753bdd
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/emoji_picker_header"
+        android:layout_width="wrap_content"
+        android:layout_height="@dimen/emoji_picker_header_height"/>
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/emoji_picker_body"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"/>
+</LinearLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker_empty_category_text_view.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker_empty_category_text_view.xml
new file mode 100644
index 0000000..a9c86e1
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/emoji_picker_empty_category_text_view.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/emoji_picker_empty_category_view"
+    style="?attr/EmojiPickerStyleCategoryEmptyHintText"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_vertical"
+    android:paddingEnd="7dp"
+    android:paddingStart="7dp"
+    android:textSize="16dp"
+    tools:ignore="SmallSp"/>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/layout/header_icon_holder.xml b/emoji2/emoji2-emojipicker/src/main/res/layout/header_icon_holder.xml
new file mode 100644
index 0000000..1569f9f
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/layout/header_icon_holder.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="@dimen/emoji_picker_header_icon_holder_width"
+    android:layout_height="match_parent"
+    android:background="?android:attr/selectableItemBackground"
+    android:minHeight="@dimen/emoji_picker_header_icon_holder_min_height">
+    <androidx.appcompat.widget.AppCompatImageView
+        android:id="@+id/emoji_picker_header_icon"
+        android:layout_width="@dimen/emoji_picker_header_icon_width"
+        android:layout_height="@dimen/emoji_picker_header_icon_height"
+        android:gravity="center"
+        style="?attr/EmojiPickerStyleHeaderIcon"
+        android:layout_gravity="center"/>
+</FrameLayout>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
index c2d25d3..1715fc6 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/arrays.xml
@@ -28,6 +28,21 @@
         <item>@raw/emoji_category_flags</item>
     </array>
 
+    <integer-array name="emoji_categories_icons">
+        <item>@drawable/quantum_gm_ic_access_time_filled_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_emotions_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_people_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_nature_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_food_beverage_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_transportation_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_events_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_objects_vd_theme_24</item>
+        <item>@drawable/gm_filled_emoji_symbols_vd_theme_24</item>
+        <item>@drawable/gm_filled_flag_vd_theme_24</item>
+    </integer-array>
+
+    <!-- The drawable resources to be used as emoji category icons. Order of the icons must match
+         order of the content descriptions below. -->
     <string-array name="category_names">
         <item>@string/emoji_category_emotions</item>
         <item>@string/emoji_category_people</item>
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/attrs.xml b/emoji2/emoji2-emojipicker/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..4db3f51
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/attrs.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<!--
+  emojiGridRows: The number of rows which could be displayed on the UI. Some apps(e.g. Gboard) have
+                 the default value of 7.5f.
+  emojiGridColumns: The number of columns which could be displayed on the UI. Some apps(e.g. Gboard)
+                    have the default value of 9.
+
+  The method to specify the values is shown below
+  <EmojiPickerView
+          app:emojiGridRows=""
+          app:emojiGridColumns=""/>
+-->
+<resources>
+    <declare-styleable name="EmojiPickerView">
+        <attr name="emojiGridRows" format="float"/>
+        <attr name="emojiGridColumns" format="integer"/>
+    </declare-styleable>
+</resources>
+
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
new file mode 100644
index 0000000..3948f9f
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/attrs_theme.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <attr name="EmojiPickerColorCategoryEmptyHintText" format="color"/>
+    <attr name="EmojiPickerColorHeaderIcon" format="color"/>
+    <attr name="EmojiPickerColorHeaderIconSelected" format="color"/>
+    <attr name="EmojiPickerStyleHeaderIcon" format="reference"/>
+    <attr name="EmojiPickerStyleCategoryEmptyHintText" format="reference"/>
+</resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5ab47ec
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/dimens.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+    <!-- Describes the category list in the emoji picker header view. -->
+    <dimen name="emoji_picker_header_icon_holder_width">34dp</dimen>
+    <dimen name="emoji_picker_header_icon_holder_min_height">36dp</dimen>
+    <dimen name="emoji_picker_header_icon_width">20dp</dimen>
+    <dimen name="emoji_picker_header_icon_height">20dp</dimen>
+    <dimen name="emoji_picker_header_icon_underline_width">28dp</dimen>
+    <dimen name="emoji_picker_header_icon_underline_height">2dp</dimen>
+    <dimen name="emoji_picker_header_height">50dp</dimen>
+</resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
index 7c57c2d..e2ffab3 100644
--- a/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/strings.xml
@@ -34,4 +34,7 @@
     <string name="emoji_category_symbols">SYMBOLS</string>
     <!-- Emoji keyboard's flags sub category. Flags sub category contains emojis that represent country and regional flags. -->
     <string name="emoji_category_flags">FLAGS</string>
+
+    <!-- Shown in emoji keyboard (non-Recent category) when the category is empty. -->
+    <string name="emoji_empty_non_recent_category">No emojis available</string>
 </resources>
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
new file mode 100644
index 0000000..fa44e69
--- /dev/null
+++ b/emoji2/emoji2-emojipicker/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<resources>
+
+    <style name="EmojiPickerStyleHeaderIcon">
+        <item name="android:tint">@drawable/icon_tint_selector</item>
+    </style>
+
+    <style name="EmojiPickerStyleCategoryEmptyHintText">
+        <item name="android:textColor">?attr/EmojiPickerColorCategoryEmptyHintText</item>
+    </style>
+
+    <style name="EmojiPickerCategoryContainer">
+        <item name="android:layout_marginBottom">8dp</item>
+    </style>
+</resources>
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
index c015eff..8d535a3 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentSavedStateRegistryTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.fragment.app
 
+import android.content.Context
 import android.os.Bundle
 import androidx.activity.result.ActivityResultRegistry
 import androidx.activity.result.contract.ActivityResultContract
@@ -89,7 +90,19 @@
     }
 
     @Test
-    fun savedStateEarlyRegister() {
+    fun savedStateOnAttachConsume() {
+        withUse(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
+            initializeSavedState(OnAttachCheckingFragment())
+            recreate()
+            val fragment = withActivity {
+                supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) as OnAttachCheckingFragment
+            }
+            assertThat(fragment.restoredState).isEqualTo(VALUE)
+        }
+    }
+
+    @Test
+    fun savedStateOnCreateConsume() {
        withUse(ActivityScenario.launch(FragmentSavedStateActivity::class.java)) {
             initializeSavedState(OnCreateCheckingFragment())
             recreate()
@@ -147,6 +160,17 @@
         ?: throw IllegalStateException("Fragment under test wasn't found")
 }
 
+class OnAttachCheckingFragment : Fragment() {
+
+    var restoredState: String? = null
+
+    override fun onAttach(context: Context) {
+        super.onAttach(context)
+        val savedState = savedStateRegistry.consumeRestoredStateForKey(CALLBACK_KEY)
+        restoredState = savedState?.getString(KEY)
+    }
+}
+
 class OnCreateCheckingFragment : Fragment() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
index 3bd791b..261d1802 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentStoreTest.kt
@@ -28,7 +28,9 @@
 import androidx.testutils.withUse
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import leakcanary.DetectLeaksAfterTestSuccess
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mockito.mock
@@ -45,6 +47,9 @@
     private lateinit var emptyFragment: Fragment
     private lateinit var emptyStateManager: FragmentStateManager
 
+    @get:Rule
+    val rule = DetectLeaksAfterTestSuccess()
+
     @Before
     fun setup() {
         fragmentStore = FragmentStore()
@@ -337,6 +342,9 @@
             val foundFragment = fragmentStore.findFragmentByWho(childFragment.mWho)
             assertThat(foundFragment)
                 .isSameInstanceAs(childFragment)
+
+            fragmentStore.removeFragment(parentFragment)
+            fragmentStore.makeInactive(parentStateManager)
         }
     }
 
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
index 6572024..e10aa608 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentTransitionTest.kt
@@ -39,18 +39,16 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.After
-import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
-import org.mockito.ArgumentCaptor
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import leakcanary.DetectLeaksAfterTestSuccess
+import org.junit.Assert.fail
+import org.junit.rules.RuleChain
 
 @MediumTest
 @RunWith(Parameterized::class)
@@ -60,11 +58,14 @@
 ) {
 
     @Suppress("DEPRECATION")
-    @get:Rule
     var activityRule = androidx.test.rule.ActivityTestRule(FragmentTestActivity::class.java)
 
+    // Detect leaks BEFORE and AFTER activity is destroyed
+    @get:Rule
+    val ruleChain: RuleChain = RuleChain.outerRule(DetectLeaksAfterTestSuccess())
+        .around(activityRule)
+
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
-    private lateinit var fragmentManager: FragmentManager
     private var onBackStackChangedTimes: Int = 0
     private val onBackStackChangedListener =
         FragmentManager.OnBackStackChangedListener { onBackStackChangedTimes++ }
@@ -73,19 +74,23 @@
     fun setup() {
         activityRule.setContentView(R.layout.simple_container)
         onBackStackChangedTimes = 0
-        fragmentManager = activityRule.activity.supportFragmentManager
-        fragmentManager.addOnBackStackChangedListener(onBackStackChangedListener)
+        activityRule.activity.supportFragmentManager.addOnBackStackChangedListener(
+            onBackStackChangedListener
+        )
     }
 
     @After
     fun teardown() {
-        fragmentManager.removeOnBackStackChangedListener(onBackStackChangedListener)
+        activityRule.activity.supportFragmentManager.removeOnBackStackChangedListener(
+            onBackStackChangedListener
+        )
     }
 
     // Test that normal view transitions (enter, exit, reenter, return) run with
     // a single fragment.
     @Test
     fun enterExitTransitions() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         // enter transition
         val fragment = setupInitialFragment()
         val blue = activityRule.findBlue()
@@ -128,6 +133,7 @@
 
     @Test
     fun enterExitChildTransitions() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         // enter transition
         val fragment = TransitionFragment()
 
@@ -179,6 +185,7 @@
     // finishes is handled correctly.
     @Test
     fun removeThenAddBeforeTransitionFinishes() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         // enter transition
         val fragment = setupInitialFragment()
 
@@ -213,6 +220,7 @@
 
     @Test
     fun ensureTransitionsFinishBeforeViewDestroyed() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         // enter transition
         val fragment = TransitionFinishFirstFragment()
         fragmentManager.beginTransaction()
@@ -279,6 +287,7 @@
 
     @Test
     fun noSharedElementReturnSharedElement() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         fragment1.sharedElementEnterTransition = null
@@ -356,6 +365,7 @@
     // Adding/removing the same fragment multiple times shouldn't mess anything up
     @Test
     fun removeAdded() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         val startBlue = activityRule.findBlue()
@@ -409,6 +419,7 @@
     // Make sure that shared elements on two different fragment containers don't interact
     @Test
     fun crossContainer() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         activityRule.setContentView(R.layout.double_container)
         val fragment1 = TransitionFragment(R.layout.scene1)
         val fragment2 = TransitionFragment(R.layout.scene1)
@@ -453,61 +464,68 @@
 
         // Now do a transition to scene2
         val fragment2 = TransitionFragment(R.layout.scene2)
+        lateinit var startNames: List<String>
+        lateinit var startViews: List<View>
+        var startSnapshots: List<View>? = null
+        lateinit var endNames: List<String>
+        lateinit var endViews: List<View>
+        var endSnapshots: List<View>? = null
 
-        val enterCallback = mock(SharedElementCallback::class.java)
+        val enterCallback = object : SharedElementCallback() {
+            override fun onSharedElementStart(
+                sharedElementNames: MutableList<String>?,
+                sharedElements: MutableList<View>?,
+                sharedElementSnapshots: MutableList<View>?
+            ) {
+                startNames = sharedElementNames!!
+                startViews = sharedElements!!
+                startSnapshots = sharedElementSnapshots
+            }
+
+            override fun onSharedElementEnd(
+                sharedElementNames: MutableList<String>?,
+                sharedElements: MutableList<View>?,
+                sharedElementSnapshots: MutableList<View>?
+            ) {
+                endNames = sharedElementNames!!
+                endViews = sharedElements!!
+                endSnapshots = sharedElementSnapshots
+            }
+        }
         fragment2.setEnterSharedElementCallback(enterCallback)
 
         val startBlue = activityRule.findBlue()
 
         verifyTransition(fragment1, fragment2, "blueSquare")
 
-        val names = ArgumentCaptor.forClass(List::class.java as Class<List<String>>)
-        val views = ArgumentCaptor.forClass(List::class.java as Class<List<View>>)
-        val snapshots = ArgumentCaptor.forClass(List::class.java as Class<List<View>>)
-        verify(enterCallback).onSharedElementStart(
-            names.capture(), views.capture(),
-            snapshots.capture()
-        )
-        assertThat(names.value).containsExactly("blueSquare")
-        assertThat(views.value).containsExactly(startBlue)
-        assertThat(snapshots.value).isNull()
+        assertThat(startNames).containsExactly("blueSquare")
+        assertThat(startViews).containsExactly(startBlue)
+        assertThat(startSnapshots).isNull()
 
         val endBlue = activityRule.findBlue()
 
-        verify(enterCallback).onSharedElementEnd(
-            names.capture(), views.capture(),
-            snapshots.capture()
-        )
-        assertThat(names.value).containsExactly("blueSquare")
-        assertThat(views.value).containsExactly(endBlue)
-        assertThat(snapshots.value).isNull()
+        assertThat(endNames).containsExactly("blueSquare")
+        assertThat(endViews).containsExactly(endBlue)
+        assertThat(endSnapshots).isNull()
 
         // Now pop the back stack
-        reset(enterCallback)
         verifyPopTransition(1, fragment2, fragment1)
 
-        verify(enterCallback).onSharedElementStart(
-            names.capture(), views.capture(),
-            snapshots.capture()
-        )
-        assertThat(names.value).containsExactly("blueSquare")
-        assertThat(views.value).containsExactly(endBlue)
-        assertThat(snapshots.value).isNull()
+        assertThat(startNames).containsExactly("blueSquare")
+        assertThat(startViews).containsExactly(endBlue)
+        assertThat(startSnapshots).isNull()
 
         val reenterBlue = activityRule.findBlue()
 
-        verify(enterCallback).onSharedElementEnd(
-            names.capture(), views.capture(),
-            snapshots.capture()
-        )
-        assertThat(names.value).containsExactly("blueSquare")
-        assertThat(views.value).containsExactly(reenterBlue)
-        assertThat(snapshots.value).isNull()
+        assertThat(endNames).containsExactly("blueSquare")
+        assertThat(endViews).containsExactly(reenterBlue)
+        assertThat(endSnapshots).isNull()
     }
 
     // Make sure that onMapSharedElement works to change the shared element going out
     @Test
     fun onMapSharedElementOut() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         // Now do a transition to scene2
@@ -580,6 +598,7 @@
     // Make sure that onMapSharedElement works to change the shared element target
     @Test
     fun onMapSharedElementIn() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         // Now do a transition to scene2
@@ -651,6 +670,7 @@
     // Ensure that shared element transitions that have targets properly target the views
     @Test
     fun complexSharedElementTransition() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         // Now do a transition to scene2
@@ -741,6 +761,7 @@
     // Ensure that transitions are done when a fragment is shown and hidden
     @Test
     fun showHideTransition() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
         val fragment2 = TransitionFragment(R.layout.scene2)
 
@@ -811,6 +832,7 @@
     // the enter transition until after the exit transition finishes
     @Test
     fun disallowEnterOverlap() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment = setupInitialFragment()
         val blue = activityRule.findBlue()
         val green = activityRule.findGreen()
@@ -871,6 +893,7 @@
     // Ensure that transitions are done when a fragment is attached and detached
     @Test
     fun attachDetachTransition() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
         val fragment2 = TransitionFragment(R.layout.scene2)
 
@@ -920,6 +943,7 @@
     // Ensure that shared element without matching transition name doesn't error out
     @Test
     fun sharedElementMismatch() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         // Now do a transition to scene2
@@ -957,6 +981,7 @@
     // Ensure that using the same source or target shared element results in an exception.
     @Test
     fun sharedDuplicateTargetNames() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         setupInitialFragment()
 
         val startBlue = activityRule.findBlue()
@@ -990,6 +1015,7 @@
     // Test that invisible fragment views don't participate in transitions
     @Test
     fun invisibleNoTransitions() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         if (reorderingAllowed is Ordered) {
             return // only reordered transitions can avoid interaction
         }
@@ -1028,6 +1054,7 @@
     // No crash when transitioning a shared element and there is no shared element transition.
     @Test
     fun noSharedElementTransition() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         val startBlue = activityRule.findBlue()
@@ -1111,6 +1138,7 @@
     // a pop
     @Test
     fun noSharedElementTransitionOnPop() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         val startBlue = activityRule.findBlue()
@@ -1171,6 +1199,7 @@
     // When there is no matching shared element, the transition name should not be changed
     @Test
     fun noMatchingSharedElementRetainName() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = setupInitialFragment()
 
         val startBlue = activityRule.findBlue()
@@ -1218,6 +1247,7 @@
     // Test to ensure fragments don't leak in the container's tags
     @Test
     fun leakingFragmentInTags() {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         // First set up scene1 which should not be in the back stack
         val fragment1 = TransitionFragment(R.layout.scene1)
 
@@ -1314,6 +1344,7 @@
     }
 
     private fun setupInitialFragment(): TransitionFragment {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val fragment1 = TransitionFragment(R.layout.scene1)
         fragmentManager.beginTransaction()
             .setReorderingAllowed(reorderingAllowed)
@@ -1341,6 +1372,7 @@
         to: TransitionFragment,
         sharedElementName: String
     ) {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val startOnBackStackChanged = onBackStackChangedTimes
         val startBlue = activityRule.findBlue()
         val startGreen = activityRule.findGreen()
@@ -1390,6 +1422,8 @@
         from1: TransitionFragment,
         from2: TransitionFragment
     ) {
+        val fragmentManager = activityRule.activity.supportFragmentManager
+
         val startNumOnBackStackChanged = onBackStackChangedTimes
         val changesPerOperation = if (reorderingAllowed is Reordered) 1 else 2
 
@@ -1530,6 +1564,7 @@
         to: TransitionFragment,
         vararg others: TransitionFragment
     ) {
+        val fragmentManager = activityRule.activity.supportFragmentManager
         val startOnBackStackChanged = onBackStackChangedTimes
         val startBlue = activityRule.findBlue()
         val startGreen = activityRule.findGreen()
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
index 9f26148..5031e9c 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentViewLifecycleTest.kt
@@ -463,19 +463,12 @@
 
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             val savedStateRegistryOwner = view.findViewTreeSavedStateRegistryOwner()!!
-            val savedStateLifecycle = savedStateRegistryOwner.lifecycle
             val savedStateRegistry = savedStateRegistryOwner.savedStateRegistry
-            savedStateLifecycle.addObserver(
-                LifecycleEventObserver { _, event ->
-                    if (event == Lifecycle.Event.ON_CREATE) {
-                        val restoredBundle = savedStateRegistry.consumeRestoredStateForKey(
-                            "savedState"
-                        )
-                        stateIsRestored = restoredBundle != null
-                        restoredState = restoredBundle?.getString("state")
-                    }
-                }
+            val restoredBundle = savedStateRegistry.consumeRestoredStateForKey(
+                "savedState"
             )
+            stateIsRestored = restoredBundle != null
+            restoredState = restoredBundle?.getString("state")
             savedStateRegistry.registerSavedStateProvider("savedState") {
                 stateIsSaved = true
                 Bundle().apply {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 0901d48..cbd53a2 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -320,6 +320,12 @@
         void onPreAttached() {
             mSavedStateRegistryController.performAttach();
             enableSavedStateHandles(Fragment.this);
+            // Restore the state immediately so that every lifecycle callback including
+            // onAttach() can safely access the state in the SavedStateRegistry
+            Bundle savedStateRegistryState = mSavedFragmentState != null
+                    ? mSavedFragmentState.getBundle(FragmentStateManager.REGISTRY_STATE_KEY)
+                    : null;
+            mSavedStateRegistryController.performRestore(savedStateRegistryState);
         }
     };
 
@@ -689,10 +695,6 @@
             mView.restoreHierarchyState(mSavedViewState);
             mSavedViewState = null;
         }
-        if (mView != null) {
-            mViewLifecycleOwner.performRestore(mSavedViewRegistryState);
-            mSavedViewRegistryState = null;
-        }
         mCalled = false;
         onViewStateRestored(savedInstanceState);
         if (!mCalled) {
@@ -3083,10 +3085,6 @@
                 }
             });
         }
-        Bundle savedStateRegistryState = mSavedFragmentState != null
-                ? mSavedFragmentState.getBundle(FragmentStateManager.REGISTRY_STATE_KEY)
-                : null;
-        mSavedStateRegistryController.performRestore(savedStateRegistryState);
         onCreate(savedInstanceState);
         mIsCreated = true;
         if (!mCalled) {
@@ -3100,7 +3098,13 @@
             @Nullable Bundle savedInstanceState) {
         mChildFragmentManager.noteStateNotSaved();
         mPerformedCreateView = true;
-        mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore());
+        mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore(),
+                () -> {
+                    // Perform the restore as soon as the FragmentViewLifecycleOwner
+                    // becomes initialized, to ensure it is always available
+                    mViewLifecycleOwner.performRestore(mSavedViewRegistryState);
+                    mSavedViewRegistryState = null;
+                });
         mView = onCreateView(inflater, container, savedInstanceState);
         if (mView != null) {
             // Initialize the view lifecycle
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
index 0f36877..6f416d8 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/FragmentViewLifecycleOwner.java
@@ -44,15 +44,21 @@
         SavedStateRegistryOwner, ViewModelStoreOwner {
     private final Fragment mFragment;
     private final ViewModelStore mViewModelStore;
+    private final Runnable mRestoreViewSavedStateRunnable;
 
     private ViewModelProvider.Factory mDefaultFactory;
 
     private LifecycleRegistry mLifecycleRegistry = null;
     private SavedStateRegistryController mSavedStateRegistryController = null;
 
-    FragmentViewLifecycleOwner(@NonNull Fragment fragment, @NonNull ViewModelStore viewModelStore) {
+    FragmentViewLifecycleOwner(
+            @NonNull Fragment fragment,
+            @NonNull ViewModelStore viewModelStore,
+            @NonNull Runnable restoreViewSavedStateRunnable
+    ) {
         mFragment = fragment;
         mViewModelStore = viewModelStore;
+        mRestoreViewSavedStateRunnable = restoreViewSavedStateRunnable;
     }
 
     @NonNull
@@ -71,6 +77,7 @@
             mSavedStateRegistryController = SavedStateRegistryController.create(this);
             mSavedStateRegistryController.performAttach();
             enableSavedStateHandles(this);
+            mRestoreViewSavedStateRunnable.run();
         }
     }
 
diff --git a/glance/glance-appwidget-proto/src/main/proto/layout.proto b/glance/glance-appwidget-proto/src/main/proto/layout.proto
index 08ef3e2..9685e81 100644
--- a/glance/glance-appwidget-proto/src/main/proto/layout.proto
+++ b/glance/glance-appwidget-proto/src/main/proto/layout.proto
@@ -84,4 +84,5 @@
   RADIO_BUTTON = 19;
   RADIO_ROW = 20;
   RADIO_COLUMN = 21;
+  SIZE_BOX = 22;
 }
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index bf3e575..6244413 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -13,6 +13,16 @@
   public final class AppWidgetModifiersKt {
   }
 
+  public fun interface AppWidgetProviderScope {
+    method public suspend Object? setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+  public final class AppWidgetSessionKt {
+  }
+
+  public final class AppWidgetUtilsKt {
+  }
+
   public final class ApplyModifiersKt {
   }
 
@@ -53,10 +63,13 @@
   public abstract class GlanceAppWidget {
     ctor public GlanceAppWidget(optional @LayoutRes int errorUiLayout);
     method @androidx.compose.runtime.Composable @androidx.glance.GlanceComposable public abstract void Content();
+    method public androidx.glance.session.SessionManager? getSessionManager();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
     method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? provideGlance(androidx.glance.appwidget.AppWidgetProviderScope, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public androidx.glance.session.SessionManager? sessionManager;
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
   }
@@ -133,6 +146,9 @@
   public final class RemoteViewsTranslatorKt {
   }
 
+  public final class SizeBoxKt {
+  }
+
   public sealed interface SizeMode {
   }
 
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 6934f35..27e2045 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -13,6 +13,16 @@
   public final class AppWidgetModifiersKt {
   }
 
+  public fun interface AppWidgetProviderScope {
+    method public suspend Object? setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+  public final class AppWidgetSessionKt {
+  }
+
+  public final class AppWidgetUtilsKt {
+  }
+
   public final class ApplyModifiersKt {
   }
 
@@ -56,10 +66,13 @@
   public abstract class GlanceAppWidget {
     ctor public GlanceAppWidget(optional @LayoutRes int errorUiLayout);
     method @androidx.compose.runtime.Composable @androidx.glance.GlanceComposable public abstract void Content();
+    method public androidx.glance.session.SessionManager? getSessionManager();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
     method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? provideGlance(androidx.glance.appwidget.AppWidgetProviderScope, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public androidx.glance.session.SessionManager? sessionManager;
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
   }
@@ -147,6 +160,9 @@
   public final class RemoteViewsTranslatorKt {
   }
 
+  public final class SizeBoxKt {
+  }
+
   public sealed interface SizeMode {
   }
 
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index bf3e575..6244413 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -13,6 +13,16 @@
   public final class AppWidgetModifiersKt {
   }
 
+  public fun interface AppWidgetProviderScope {
+    method public suspend Object? setContent(kotlin.jvm.functions.Function0<kotlin.Unit> content, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+  }
+
+  public final class AppWidgetSessionKt {
+  }
+
+  public final class AppWidgetUtilsKt {
+  }
+
   public final class ApplyModifiersKt {
   }
 
@@ -53,10 +63,13 @@
   public abstract class GlanceAppWidget {
     ctor public GlanceAppWidget(optional @LayoutRes int errorUiLayout);
     method @androidx.compose.runtime.Composable @androidx.glance.GlanceComposable public abstract void Content();
+    method public androidx.glance.session.SessionManager? getSessionManager();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
     method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    method public suspend Object? provideGlance(androidx.glance.appwidget.AppWidgetProviderScope, android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit>);
+    property public androidx.glance.session.SessionManager? sessionManager;
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
   }
@@ -133,6 +146,9 @@
   public final class RemoteViewsTranslatorKt {
   }
 
+  public final class SizeBoxKt {
+  }
+
   public sealed interface SizeMode {
   }
 
diff --git a/glance/glance-appwidget/build.gradle b/glance/glance-appwidget/build.gradle
index 09baaa6..c8aa9d9 100644
--- a/glance/glance-appwidget/build.gradle
+++ b/glance/glance-appwidget/build.gradle
@@ -74,6 +74,7 @@
     androidTestImplementation(project(":test:screenshot:screenshot"))
     androidTestImplementation("androidx.test.uiautomator:uiautomator:2.2.0")
     androidTestImplementation('androidx.core:core-ktx:1.7.0')
+    androidTestImplementation("androidx.work:work-testing:2.7.1")
     androidTestImplementation(libs.espressoCore)
     androidTestImplementation(libs.espressoIdlingResource)
     androidTestImplementation(libs.kotlinCoroutinesTest)
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
index 8639a2f..f2aa39f 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostRule.kt
@@ -17,44 +17,41 @@
 package androidx.glance.appwidget
 
 import android.Manifest
-import android.app.Activity
 import android.appwidget.AppWidgetHostView
 import android.appwidget.AppWidgetManager
 import android.content.Context
 import android.content.pm.ActivityInfo
-import android.os.Build
+import android.content.res.Configuration
 import android.view.View
 import android.view.ViewGroup
 import android.view.ViewTreeObserver
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import androidx.core.view.children
+import androidx.glance.session.GlanceSessionManager
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso.onView
-import androidx.test.espresso.UiController
-import androidx.test.espresso.ViewAction
-import androidx.test.espresso.matcher.ViewMatchers.isRoot
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry
-import androidx.test.runner.lifecycle.Stage
 import androidx.test.uiautomator.UiDevice
-import org.hamcrest.Matcher
-import org.junit.rules.RuleChain
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
+import androidx.work.WorkManager
+import androidx.work.testing.WorkManagerTestInitHelper
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlin.test.assertIs
 import kotlin.test.fail
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
 
 @SdkSuppress(minSdkVersion = 29)
 class AppWidgetHostRule(
     private var mPortraitSize: DpSize = DpSize(200.dp, 300.dp),
-    private var mLandscapeSize: DpSize = DpSize(300.dp, 200.dp)
+    private var mLandscapeSize: DpSize = DpSize(300.dp, 200.dp),
+    private var useSessionManager: Boolean = false,
 ) : TestRule {
 
     val portraitSize: DpSize
@@ -101,6 +98,9 @@
     override fun apply(base: Statement, description: Description) = object : Statement() {
 
         override fun evaluate() {
+            WorkManagerTestInitHelper.initializeTestWorkManager(mContext)
+            TestGlanceAppWidget.sessionManager =
+                if (useSessionManager) GlanceSessionManager else null
             mInnerRules.apply(base, description).evaluate()
             stopHost()
         }
@@ -109,6 +109,7 @@
             if (mHostStarted) {
                 mUiAutomation.dropShellPermissionIdentity()
             }
+            WorkManager.getInstance(mContext).cancelAllWork()
         }
     }
 
@@ -129,15 +130,32 @@
         }
     }
 
-    suspend fun updateAppWidget() {
+    /**
+     * Run the [block] (usually some sort of app widget update) and wait for new RemoteViews to be
+     * applied.
+     */
+    suspend fun runAndWaitForUpdate(block: suspend () -> Unit) {
         val hostView = checkNotNull(mMaybeHostView) { "Host view wasn't successfully started" }
         hostView.resetRemoteViewsLatch()
-        TestGlanceAppWidget.update(mContext, AppWidgetId(mAppWidgetId))
+        block()
         runAndWaitForChildren {
             hostView.waitForRemoteViews()
         }
     }
 
+    /**
+     * Set TestGlanceAppWidgetReceiver to ignore broadcasts, run [block], and then reset
+     * TestGlanceAppWidgetReceiver.
+     */
+    fun ignoreBroadcasts(block: () -> Unit) {
+        TestGlanceAppWidgetReceiver.ignoreBroadcasts = true
+        try {
+            block()
+        } finally {
+            TestGlanceAppWidgetReceiver.ignoreBroadcasts = false
+        }
+    }
+
     fun removeAppWidget() {
         mActivityRule.scenario.onActivity { activity ->
             val hostView = checkNotNull(mMaybeHostView) { "No app widget to remove" }
@@ -168,12 +186,30 @@
 
     /** Change the orientation to landscape.*/
     fun setLandscapeOrientation() {
-        onView(isRoot()).perform(orientationLandscape())
+        var activity: AppWidgetHostTestActivity? = null
+        onHostActivity {
+            it.resetConfigurationChangedLatch()
+            it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
+            activity = it
+        }
+        checkNotNull(activity).apply {
+            waitForConfigurationChange()
+            assertThat(lastConfiguration.orientation).isEqualTo(Configuration.ORIENTATION_LANDSCAPE)
+        }
     }
 
     /** Change the orientation to portrait.*/
     fun setPortraitOrientation() {
-        onView(isRoot()).perform(orientationPortrait())
+        var activity: AppWidgetHostTestActivity? = null
+        onHostActivity {
+            it.resetConfigurationChangedLatch()
+            it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+            activity = it
+        }
+        checkNotNull(activity).apply {
+            waitForConfigurationChange()
+            assertThat(lastConfiguration.orientation).isEqualTo(Configuration.ORIENTATION_PORTRAIT)
+        }
     }
 
     /**
@@ -263,46 +299,4 @@
             mHostView.childCount > 0
         }
     }
-
-    private inner class OrientationChangeAction constructor(private val orientation: Int) :
-        ViewAction {
-        override fun getConstraints(): Matcher<View> = isRoot()
-
-        override fun getDescription() = "change orientation to $orientationName"
-
-        private val orientationName: String
-            get() =
-                if (orientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {
-                    "landscape"
-                } else {
-                    "portrait"
-                }
-
-        override fun perform(uiController: UiController, view: View) {
-            uiController.loopMainThreadUntilIdle()
-            mActivityRule.scenario.onActivity { it.requestedOrientation = orientation }
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
-                // Somehow, before Android S, changing the orientation doesn't trigger the
-                // onConfigurationChange
-                uiController.loopMainThreadUntilIdle()
-                mScenario.onActivity {
-                    it.updateAllSizes(it.resources.configuration.orientation)
-                    it.reapplyRemoteViews()
-                }
-            }
-            val resumedActivities: Collection<Activity> =
-                ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED)
-            if (resumedActivities.isEmpty()) {
-                throw RuntimeException("Could not change orientation")
-            }
-        }
-    }
-
-    private fun orientationLandscape(): ViewAction {
-        return OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)
-    }
-
-    private fun orientationPortrait(): ViewAction {
-        return OrientationChangeAction(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)
-    }
 }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
index 8f98489..03f13ef 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/AppWidgetHostTestActivity.kt
@@ -46,6 +46,10 @@
 class AppWidgetHostTestActivity : Activity() {
     private var mHost: AppWidgetHost? = null
     private val mHostViews = mutableListOf<TestAppWidgetHostView>()
+    private var mConfigurationChanged: CountDownLatch? = null
+    private var mLastConfiguration: Configuration? = null
+    val lastConfiguration: Configuration
+        get() = synchronized(this) { mLastConfiguration!! }
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -115,16 +119,28 @@
 
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
-        updateAllSizes(newConfig.orientation)
-        reapplyRemoteViews()
+        mHostViews.forEach {
+            it.updateSize(newConfig.orientation)
+            it.reapplyRemoteViews()
+        }
+        synchronized(this) {
+            mLastConfiguration = newConfig
+            mConfigurationChanged?.countDown()
+        }
     }
 
-    fun updateAllSizes(orientation: Int) {
-        mHostViews.forEach { it.updateSize(orientation) }
+    fun resetConfigurationChangedLatch() {
+       synchronized(this) {
+           mConfigurationChanged = CountDownLatch(1)
+           mLastConfiguration = null
+       }
     }
 
-    fun reapplyRemoteViews() {
-        mHostViews.forEach { it.reapplyRemoteViews() }
+    // This should not be called from the main thread, so that it does not block
+    // onConfigurationChanged from being called.
+    fun waitForConfigurationChange() {
+        val result = mConfigurationChanged?.await(5, TimeUnit.SECONDS)!!
+        require(result) { "Timeout before getting configuration" }
     }
 }
 
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 20c2755..a3ca4e5 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -94,18 +94,28 @@
 import kotlin.test.assertIs
 import kotlin.test.assertNotNull
 import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertThrows
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @SdkSuppress(minSdkVersion = 29)
 @MediumTest
-class GlanceAppWidgetReceiverTest {
+@RunWith(Parameterized::class)
+class GlanceAppWidgetReceiverTest(useSessionManager: Boolean) {
     @get:Rule
-    val mHostRule = AppWidgetHostRule()
+    val mHostRule = AppWidgetHostRule(useSessionManager = useSessionManager)
 
     val context = InstrumentationRegistry.getInstrumentation().targetContext!!
 
+    companion object {
+        @Parameterized.Parameters(name = "useGlanceSession={0}")
+        @JvmStatic
+        fun data() = mutableListOf(true, false)
+    }
+
     @Before
     fun setUp() {
         // Reset the size mode to the default
@@ -543,64 +553,65 @@
     }
 
     @Test
-    fun updateAll() {
+    fun updateAll() = runBlocking {
         TestGlanceAppWidget.uiDefinition = {
-            Text("before")
+            Text("text")
         }
 
         mHostRule.startHost()
 
-        val didRun = AtomicBoolean(false)
-        TestGlanceAppWidget.uiDefinition = {
-            didRun.set(true)
-            Text("after")
-        }
-
-        runBlocking {
+        mHostRule.runAndWaitForUpdate {
             TestGlanceAppWidget.updateAll(context)
         }
-        assertThat(didRun.get()).isTrue()
     }
 
     @Test
-    fun updateIf() {
+    fun updateIf() = runBlocking {
+        val didRun = AtomicBoolean(false)
         TestGlanceAppWidget.uiDefinition = {
-            Text("before")
+            currentState<Preferences>()
+            didRun.set(true)
+            Text("text")
         }
 
         mHostRule.startHost()
+        assertThat(didRun.get()).isTrue()
 
-        val appWidgetManager = GlanceAppWidgetManager(context)
-        runBlocking {
-            appWidgetManager.getGlanceIds(TestGlanceAppWidget::class.java)
-                .forEach { glanceId ->
-                    updateAppWidgetState(context, glanceId) {
-                        it[testKey] = 2
-                    }
+        GlanceAppWidgetManager(context)
+            .getGlanceIds(TestGlanceAppWidget::class.java)
+            .forEach { glanceId ->
+                updateAppWidgetState(context, glanceId) {
+                    it[testKey] = 2
                 }
-        }
+            }
 
         // Make sure the app widget is updated if the test is true
-        val didRun = AtomicBoolean(false)
-        TestGlanceAppWidget.uiDefinition = {
-            didRun.set(true)
-            Text("after")
-        }
-        runBlocking {
+        didRun.set(false)
+        mHostRule.runAndWaitForUpdate {
             TestGlanceAppWidget.updateIf<Preferences>(context) { prefs ->
                 prefs[testKey] == 2
             }
         }
-
         assertThat(didRun.get()).isTrue()
 
         // Make sure it is not if the test is false
         didRun.set(false)
-        runBlocking {
-            TestGlanceAppWidget.updateIf<Preferences>(context) { prefs ->
-                prefs[testKey] == 3
+
+        // Waiting for the update should timeout since it is never triggered.
+        val exception = assertThrows(IllegalArgumentException::class.java) {
+            // AppWidgetService may send an APPWIDGET_UPDATE broadcast, which is not relevant to
+            // this and should be ignored.
+            mHostRule.ignoreBroadcasts {
+                runBlocking {
+                    mHostRule.runAndWaitForUpdate {
+                        TestGlanceAppWidget.updateIf<Preferences>(context) { prefs ->
+                            prefs[testKey] == 3
+                        }
+                    }
+                }
             }
         }
+        assertThat(exception).hasMessageThat().contains("Timeout before getting RemoteViews")
 
         assertThat(didRun.get()).isFalse()
     }
@@ -608,7 +619,7 @@
     @Test
     fun viewState() {
         TestGlanceAppWidget.uiDefinition = {
-            val value = currentState<Preferences>()[testKey] ?: -1
+            val value = currentState(testKey) ?: -1
             Text("Value = $value")
         }
 
@@ -810,7 +821,9 @@
 
         mHostRule.startHost()
         runBlocking {
-            mHostRule.updateAppWidget()
+            mHostRule.runAndWaitForUpdate {
+                TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
+            }
         }
 
         // if no crash, we're good
@@ -870,7 +883,9 @@
             updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
                 it[testBoolKey] = false
             }
-            mHostRule.updateAppWidget()
+            mHostRule.runAndWaitForUpdate {
+                TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
+            }
         }
 
         mHostRule.onHostView { root ->
@@ -914,7 +929,9 @@
             updateAppWidgetState(context, AppWidgetId(mHostRule.appWidgetId)) {
                 it[testBoolKey] = false
             }
-            mHostRule.updateAppWidget()
+            mHostRule.runAndWaitForUpdate {
+                TestGlanceAppWidget.update(context, AppWidgetId(mHostRule.appWidgetId))
+            }
         }
 
         CompoundButtonActionTest.received.set(emptyList())
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index 9a750be5..d2832ae 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -17,14 +17,29 @@
 package androidx.glance.appwidget
 
 import android.content.Context
+import android.content.Intent
+import android.util.Log
 import androidx.compose.runtime.Composable
 import androidx.glance.GlanceId
+import androidx.glance.session.SessionManager
 
 class TestGlanceAppWidgetReceiver : GlanceAppWidgetReceiver() {
     override val glanceAppWidget: GlanceAppWidget = TestGlanceAppWidget
+    companion object {
+        var ignoreBroadcasts = false
+    }
+
+    override fun onReceive(context: Context, intent: Intent) {
+        if (ignoreBroadcasts) {
+            Log.w("TestGlanceAppWidgetReceiver", "Ignored $intent")
+            return
+        }
+        super.onReceive(context, intent)
+    }
 }
 
 object TestGlanceAppWidget : GlanceAppWidget(errorUiLayout = 0) {
+    override var sessionManager: SessionManager? = null
 
     override var sizeMode: SizeMode = SizeMode.Single
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
new file mode 100644
index 0000000..2250fbc
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetSession.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.glance.appwidget
+
+import android.appwidget.AppWidgetManager
+import android.content.Context
+import android.os.Bundle
+import android.util.Log
+import android.widget.RemoteViews
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.neverEqualPolicy
+import androidx.compose.ui.unit.DpSize
+import androidx.datastore.preferences.core.emptyPreferences
+import androidx.glance.EmittableWithChildren
+import androidx.glance.GlanceComposable
+import androidx.glance.LocalContext
+import androidx.glance.LocalGlanceId
+import androidx.glance.LocalState
+import androidx.glance.session.Session
+import androidx.glance.session.SetContentFn
+import androidx.glance.state.ConfigManager
+import androidx.glance.state.GlanceState
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import java.util.concurrent.CancellationException
+
+/**
+ * A session that composes UI for a single app widget.
+ *
+ * This class represents the lifecycle of composition for an app widget. This is started by
+ * [GlanceAppWidget] in response to APPWIDGET_UPDATE broadcasts. The session is run in
+ * [androidx.glance.session.SessionWorker] on a background thread (WorkManager). While it is active,
+ * the session will continue to recompose in response to composition state changes or external
+ * events (e.g. [AppWidgetSession.updateGlance]). If a session is already running, GlanceAppWidget
+ * will trigger events on the session instead of starting a new one.
+ *
+ * @property widget the GlanceAppWidget that contains the composable for this session.
+ * @property id identifies which widget will be updated when the UI is ready.
+ * @property initialOptions options to be provided to the composition and determine sizing.
+ * @property configManager used by the session to retrieve configuration state.
+ */
+internal class AppWidgetSession(
+    private val widget: GlanceAppWidget,
+    private val id: AppWidgetId,
+    private val initialOptions: Bundle? = null,
+    private val configManager: ConfigManager = GlanceState,
+) : Session(id.toSessionKey()) {
+
+    private companion object {
+        const val TAG = "AppWidgetSession"
+        const val DEBUG = false
+    }
+
+    private val glanceState = mutableStateOf(emptyPreferences(), neverEqualPolicy())
+    private val options = mutableStateOf(Bundle(), neverEqualPolicy())
+    @VisibleForTesting
+    internal var lastRemoteViews: RemoteViews? = null
+
+    override fun createRootEmittable() = RemoteViewsRoot(MaxComposeTreeDepth)
+
+    override suspend fun provideGlance(
+        context: Context,
+        setContent: SetContentFn,
+    ) {
+        val manager = context.appWidgetManager
+        val minSize = appWidgetMinSize(
+            context.resources.displayMetrics,
+            manager,
+            id.appWidgetId
+        )
+        options.value = initialOptions ?: manager.getAppWidgetOptions(id.appWidgetId)!!
+        glanceState.value =
+            configManager.getValue(context, PreferencesGlanceStateDefinition, key)
+
+        val scope = AppWidgetProviderScope { content: @Composable @GlanceComposable () -> Unit ->
+            setContent {
+                CompositionLocalProvider(
+                    LocalContext provides context,
+                    LocalGlanceId provides id,
+                    LocalAppWidgetOptions provides options.value,
+                    LocalState provides glanceState.value,
+                ) {
+                    ForEachSize(widget.sizeMode, minSize, content)
+                }
+            }
+        }
+
+        widget.apply {
+            scope.provideGlance(context, id)
+        }
+    }
+
+    override suspend fun processEmittableTree(
+        context: Context,
+        root: EmittableWithChildren
+    ) {
+        root as RemoteViewsRoot
+        val layoutConfig = LayoutConfiguration.load(context, id.appWidgetId)
+        try {
+            normalizeCompositionTree(root)
+            val rv = translateComposition(
+                context,
+                id.appWidgetId,
+                root,
+                layoutConfig,
+                layoutConfig.addLayout(root),
+                DpSize.Unspecified
+            )
+            context.appWidgetManager.updateAppWidget(id.appWidgetId, rv)
+            lastRemoteViews = rv
+        } catch (ex: CancellationException) {
+            // Nothing to do
+        } catch (throwable: Throwable) {
+            if (widget.errorUiLayout == 0) {
+                throw throwable
+            }
+            logException(throwable)
+            val rv = RemoteViews(context.packageName, widget.errorUiLayout)
+            context.appWidgetManager.updateAppWidget(id.appWidgetId, rv)
+            lastRemoteViews = rv
+        } finally {
+            layoutConfig.save()
+        }
+    }
+
+    override suspend fun processEvent(context: Context, event: Any) {
+        when (event) {
+            is UpdateGlanceState -> {
+                if (DEBUG) Log.i(TAG, "Received UpdateGlanceState event for session($key)")
+                glanceState.value =
+                    configManager.getValue(context, PreferencesGlanceStateDefinition, key)
+            }
+            is UpdateAppWidgetOptions -> {
+                if (DEBUG) {
+                    Log.i(
+                        TAG,
+                        "Received UpdateAppWidgetOptions(${event.newOptions}) event" +
+                            "for session($key)"
+                    )
+                }
+                options.value = event.newOptions
+            }
+            else -> {
+                throw IllegalArgumentException(
+                    "Sent unrecognized event type ${event.javaClass} to AppWidgetSession"
+                )
+            }
+        }
+    }
+
+    suspend fun updateGlance() {
+        sendEvent(UpdateGlanceState)
+    }
+
+    suspend fun updateAppWidgetOptions(newOptions: Bundle) {
+        sendEvent(UpdateAppWidgetOptions(newOptions))
+    }
+
+    // Action types that this session supports.
+    @VisibleForTesting
+    internal object UpdateGlanceState
+
+    @VisibleForTesting
+    internal class UpdateAppWidgetOptions(val newOptions: Bundle)
+
+    private val Context.appWidgetManager: AppWidgetManager
+        get() = this.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
+}
+
+internal fun createUniqueRemoteUiName(appWidgetId: Int) = "appWidget-$appWidgetId"
+internal fun AppWidgetId.toSessionKey() = createUniqueRemoteUiName(appWidgetId)
+
+/**
+ * Maximum depth for a composition. Although there is no hard limit, this should avoid deep
+ * recursions, which would create [RemoteViews] too large to be sent.
+ */
+private const val MaxComposeTreeDepth = 50
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt
new file mode 100644
index 0000000..e9063f8a
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/AppWidgetUtils.kt
@@ -0,0 +1,151 @@
+/*
+ * 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.glance.appwidget
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.os.Bundle
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.SizeF
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import kotlin.math.ceil
+import kotlin.math.min
+
+// Retrieves the minimum size of an App Widget, as configured by the App Widget provider.
+internal fun appWidgetMinSize(
+    displayMetrics: DisplayMetrics,
+    appWidgetManager: AppWidgetManager,
+    appWidgetId: Int
+): DpSize {
+    val info = appWidgetManager.getAppWidgetInfo(appWidgetId) ?: return DpSize.Zero
+    val minWidth = min(
+        info.minWidth,
+        if (info.resizeMode and AppWidgetProviderInfo.RESIZE_HORIZONTAL != 0) {
+            info.minResizeWidth
+        } else {
+            Int.MAX_VALUE
+        }
+    )
+    val minHeight = min(
+        info.minHeight,
+        if (info.resizeMode and AppWidgetProviderInfo.RESIZE_VERTICAL != 0) {
+            info.minResizeHeight
+        } else {
+            Int.MAX_VALUE
+        }
+    )
+    return DpSize(minWidth.pixelsToDp(displayMetrics), minHeight.pixelsToDp(displayMetrics))
+}
+
+// Extract the sizes from the bundle
+@Suppress("DEPRECATION")
+internal fun Bundle.extractAllSizes(minSize: () -> DpSize): List<DpSize> {
+    val sizes = getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
+    return if (sizes.isNullOrEmpty()) {
+        estimateSizes(minSize)
+    } else {
+        sizes.map { DpSize(it.width.dp, it.height.dp) }
+    }
+}
+
+// If the list of sizes is not available, estimate it from the min/max width and height.
+// We can assume that the min width and max height correspond to the portrait mode and the max
+// width / min height to the landscape mode.
+private fun Bundle.estimateSizes(minSize: () -> DpSize): List<DpSize> {
+    val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
+    val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
+    val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
+    val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
+    // If the min / max widths and heights are not specified, fall back to the unique mode,
+    // giving the minimum size the app widget may have.
+    if (minHeight == 0 || maxHeight == 0 || minWidth == 0 || maxWidth == 0) {
+        return listOf(minSize())
+    }
+    return listOf(DpSize(minWidth.dp, maxHeight.dp), DpSize(maxWidth.dp, minHeight.dp))
+}
+
+// Landscape is min height / max width
+private fun Bundle.extractLandscapeSize(): DpSize? {
+    val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
+    val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
+    return if (minHeight == 0 || maxWidth == 0) null else DpSize(maxWidth.dp, minHeight.dp)
+}
+
+// Portrait is max height / min width
+private fun Bundle.extractPortraitSize(): DpSize? {
+    val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
+    val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
+    return if (maxHeight == 0 || minWidth == 0) null else DpSize(minWidth.dp, maxHeight.dp)
+}
+
+internal fun Bundle.extractOrientationSizes() =
+    listOfNotNull(extractLandscapeSize(), extractPortraitSize())
+
+// True if the object fits in the given size.
+private infix fun DpSize.fitsIn(other: DpSize) =
+    (ceil(other.width.value) + 1 > width.value) &&
+        (ceil(other.height.value) + 1 > height.value)
+
+internal fun DpSize.toSizeF(): SizeF = SizeF(width.value, height.value)
+
+private fun squareDistance(widgetSize: DpSize, layoutSize: DpSize): Float {
+    val dw = widgetSize.width.value - layoutSize.width.value
+    val dh = widgetSize.height.value - layoutSize.height.value
+    return dw * dw + dh * dh
+}
+
+// Find the best size that fits in the available [widgetSize] or null if no layout fits.
+internal fun findBestSize(widgetSize: DpSize, layoutSizes: Collection<DpSize>): DpSize? =
+    layoutSizes.mapNotNull { layoutSize ->
+        if (layoutSize fitsIn widgetSize) {
+            layoutSize to squareDistance(widgetSize, layoutSize)
+        } else {
+            null
+        }
+    }.minByOrNull { it.second }?.first
+
+/**
+ * @return the minimum size as configured by the App Widget provider.
+ */
+internal fun AppWidgetProviderInfo.getMinSize(displayMetrics: DisplayMetrics): DpSize {
+    val minWidth = min(
+        minWidth,
+        if (resizeMode and AppWidgetProviderInfo.RESIZE_HORIZONTAL != 0) {
+            minResizeWidth
+        } else {
+            Int.MAX_VALUE
+        }
+    )
+    val minHeight = min(
+        minHeight,
+        if (resizeMode and AppWidgetProviderInfo.RESIZE_VERTICAL != 0) {
+            minResizeHeight
+        } else {
+            Int.MAX_VALUE
+        }
+    )
+    return DpSize(minWidth.pixelsToDp(displayMetrics), minHeight.pixelsToDp(displayMetrics))
+}
+
+internal fun Collection<DpSize>.sortedBySize() =
+    sortedWith(compareBy({ it.width.value * it.height.value }, { it.width.value }))
+
+internal fun logException(throwable: Throwable) {
+    Log.e(GlanceAppWidgetTag, "Error in Glance App Widget", throwable)
+}
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
index 89529bc..45be98b 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/CompositionLocals.kt
@@ -18,7 +18,7 @@
 
 import android.os.Bundle
 import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.staticCompositionLocalOf
+import androidx.compose.runtime.compositionLocalOf
 
 /**
  * Option Bundle accessible when generating an App Widget.
@@ -26,4 +26,4 @@
  * See [AppWidgetManager#getAppWidgetOptions] for details
  */
 val LocalAppWidgetOptions: ProvidableCompositionLocal<Bundle> =
-    staticCompositionLocalOf { error("No default app widget options") }
+    compositionLocalOf { Bundle() }
\ No newline at end of file
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index 51535cd..56938e9 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -17,13 +17,10 @@
 package androidx.glance.appwidget
 
 import android.appwidget.AppWidgetManager
-import android.appwidget.AppWidgetProviderInfo
 import android.content.Context
 import android.os.Build
 import android.os.Bundle
-import android.util.DisplayMetrics
 import android.util.Log
-import android.util.SizeF
 import android.widget.RemoteViews
 import androidx.annotation.DoNotInline
 import androidx.annotation.LayoutRes
@@ -35,7 +32,6 @@
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Recomposer
 import androidx.compose.ui.unit.DpSize
-import androidx.compose.ui.unit.dp
 import androidx.glance.Applier
 import androidx.glance.GlanceComposable
 import androidx.glance.GlanceId
@@ -44,11 +40,10 @@
 import androidx.glance.LocalSize
 import androidx.glance.LocalState
 import androidx.glance.appwidget.state.getAppWidgetState
+import androidx.glance.session.SessionManager
 import androidx.glance.state.GlanceState
 import androidx.glance.state.GlanceStateDefinition
 import androidx.glance.state.PreferencesGlanceStateDefinition
-import kotlin.math.ceil
-import kotlin.math.min
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -59,6 +54,15 @@
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 
+fun interface AppWidgetProviderScope {
+    /**
+     * Provides [content] to the Glance host, suspending until the Glance session is
+     * shut down. If this method is called concurrently with itself, the previous
+     * call will throw [CancellationException] and the new content will replace it.
+     */
+    suspend fun setContent(content: @Composable @GlanceComposable () -> Unit)
+}
+
 /**
  * Object handling the composition and the communication with [AppWidgetManager].
  *
@@ -71,10 +75,27 @@
  */
 abstract class GlanceAppWidget(
     @LayoutRes
-    private val errorUiLayout: Int = R.layout.glance_error_layout
+    internal val errorUiLayout: Int = R.layout.glance_error_layout,
 ) {
     /**
+     * Override this function to provide the Glance Composable.
+     *
+     * This is a good place to load any data needed to render the Composable. Use
+     * [AppWidgetProviderScope.setContent] to provide the Composable once it is ready.
+     *
+     * TODO(b/239747024) make abstract once Content() is removed.
+     */
+    @Suppress("UNUSED_PARAMETER")
+    open suspend fun AppWidgetProviderScope.provideGlance(
+        @Suppress("ContextFirst") context: Context,
+        glanceId: GlanceId,
+    ) {
+        setContent { Content() }
+    }
+
+    /**
      * Definition of the UI.
+     * TODO(b/239747024) remove and update any usage to the new provideGlance API.
      */
     @Composable
     @GlanceComposable
@@ -97,6 +118,9 @@
      */
     open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
 
+    // TODO(b/239747024) remove once SessionManager is the default
+    open val sessionManager: SessionManager? = null
+
     /**
      * Triggers the composition of [Content] and sends the result to the [AppWidgetManager].
      */
@@ -114,6 +138,7 @@
      */
     internal suspend fun deleted(context: Context, appWidgetId: Int) {
         val glanceId = AppWidgetId(appWidgetId)
+        sessionManager?.closeSession(glanceId.toSessionKey())
         try {
             onDelete(context, glanceId)
         } catch (cancelled: CancellationException) {
@@ -136,6 +161,16 @@
         appWidgetId: Int,
         options: Bundle? = null,
     ) {
+        sessionManager?.let {
+            val glanceId = AppWidgetId(appWidgetId)
+            if (!it.isSessionRunning(context, glanceId.toSessionKey())) {
+                it.startSession(context, AppWidgetSession(this, glanceId, options))
+            } else {
+                val session = it.getSession(glanceId.toSessionKey()) as AppWidgetSession
+                session.updateGlance()
+            }
+            return
+        }
         safeRun(context, appWidgetManager, appWidgetId) {
             val opts = options ?: appWidgetManager.getAppWidgetOptions(appWidgetId)!!
             val state = stateDefinition?.let {
@@ -157,6 +192,16 @@
         appWidgetId: Int,
         options: Bundle
     ) {
+        sessionManager?.let { manager ->
+            val glanceId = AppWidgetId(appWidgetId)
+            if (!manager.isSessionRunning(context, glanceId.toSessionKey())) {
+                manager.startSession(context, AppWidgetSession(this, glanceId, options))
+            } else {
+                val session = manager.getSession(glanceId.toSessionKey()) as AppWidgetSession
+                session.updateAppWidgetOptions(options)
+            }
+            return
+        }
         // Note, on Android S, if the mode is `Responsive`, then all the sizes are specified from
         // the start and we don't need to update the AppWidget when the size changes.
         if (sizeMode is SizeMode.Exact ||
@@ -166,17 +211,6 @@
         }
     }
 
-    // Retrieves the minimum size of an App Widget, as configured by the App Widget provider.
-    @VisibleForTesting
-    internal fun appWidgetMinSize(
-        displayMetrics: DisplayMetrics,
-        appWidgetManager: AppWidgetManager,
-        appWidgetId: Int
-    ): DpSize {
-        val info = appWidgetManager.getAppWidgetInfo(appWidgetId) ?: return DpSize.Zero
-        return info.getMinSize(displayMetrics)
-    }
-
     // Trigger the composition of the View to create the RemoteViews.
     @VisibleForTesting
     internal suspend fun compose(
@@ -486,109 +520,8 @@
     }
 }
 
-internal fun createUniqueRemoteUiName(appWidgetId: Int) = "appWidget-$appWidgetId"
-
 internal data class AppWidgetId(val appWidgetId: Int) : GlanceId
 
-// Extract the sizes from the bundle
-@Suppress("DEPRECATION")
-internal fun Bundle.extractAllSizes(minSize: () -> DpSize): List<DpSize> {
-    val sizes = getParcelableArrayList<SizeF>(AppWidgetManager.OPTION_APPWIDGET_SIZES)
-    return if (sizes.isNullOrEmpty()) {
-        estimateSizes(minSize)
-    } else {
-        sizes.map { DpSize(it.width.dp, it.height.dp) }
-    }
-}
-
-// If the list of sizes is not available, estimate it from the min/max width and height.
-// We can assume that the min width and max height correspond to the portrait mode and the max
-// width / min height to the landscape mode.
-private fun Bundle.estimateSizes(minSize: () -> DpSize): List<DpSize> {
-    val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
-    val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
-    val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
-    val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
-    // If the min / max widths and heights are not specified, fall back to the unique mode,
-    // giving the minimum size the app widget may have.
-    if (minHeight == 0 || maxHeight == 0 || minWidth == 0 || maxWidth == 0) {
-        return listOf(minSize())
-    }
-    return listOf(DpSize(minWidth.dp, maxHeight.dp), DpSize(maxWidth.dp, minHeight.dp))
-}
-
-// Landscape is min height / max width
-private fun Bundle.extractLandscapeSize(): DpSize? {
-    val minHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0)
-    val maxWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0)
-    return if (minHeight == 0 || maxWidth == 0) null else DpSize(maxWidth.dp, minHeight.dp)
-}
-
-// Portrait is max height / min width
-private fun Bundle.extractPortraitSize(): DpSize? {
-    val maxHeight = getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0)
-    val minWidth = getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0)
-    return if (maxHeight == 0 || minWidth == 0) null else DpSize(minWidth.dp, maxHeight.dp)
-}
-
-private fun Bundle.extractOrientationSizes() =
-    listOfNotNull(extractLandscapeSize(), extractPortraitSize())
-
-// True if the object fits in the given size.
-private infix fun DpSize.fitsIn(other: DpSize) =
-    (ceil(other.width.value) + 1 > width.value) &&
-        (ceil(other.height.value) + 1 > height.value)
-
-@VisibleForTesting
-internal fun DpSize.toSizeF(): SizeF = SizeF(width.value, height.value)
-
-private fun squareDistance(widgetSize: DpSize, layoutSize: DpSize): Float {
-    val dw = widgetSize.width.value - layoutSize.width.value
-    val dh = widgetSize.height.value - layoutSize.height.value
-    return dw * dw + dh * dh
-}
-
-// Find the best size that fits in the available [widgetSize] or null if no layout fits.
-@VisibleForTesting
-internal fun findBestSize(widgetSize: DpSize, layoutSizes: Collection<DpSize>): DpSize? =
-    layoutSizes.mapNotNull { layoutSize ->
-        if (layoutSize fitsIn widgetSize) {
-            layoutSize to squareDistance(widgetSize, layoutSize)
-        } else {
-            null
-        }
-    }.minByOrNull { it.second }?.first
-
-/**
- * @return the minimum size as configured by the App Widget provider.
- */
-internal fun AppWidgetProviderInfo.getMinSize(displayMetrics: DisplayMetrics): DpSize {
-    val minWidth = min(
-        minWidth,
-        if (resizeMode and AppWidgetProviderInfo.RESIZE_HORIZONTAL != 0) {
-            minResizeWidth
-        } else {
-            Int.MAX_VALUE
-        }
-    )
-    val minHeight = min(
-        minHeight,
-        if (resizeMode and AppWidgetProviderInfo.RESIZE_VERTICAL != 0) {
-            minResizeHeight
-        } else {
-            Int.MAX_VALUE
-        }
-    )
-    return DpSize(minWidth.pixelsToDp(displayMetrics), minHeight.pixelsToDp(displayMetrics))
-}
-
-private fun Collection<DpSize>.sortedBySize() =
-    sortedWith(compareBy({ it.width.value * it.height.value }, { it.width.value }))
-
-internal fun logException(throwable: Throwable) {
-    Log.e(GlanceAppWidgetTag, "Error in Glance App Widget", throwable)
-}
-
 /** Update all App Widgets managed by the [GlanceAppWidget] class. */
 suspend fun GlanceAppWidget.updateAll(@Suppress("ContextFirst") context: Context) {
     val manager = GlanceAppWidgetManager(context)
@@ -609,4 +542,4 @@
         val state = getAppWidgetState(context, stateDef, glanceId) as State
         if (predicate(state)) update(context, glanceId)
     }
-}
\ No newline at end of file
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
index 50f937e..a191446 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/NormalizeCompositionTree.kt
@@ -52,8 +52,28 @@
     }
 }
 
+/**
+ * Ensure that [container] has only one direct child.
+ *
+ * If [container] has multiple children, wrap them in an [EmittableBox] and make that the only child
+ * of container. If [container] contains only children of type [EmittableSizeBox], then we will make
+ * sure each of the [EmittableSizeBox]es has one child by wrapping their children in an
+ * [EmittableBox].
+ */
 private fun coerceToOneChild(container: EmittableWithChildren) {
-    if (container.children.size == 1) return
+    if (container.children.isNotEmpty() && container.children.all { it is EmittableSizeBox }) {
+        for (item in container.children) {
+            item as EmittableSizeBox
+            if (item.children.size == 1) continue
+            val box = EmittableBox()
+            box.children += item.children
+            item.children.clear()
+            item.children += box
+        }
+        return
+    } else if (container.children.size == 1) {
+        return
+    }
     val box = EmittableBox()
     box.children += container.children
     container.children.clear()
@@ -117,10 +137,9 @@
  * convert the target emittable to an [EmittableText]
  */
 private fun Emittable.transformBackgroundImageAndActionRipple(): Emittable {
-    // EmittableLazyListItem is a wrapper for its immediate only child,
-    // and does not get translated to its own element. We will transform
-    // the child instead.
-    if (this is EmittableLazyListItem) return this
+    // EmittableLazyListItem and EmittableSizeBox are wrappers for their immediate only child,
+    // and do not get translated to their own element. We will transform their child instead.
+    if (this is EmittableLazyListItem || this is EmittableSizeBox) return this
 
     var target = this
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
index b347e36..9f0ec001 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/RemoteViewsTranslator.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.os.Build
 import android.util.Log
+import android.util.SizeF
 import android.view.Gravity
 import android.view.View
 import android.widget.RemoteViews
@@ -87,22 +88,59 @@
     get() = forceRtl
         ?: (resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL)
 
+@RequiresApi(Build.VERSION_CODES.S)
+private object Api31Impl {
+    @DoNotInline
+    fun createRemoteViews(sizeMap: Map<SizeF, RemoteViews>): RemoteViews = RemoteViews(sizeMap)
+}
+
 internal fun translateComposition(
     translationContext: TranslationContext,
     children: List<Emittable>,
     rootViewIndex: Int
 ): RemoteViews {
-    require(children.size == 1) {
-        "The root of the tree must have exactly one child. " +
-            "The normalization of the composition tree failed."
+    if (children.all { it is EmittableSizeBox }) {
+        // If the children of root are all EmittableSizeBoxes, then we must translate each
+        // EmittableSizeBox into a distinct RemoteViews object. Then, we combine them into one
+        // multi-sized RemoteViews (a RemoteViews that contains either landscape & portrait RVs or
+        // multiple RVs mapped by size).
+        val sizeMode = (children.first() as EmittableSizeBox).sizeMode
+        val views = children.map { child ->
+            val size = (child as EmittableSizeBox).size
+            val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
+            val rv = remoteViewsInfo.remoteViews.apply {
+                translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
+            }
+            size.toSizeF() to rv
+        }
+        return when (sizeMode) {
+            is SizeMode.Single -> views.single().second
+            is SizeMode.Responsive, SizeMode.Exact -> {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                    Api31Impl.createRemoteViews(views.toMap())
+                } else {
+                    require(views.size == 1 || views.size == 2)
+                    combineLandscapeAndPortrait(views.map { it.second })
+                }
+            }
+        }
+    } else {
+        return children.single().let { child ->
+            val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
+            remoteViewsInfo.remoteViews.apply {
+                translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
+            }
+        }
     }
-    val child = children.first()
-    val remoteViewsInfo = createRootView(translationContext, child.modifier, rootViewIndex)
-    val rv = remoteViewsInfo.remoteViews
-    rv.translateChild(translationContext.forRoot(root = remoteViewsInfo), child)
-    return rv
 }
 
+private fun combineLandscapeAndPortrait(views: List<RemoteViews>): RemoteViews =
+    when (views.size) {
+        2 -> RemoteViews(views[0], views[1])
+        1 -> views[0]
+        else -> throw IllegalArgumentException("There must be between 1 and 2 views.")
+    }
+
 internal data class TranslationContext(
     val context: Context,
     val appWidgetId: Int,
@@ -126,6 +164,10 @@
 
     fun forRoot(root: RemoteViewsInfo): TranslationContext =
         forChild(pos = 0, parent = root.view)
+            .copy(
+                isBackgroundSpecified = AtomicBoolean(false),
+                lastViewId = AtomicInteger(0),
+            )
 
     fun resetViewId(newViewId: Int = 0) = copy(lastViewId = AtomicInteger(newViewId))
 
@@ -172,6 +214,7 @@
           translateEmittableLazyVerticalGridListItem(translationContext, element)
         }
         is EmittableRadioButton -> translateEmittableRadioButton(translationContext, element)
+        is EmittableSizeBox -> translateEmittableSizeBox(translationContext, element)
         else -> {
             throw IllegalArgumentException(
                 "Unknown element type ${element.javaClass.canonicalName}"
@@ -180,6 +223,17 @@
     }
 }
 
+internal fun RemoteViews.translateEmittableSizeBox(
+    translationContext: TranslationContext,
+    element: EmittableSizeBox
+) {
+    require(element.children.size <= 1) {
+        "Size boxes can only have at most one child ${element.children.size}. " +
+            "The normalization of the composition tree failed."
+    }
+    element.children.firstOrNull()?.let { translateChild(translationContext, it) }
+}
+
 internal fun remoteViews(translationContext: TranslationContext, @LayoutRes layoutId: Int) =
     RemoteViews(translationContext.context.packageName, layoutId)
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/SizeBox.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/SizeBox.kt
new file mode 100644
index 0000000..8425999
--- /dev/null
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/SizeBox.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget
+
+import android.os.Build
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.unit.DpSize
+import androidx.glance.Emittable
+import androidx.glance.EmittableWithChildren
+import androidx.glance.GlanceModifier
+import androidx.glance.GlanceNode
+import androidx.glance.LocalSize
+import androidx.glance.layout.fillMaxSize
+
+/**
+ * A marker for the translator that indicates that this [EmittableSizeBox] and its children should
+ * be translated into a distinct [android.widget.RemoteViews] object.
+ *
+ * EmittableSizeBox is only functional when it is a direct child of the root [RemoteViewsRoot].
+ * Multiple EmittableSizeBox children will each be translated into a distinct RemoteViews, then
+ * combined into one multi-sized RemoteViews.
+ */
+internal class EmittableSizeBox : EmittableWithChildren() {
+    override var modifier: GlanceModifier
+        get() = children.singleOrNull()?.modifier
+            ?: GlanceModifier.fillMaxSize()
+        set(_) {
+            throw IllegalAccessError("You cannot set the modifier of an EmittableSizeBox")
+        }
+    var size: DpSize = DpSize.Unspecified
+    var sizeMode: SizeMode = SizeMode.Single
+
+    override fun copy(): Emittable = EmittableSizeBox().also {
+        it.size = size
+        it.sizeMode = sizeMode
+        it.children.addAll(children.map { it.copy() })
+    }
+
+    override fun toString(): String = "EmittableSizeBox(" +
+        "size=$size, " +
+        "sizeMode=$sizeMode, " +
+        "children=[\n${childrenToString()}\n]" +
+        ")"
+}
+
+/**
+ * This composable emits a marker that lets the translator know that this [SizeBox] and its children
+ * should be translated into a distinct RemoteViews that is then combined with its siblings to form
+ * a multi-sized RemoteViews.
+ *
+ * This should not be used directly. The correct SizeBoxes can be generated with [ForEachSize].
+ */
+@Composable
+internal fun SizeBox(
+    size: DpSize,
+    sizeMode: SizeMode,
+    content: @Composable () -> Unit
+) {
+    CompositionLocalProvider(LocalSize provides size) {
+        GlanceNode(
+            factory = ::EmittableSizeBox,
+            update = {
+                this.set(size) { this.size = it }
+                this.set(sizeMode) { this.sizeMode = it }
+            },
+            content = content
+        )
+    }
+}
+
+/**
+ * For each size indicated by [sizeMode], run [content] with a [SizeBox] set to the corresponding
+ * size.
+ */
+@Composable
+internal fun ForEachSize(
+    sizeMode: SizeMode,
+    minSize: DpSize,
+    content: @Composable () -> Unit
+) {
+    val sizes = when (sizeMode) {
+        is SizeMode.Single -> listOf(minSize)
+        is SizeMode.Exact -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            LocalAppWidgetOptions.current.extractAllSizes { minSize }
+        } else {
+            LocalAppWidgetOptions.current.extractOrientationSizes()
+                .ifEmpty { listOf(minSize) }
+        }
+        is SizeMode.Responsive -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            sizeMode.sizes
+        } else {
+            val smallestSize = sizeMode.sizes.sortedBySize()[0]
+            LocalAppWidgetOptions.current.extractOrientationSizes()
+                .mapNotNull { findBestSize(it, sizeMode.sizes) }
+                .ifEmpty { listOf(smallestSize, smallestSize) }
+        }
+    }
+    sizes.map { size ->
+        SizeBox(size, sizeMode, content)
+    }
+}
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
index 60e39f3..91255d2 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/WidgetLayout.kt
@@ -338,6 +338,7 @@
         is EmittableLazyVerticalGridListItem -> LayoutProto.LayoutType.LIST_ITEM
         is RemoteViewsRoot -> LayoutProto.LayoutType.REMOTE_VIEWS_ROOT
         is EmittableRadioButton -> LayoutProto.LayoutType.RADIO_BUTTON
+        is EmittableSizeBox -> LayoutProto.LayoutType.SIZE_BOX
         else ->
             throw IllegalArgumentException("Unknown element type ${this.javaClass.canonicalName}")
     }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
index 6963b7c..ca46041 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GalleryTemplateLayouts.kt
@@ -180,11 +180,15 @@
     // Show first block by lower numbered priority
     if (data.mainTextBlock.priority <= data.mainImageBlock.priority) {
         HeaderAndTextBlocks(data, modifier)
-        Spacer(modifier = GlanceModifier.width(16.dp))
-        SingleImageBlockTemplate(data.mainImageBlock)
+        if (LocalSize.current.width > sizeMin && LocalSize.current.height > sizeMin) {
+            Spacer(modifier = GlanceModifier.width(16.dp))
+            SingleImageBlockTemplate(data.mainImageBlock)
+        }
     } else {
-        SingleImageBlockTemplate(data.mainImageBlock)
-        Spacer(modifier = GlanceModifier.width(16.dp))
+        if (LocalSize.current.width > sizeMin && LocalSize.current.height > sizeMin) {
+            SingleImageBlockTemplate(data.mainImageBlock)
+            Spacer(modifier = GlanceModifier.width(16.dp))
+        }
         HeaderAndTextBlocks(data, modifier)
     }
 }
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
index 82b55d2..a090b3f 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/template/GlanceTemplateAppWidget.kt
@@ -40,10 +40,12 @@
         internal val sizeS = 200.dp
         internal val sizeM = 241.dp
         internal val sizeL = 350.dp
+        internal val sizeXL = 600.dp
         private val COLLAPSED = DpSize(sizeMin, sizeMin)
         private val HORIZONTAL_S = DpSize(sizeM, sizeMin)
         private val HORIZONTAL_M = DpSize(sizeM, sizeS)
-        private val HORIZONTAL_L = DpSize(sizeL, sizeS)
+        private val HORIZONTAL_L = DpSize(sizeL, sizeMin)
+        private val HORIZONTAL_XL = DpSize(sizeXL, sizeL)
         private val VERTICAL_S = DpSize(sizeMin, sizeM)
         private val VERTICAL_M = DpSize(sizeS, sizeM)
         private val VERTICAL_L = DpSize(sizeS, sizeL)
@@ -52,7 +54,14 @@
     /** Default widget size mode is [SizeMode.Responsive] */
     override val sizeMode: SizeMode = SizeMode.Responsive(
         setOf(
-            COLLAPSED, VERTICAL_S, VERTICAL_M, VERTICAL_L, HORIZONTAL_S, HORIZONTAL_M, HORIZONTAL_L
+            COLLAPSED,
+            VERTICAL_S,
+            VERTICAL_M,
+            VERTICAL_L,
+            HORIZONTAL_S,
+            HORIZONTAL_M,
+            HORIZONTAL_L,
+            HORIZONTAL_XL
         )
     )
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
new file mode 100644
index 0000000..616ba95
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/AppWidgetSessionTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.glance.appwidget
+
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Context
+import android.widget.TextView
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.Emittable
+import androidx.glance.GlanceModifier
+import androidx.glance.state.GlanceStateDefinition
+import androidx.glance.state.PreferencesGlanceStateDefinition
+import androidx.glance.state.ConfigManager
+import androidx.glance.text.EmittableText
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class AppWidgetSessionTest {
+
+    private val id = AppWidgetId(123)
+    private val widget = SampleGlanceAppWidget {}
+    private val context = ApplicationProvider.getApplicationContext<Context>()
+    private val defaultOptions =
+        optionsBundleOf(listOf(DpSize(100.dp, 50.dp), DpSize(50.dp, 100.dp)))
+    private val testState = TestGlanceState()
+    private val session = AppWidgetSession(widget, id, defaultOptions, testState)
+
+    @Before
+    fun setUp() {
+        val appWidgetManager = Shadows.shadowOf(
+            context.getSystemService(Context.APPWIDGET_SERVICE) as AppWidgetManager
+        )
+        appWidgetManager.addBoundWidget(id.appWidgetId, AppWidgetProviderInfo())
+    }
+
+    @Test
+    fun createRootEmittable() = runTest {
+        assertIs<RemoteViewsRoot>(session.createRootEmittable())
+    }
+
+    @Test
+    fun provideGlanceCallsSetContent() = runTest {
+        var wasCalled = false
+        session.provideGlance(context) {
+            wasCalled = true
+        }
+        assertThat(wasCalled).isTrue()
+    }
+
+    @Test
+    fun processEmittableTree() = runTest {
+        val root = RemoteViewsRoot(maxDepth = 1).apply {
+            children += EmittableText().apply {
+                text = "hello"
+            }
+        }
+
+        session.processEmittableTree(context, root)
+        context.applyRemoteViews(session.lastRemoteViews!!).let {
+            val text = assertIs<TextView>(it)
+            assertThat(text.text).isEqualTo("hello")
+        }
+    }
+
+    @Test
+    fun processEmittableTree_catchesException() = runTest {
+        val root = RemoteViewsRoot(maxDepth = 1).apply {
+            children += object : Emittable {
+                override var modifier: GlanceModifier = GlanceModifier
+                override fun copy() = this
+            }
+        }
+
+        session.processEmittableTree(context, root)
+        assertThat(session.lastRemoteViews!!.layoutId).isEqualTo(widget.errorUiLayout)
+    }
+
+    @Test
+    fun processEvent_unknownAction() = runTest {
+        assertThrows(IllegalArgumentException::class.java) {
+            runBlocking { session.processEvent(context, Any()) }
+        }
+    }
+
+    @Test
+    fun processEvent_updateGlance() = runTest {
+        session.processEvent(context, AppWidgetSession.UpdateGlanceState)
+        assertThat(testState.getValueCalls).containsExactly(id.toSessionKey())
+    }
+
+    @Test
+    fun updateGlance() = runTest {
+        session.updateGlance()
+        session.receiveEvents(context) {
+            this@runTest.launch { session.close() }
+        }
+        assertThat(testState.getValueCalls).containsExactly(id.toSessionKey())
+    }
+
+    private class SampleGlanceAppWidget(
+        val ui: @Composable () -> Unit,
+    ) : GlanceAppWidget() {
+        @Composable
+        override fun Content() {
+            ui()
+        }
+    }
+
+    private class TestGlanceState : ConfigManager {
+
+        val getValueCalls = mutableListOf<String>()
+        @Suppress("UNCHECKED_CAST")
+        override suspend fun <T> getValue(
+            context: Context,
+            definition: GlanceStateDefinition<T>,
+            fileKey: String
+        ): T {
+            assertIs<PreferencesGlanceStateDefinition>(definition)
+            getValueCalls.add(fileKey)
+            return definition.getDataStore(context, fileKey).also {
+                definition.getLocation(context, fileKey).delete()
+            }.data.first() as T
+        }
+
+        override suspend fun <T> updateValue(
+            context: Context,
+            definition: GlanceStateDefinition<T>,
+            fileKey: String,
+            updateBlock: suspend (T) -> T
+        ): T {
+            TODO("Not yet implemented")
+        }
+
+        override suspend fun deleteStore(
+            context: Context,
+            definition: GlanceStateDefinition<*>,
+            fileKey: String
+        ) {
+            TODO("Not yet implemented")
+        }
+    }
+}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
index 1aa10a4..ca542cd 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/GlanceAppWidgetTest.kt
@@ -316,7 +316,6 @@
 
     @Test
     fun appWidgetMinSize_noResizing() {
-        val composer = SampleGlanceAppWidget { }
         val appWidgetManager = mock<AppWidgetManager> {
             on { getAppWidgetInfo(1) }.thenReturn(
                 appWidgetProviderInfo {
@@ -329,13 +328,12 @@
             )
         }
 
-        assertThat(composer.appWidgetMinSize(displayMetrics, appWidgetManager, 1))
+        assertThat(appWidgetMinSize(displayMetrics, appWidgetManager, 1))
             .isEqualTo(DpSize(50.dp, 50.dp))
     }
 
     @Test
     fun appWidgetMinSize_horizontalResizing() {
-        val composer = SampleGlanceAppWidget { }
         val appWidgetManager = mock<AppWidgetManager> {
             on { getAppWidgetInfo(1) }.thenReturn(
                 appWidgetProviderInfo {
@@ -348,13 +346,12 @@
             )
         }
 
-        assertThat(composer.appWidgetMinSize(displayMetrics, appWidgetManager, 1))
+        assertThat(appWidgetMinSize(displayMetrics, appWidgetManager, 1))
             .isEqualTo(DpSize(40.dp, 50.dp))
     }
 
     @Test
     fun appWidgetMinSize_verticalResizing() {
-        val composer = SampleGlanceAppWidget { }
         val appWidgetManager = mock<AppWidgetManager> {
             on { getAppWidgetInfo(1) }.thenReturn(
                 appWidgetProviderInfo {
@@ -367,13 +364,12 @@
             )
         }
 
-        assertThat(composer.appWidgetMinSize(displayMetrics, appWidgetManager, 1))
+        assertThat(appWidgetMinSize(displayMetrics, appWidgetManager, 1))
             .isEqualTo(DpSize(50.dp, 30.dp))
     }
 
     @Test
     fun appWidgetMinSize_bigMinResize() {
-        val composer = SampleGlanceAppWidget { }
         val appWidgetManager = mock<AppWidgetManager> {
             on { getAppWidgetInfo(1) }.thenReturn(
                 appWidgetProviderInfo {
@@ -386,7 +382,7 @@
             )
         }
 
-        assertThat(composer.appWidgetMinSize(displayMetrics, appWidgetManager, 1))
+        assertThat(appWidgetMinSize(displayMetrics, appWidgetManager, 1))
             .isEqualTo(DpSize(50.dp, 50.dp))
     }
 
@@ -452,30 +448,6 @@
         )
     }
 
-    private fun optionsBundleOf(sizes: List<DpSize>): Bundle {
-        require(sizes.isNotEmpty()) { "There must be at least one size" }
-        val (minSize, maxSize) = sizes.fold(sizes[0] to sizes[0]) { acc, s ->
-            DpSize(min(acc.first.width, s.width), min(acc.first.height, s.height)) to
-                DpSize(max(acc.second.width, s.width), max(acc.second.height, s.height))
-        }
-        return Bundle().apply {
-            putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minSize.width.value.toInt())
-            putInt(
-                AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
-                minSize.height.value.toInt()
-            )
-            putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxSize.width.value.toInt())
-            putInt(
-                AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
-                maxSize.height.value.toInt()
-            )
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-                val sizeList = sizes.map { it.toSizeF() }.toArrayList()
-                putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizeList)
-            }
-        }
-    }
-
     private fun createPortraitContext() =
         makeOrientationContext(Configuration.ORIENTATION_PORTRAIT)
 
@@ -498,3 +470,27 @@
         }
     }
 }
+
+internal fun optionsBundleOf(sizes: List<DpSize>): Bundle {
+    require(sizes.isNotEmpty()) { "There must be at least one size" }
+    val (minSize, maxSize) = sizes.fold(sizes[0] to sizes[0]) { acc, s ->
+        DpSize(min(acc.first.width, s.width), min(acc.first.height, s.height)) to
+            DpSize(max(acc.second.width, s.width), max(acc.second.height, s.height))
+    }
+    return Bundle().apply {
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, minSize.width.value.toInt())
+        putInt(
+            AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT,
+            minSize.height.value.toInt()
+        )
+        putInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, maxSize.width.value.toInt())
+        putInt(
+            AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT,
+            maxSize.height.value.toInt()
+        )
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            val sizeList = sizes.map { it.toSizeF() }.toArrayList()
+            putParcelableArrayList(AppWidgetManager.OPTION_APPWIDGET_SIZES, sizeList)
+        }
+    }
+}
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/SizeBoxTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/SizeBoxTest.kt
new file mode 100644
index 0000000..ba438eb
--- /dev/null
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/SizeBoxTest.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.glance.appwidget
+
+import android.os.Bundle
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.glance.LocalSize
+import androidx.glance.text.EmittableText
+import androidx.glance.text.Text
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertIs
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class SizeBoxTest {
+    private val minSize = DpSize(50.dp, 100.dp)
+
+    @Test
+    fun sizeModeSingle() = runTest {
+        val root = runTestingComposition {
+            ForEachSize(SizeMode.Single, minSize) {
+                val size = LocalSize.current
+                Text("${size.width} x ${size.height}")
+            }
+        }
+        val sizeBox = assertIs<EmittableSizeBox>(root.children.single())
+        assertThat(sizeBox.size).isEqualTo(minSize)
+        assertThat(sizeBox.sizeMode).isEqualTo(SizeMode.Single)
+        val text = assertIs<EmittableText>(sizeBox.children.single())
+        assertThat(text.text).isEqualTo("50.0.dp x 100.0.dp")
+    }
+
+    @Config(sdk = [30])
+    @Test
+    fun sizeModeExactPreS() = runTest {
+        val options = optionsBundleOf(
+            listOf(
+                DpSize(100.dp, 50.dp),
+                DpSize(50.dp, 100.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val root = runTestingComposition {
+            CompositionLocalProvider(LocalAppWidgetOptions provides options) {
+                ForEachSize(SizeMode.Exact, minSize) {
+                    val size = LocalSize.current
+                    Text("${size.width} x ${size.height}")
+                }
+            }
+        }
+        // On Pre-S, since AppWidgetManager.OPTION_APPWIDGET_SIZES isn't available, we use
+        // AppWidgetManager.OPTION_APPWIDGET_{MIN,MAX}_{HEIGHT,WIDTH} to find the landscape and
+        // portrait sizes.
+        assertThat(root.children).hasSize(2)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(DpSize(100.dp, 50.dp))
+        assertThat(sizeBox1.sizeMode).isEqualTo(SizeMode.Exact)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("100.0.dp x 50.0.dp")
+
+        val sizeBox2 = assertIs<EmittableSizeBox>(root.children[1])
+        assertThat(sizeBox2.size).isEqualTo(DpSize(50.dp, 100.dp))
+        assertThat(sizeBox2.sizeMode).isEqualTo(SizeMode.Exact)
+        val text2 = assertIs<EmittableText>(sizeBox2.children.single())
+        assertThat(text2.text).isEqualTo("50.0.dp x 100.0.dp")
+    }
+
+    @Config(sdk = [31])
+    @Test
+    fun sizeModeExactS() = runTest {
+        val options = optionsBundleOf(
+            listOf(
+                DpSize(100.dp, 50.dp),
+                DpSize(50.dp, 100.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val root = runTestingComposition {
+            CompositionLocalProvider(LocalAppWidgetOptions provides options) {
+                ForEachSize(SizeMode.Exact, minSize) {
+                    val size = LocalSize.current
+                    Text("${size.width} x ${size.height}")
+                }
+            }
+        }
+        // On S+, AppWidgetManager.OPTION_APPWIDGET_SIZES is available so we create a SizeBox for
+        // each size.
+        assertThat(root.children).hasSize(3)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(DpSize(100.dp, 50.dp))
+        assertThat(sizeBox1.sizeMode).isEqualTo(SizeMode.Exact)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("100.0.dp x 50.0.dp")
+
+        val sizeBox2 = assertIs<EmittableSizeBox>(root.children[1])
+        assertThat(sizeBox2.size).isEqualTo(DpSize(50.dp, 100.dp))
+        assertThat(sizeBox2.sizeMode).isEqualTo(SizeMode.Exact)
+        val text2 = assertIs<EmittableText>(sizeBox2.children.single())
+        assertThat(text2.text).isEqualTo("50.0.dp x 100.0.dp")
+
+        val sizeBox3 = assertIs<EmittableSizeBox>(root.children[2])
+        assertThat(sizeBox3.size).isEqualTo(DpSize(75.dp, 75.dp))
+        assertThat(sizeBox3.sizeMode).isEqualTo(SizeMode.Exact)
+        val text3 = assertIs<EmittableText>(sizeBox3.children.single())
+        assertThat(text3.text).isEqualTo("75.0.dp x 75.0.dp")
+    }
+
+    @Test
+    fun sizeModeExactEmptySizes() = runTest {
+        val options = Bundle()
+        val root = runTestingComposition {
+            CompositionLocalProvider(LocalAppWidgetOptions provides options) {
+                ForEachSize(SizeMode.Exact, minSize) {
+                    val size = LocalSize.current
+                    Text("${size.width} x ${size.height}")
+                }
+            }
+        }
+        // When no sizes are available, a single SizeBox for minSize should be created
+        assertThat(root.children).hasSize(1)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(minSize)
+        assertThat(sizeBox1.sizeMode).isEqualTo(SizeMode.Exact)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("50.0.dp x 100.0.dp")
+    }
+
+    @Config(sdk = [30])
+    @Test
+    fun sizeModeResponsivePreS() = runTest {
+        val options = optionsBundleOf(
+            listOf(
+                DpSize(100.dp, 50.dp),
+                DpSize(50.dp, 100.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val sizeMode = SizeMode.Responsive(
+            setOf(
+                DpSize(99.dp, 49.dp),
+                DpSize(49.dp, 99.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val root = runTestingComposition {
+            CompositionLocalProvider(LocalAppWidgetOptions provides options) {
+                ForEachSize(sizeMode, minSize) {
+                    val size = LocalSize.current
+                    Text("${size.width} x ${size.height}")
+                }
+            }
+        }
+        // On Pre-S, we extract orientation sizes from
+        // AppWidgetManager.OPTION_APPWIDGET_{MIN,MAX}_{HEIGHT,WIDTH} to find the landscape and
+        // portrait sizes, then find which responsive size fits best for each.
+        assertThat(root.children).hasSize(2)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(DpSize(99.dp, 49.dp))
+        assertThat(sizeBox1.sizeMode).isEqualTo(sizeMode)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("99.0.dp x 49.0.dp")
+
+        val sizeBox2 = assertIs<EmittableSizeBox>(root.children[1])
+        assertThat(sizeBox2.size).isEqualTo(DpSize(49.dp, 99.dp))
+        assertThat(sizeBox2.sizeMode).isEqualTo(sizeMode)
+        val text2 = assertIs<EmittableText>(sizeBox2.children.single())
+        assertThat(text2.text).isEqualTo("49.0.dp x 99.0.dp")
+    }
+
+    @Config(sdk = [30])
+    @Test
+    fun sizeModeResponsiveUseSmallestSize() = runTest {
+        val options = optionsBundleOf(
+            listOf(
+                DpSize(100.dp, 50.dp),
+                DpSize(50.dp, 100.dp),
+            )
+        )
+        val sizeMode = SizeMode.Responsive(
+            setOf(
+                DpSize(200.dp, 200.dp),
+                DpSize(300.dp, 300.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val root = runTestingComposition {
+            CompositionLocalProvider(LocalAppWidgetOptions provides options) {
+                ForEachSize(sizeMode, minSize) {
+                    val size = LocalSize.current
+                    Text("${size.width} x ${size.height}")
+                }
+            }
+        }
+        // On Pre-S, we extract orientation sizes from
+        // AppWidgetManager.OPTION_APPWIDGET_{MIN,MAX}_{HEIGHT,WIDTH} to find the landscape and
+        // portrait sizes, then find which responsive size fits best for each. If none fits, then we
+        // use the smallest size for both landscape and portrait.
+        assertThat(root.children).hasSize(2)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(DpSize(75.dp, 75.dp))
+        assertThat(sizeBox1.sizeMode).isEqualTo(sizeMode)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("75.0.dp x 75.0.dp")
+
+        val sizeBox2 = assertIs<EmittableSizeBox>(root.children[1])
+        assertThat(sizeBox2.size).isEqualTo(DpSize(75.dp, 75.dp))
+        assertThat(sizeBox2.sizeMode).isEqualTo(sizeMode)
+        val text2 = assertIs<EmittableText>(sizeBox2.children.single())
+        assertThat(text2.text).isEqualTo("75.0.dp x 75.0.dp")
+    }
+
+    @Config(sdk = [31])
+    @Test
+    fun sizeModeResponsiveS() = runTest {
+        val sizeMode = SizeMode.Responsive(
+            setOf(
+                DpSize(100.dp, 50.dp),
+                DpSize(50.dp, 100.dp),
+                DpSize(75.dp, 75.dp),
+            )
+        )
+        val root = runTestingComposition {
+            ForEachSize(sizeMode, minSize) {
+                val size = LocalSize.current
+                Text("${size.width} x ${size.height}")
+            }
+        }
+        // On S, we create a SizeBox for each given size.
+        assertThat(root.children).hasSize(3)
+        val sizeBox1 = assertIs<EmittableSizeBox>(root.children[0])
+        assertThat(sizeBox1.size).isEqualTo(DpSize(100.dp, 50.dp))
+        assertThat(sizeBox1.sizeMode).isEqualTo(sizeMode)
+        val text1 = assertIs<EmittableText>(sizeBox1.children.single())
+        assertThat(text1.text).isEqualTo("100.0.dp x 50.0.dp")
+
+        val sizeBox2 = assertIs<EmittableSizeBox>(root.children[1])
+        assertThat(sizeBox2.size).isEqualTo(DpSize(50.dp, 100.dp))
+        assertThat(sizeBox2.sizeMode).isEqualTo(sizeMode)
+        val text2 = assertIs<EmittableText>(sizeBox2.children.single())
+        assertThat(text2.text).isEqualTo("50.0.dp x 100.0.dp")
+
+        val sizeBox3 = assertIs<EmittableSizeBox>(root.children[2])
+        assertThat(sizeBox3.size).isEqualTo(DpSize(75.dp, 75.dp))
+        assertThat(sizeBox3.sizeMode).isEqualTo(sizeMode)
+        val text3 = assertIs<EmittableText>(sizeBox3.children.single())
+        assertThat(text3.text).isEqualTo("75.0.dp x 75.0.dp")
+    }
+}
\ No newline at end of file
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
index 39a8849..f563295 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/CompositionLocals.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.State
 import androidx.compose.runtime.compositionLocalOf
 import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.unit.DpSize
@@ -59,12 +58,7 @@
  * @return the current store of the provided type [T]
  */
 @Composable
-inline fun <reified T> currentState(): T = LocalState.current.let {
-    when (it) {
-        is State<*> -> it.value as T
-        else -> it as T
-    }
-}
+inline fun <reified T> currentState(): T = LocalState.current as T
 
 /**
  * Retrieves the current [Preferences] value of the provided [Preferences.Key] from the current
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
index c1f265b..9b7d161 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/state/GlanceStateDefinition.kt
@@ -54,14 +54,13 @@
 }
 
 /**
- * Data store for data specific to the glanceable view. Stored data should include information
- * relevant to the representation of views, but not surface specific view data. For example, the
- * month displayed on a calendar rather than actual calendar entries.
+ * Interface for an object that manages configuration for glanceables using the given
+ * GlanceStateDefinition.
  *
  * @suppress
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-object GlanceState {
+interface ConfigManager {
     /**
      * Returns the stored data associated with the given UI key string.
      *
@@ -73,7 +72,7 @@
         context: Context,
         definition: GlanceStateDefinition<T>,
         fileKey: String
-    ): T = getDataStore(context, definition, fileKey).data.first()
+    ): T
 
     /**
      * Updates the underlying data by applying the provided update block.
@@ -87,7 +86,7 @@
         definition: GlanceStateDefinition<T>,
         fileKey: String,
         updateBlock: suspend (T) -> T
-    ): T = getDataStore(context, definition, fileKey).updateData(updateBlock)
+    ): T
 
     /**
      * Delete the file underlying the [DataStore] and remove local references to the [DataStore].
@@ -96,6 +95,35 @@
         context: Context,
         definition: GlanceStateDefinition<*>,
         fileKey: String
+    )
+}
+
+/**
+ * Data store for data specific to the glanceable view. Stored data should include information
+ * relevant to the representation of views, but not surface specific view data. For example, the
+ * month displayed on a calendar rather than actual calendar entries.
+ *
+ * @suppress
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object GlanceState : ConfigManager {
+    override suspend fun <T> getValue(
+        context: Context,
+        definition: GlanceStateDefinition<T>,
+        fileKey: String
+    ): T = getDataStore(context, definition, fileKey).data.first()
+
+    override suspend fun <T> updateValue(
+        context: Context,
+        definition: GlanceStateDefinition<T>,
+        fileKey: String,
+        updateBlock: suspend (T) -> T
+    ): T = getDataStore(context, definition, fileKey).updateData(updateBlock)
+
+    override suspend fun deleteStore(
+        context: Context,
+        definition: GlanceStateDefinition<*>,
+        fileKey: String
     ) {
         mutex.withLock {
             dataStores.remove(fileKey)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f628ef5..58e943f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -78,7 +78,7 @@
 autoValue = { module = "com.google.auto.value:auto-value", version.ref = "autoValue" }
 autoValueAnnotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "autoValue" }
 autoValueParcel = { module = "com.ryanharter.auto.value:auto-value-parcel", version = "0.2.6" }
-antlr4 = { module = "org.antlr:antlr4", version = "4.7.1" }
+antlr4 = { module = "org.antlr:antlr4", version = "4.11.1" }
 apacheAnt = { module = "org.apache.ant:ant", version = "1.10.11" }
 apacheCommonsCodec = { module = "commons-codec:commons-codec", version = "1.15" }
 apacheCommonIo = { module = "commons-io:commons-io", version = "2.4" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index ccfa886..b0c2a94 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -2377,6 +2377,52 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    2A742740E08E7F8D
+uid    Terence Parr <parrt@antlr.org>
+
+sub    74C249541619FF0B
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQMuBGJIi4URCADFspeHyziASBuPXpLpikWjmC3D6VtTaDT17ogOyGLf6/sjsQUz
+0KS3PzWBuPoqRGRpTtZxJ5yr10apr8mJF9Po5LFkrtcexaiYmUWAZAik894OhKt1
+O9he2Sh1OTUiTmFU4ImQY+AeRqASZMYabhbbJOfQLJV2Er+foKzRC0T2MSQVjDho
+NywU1IsQG58lXEQNOA50uCuhnaCUy7Mh/GKCejyBaqXMtd517evHhqsJd9tWMNW/
+W2xKvGiH2mWSRjgllZ0h3zHuUEo8xqetOuHrDGGRpkzRES0kIT7En39hpVpP662Y
+97Wolv4C0/UE4rlfCmiplf6mG9QPET8wZeRvAQDh+Z27sD3ODWU2P5s/EXzsmBoi
+kK2KzGE+n32kRY4fHQf7BE9PfZ3f58KICY7p5apxP/6+bl8uq9vcszGPHl1aT2cH
+oQpPm5i2UHRoWXh7U9TpKWxxqx+yvJlPhV1c2DTvdbx530xEMF6up1f04+axhlqE
+DzjzOenwpnS3DR5iUqflEM33njj4tK+Tw51kXjyBxafPmaKEwuqzYzCFoojidmSx
+Vk4la9hCvIJzGQ+3iTX6OR4d1lGvRGmVm2g2AVrpZ1yPncgjCu5qFH9UiBblE8LV
+SVGBufS86pRwTrwW+fXaKw5iIyiPMSSE11H8uw4q75uFVnmEd4JUtQxGjnv7vbjq
+7hAeE3T4HlLPFBr3y8c3829HY0ozNVKKtXs0/lBzwwgAm/59t1B/dMl61BaufnY1
+5tYFAcStTrl0c3ZAYHO6DWYTJ8ZZQqiBfeyVI3yqWKQAYg0DxC9AzTtNSOs7SGJK
+dgdencJoa0ElraZuVXfb2Pr6cBv5wKRfU7ZBvHfCE56vJ/0zAvGbIRy4DYup8+Pj
+vcLSSWvQMT0iHk9TTw6sJNV/S4aH37Ux2N3SsARvqR1nZ7rQaGN5eTg7qHmwgIgd
+AoHa2Jd3ixOKuLzwIF4hoh+XKI7bYzVHwYq6yVZWevloxlky0FfAXCH2/lrTGyvj
+i83tPUIushngZ07senzgK0IWQIuLVjl03tJ/rc12AtiZkx1/ykssQ2uJolIRLfou
+u7QeVGVyZW5jZSBQYXJyIDxwYXJydEBhbnRsci5vcmc+iJQEExEIADwWIQRXGeUO
+rFpLHdOQtywqdCdA4I5/jQUCYkiLhQIbAwULCQgHAgMiAgEGFQoJCAsCBBYCAwEC
+HgcCF4AACgkQKnQnQOCOf43jewEA0qzHkKX4SR0D4BcVs8wwThZHtDKAu6grdhPd
+jqywgu4BALSDeN3OVYhAc06V9D059yYLDPRNjk+98xtBK+u+un7NuQINBGJIi4UQ
+CAD5Ghrh2sWmpfEBHhmMnZYDS/1ZQZaZtmvTcGmqOhbOAe8zjnchtJDd76X4NjN/
+HDQSlUqz7saJEs/j9rV/e/S5sE9/9Ad+Jj+XN+pQPAJQ18HxmTDKC+zJ22Ej4MPq
+GBY6d3qVrc7m+0Ue+m4fxy/q5glVYifnBVu8BvKEkifVSDf75Cr+DgObtAIqy7+m
+G6VyAm1pm4NM9EYos2GbBVs7sT1yGNbWl5oRtiHAjQfWMdTyidez7TS6IzRto6ek
+jbT7lu8jxmbzBi7cUabOHKQ4so7B3zLw2VHZVJpNhkAY7b6Ha+b89a4yeeX2/yal
+iL1dsd2t/0qd0Bx/H6Bi2N3HAAUTCACqD0obDFeg+1qELOsF2rgzgUrvMnKrkZW1
+JBcUTM+OeUXjG9wX2e0b7rtLu+48C9OwfKG98ZqoGyyyL94NJULEP0UhhcZkOutU
+IKgyQzVHuqYTcRkqvZ+MEpSaZBvyq7qySeMOpSQ3DKiWEyKnXb0BS+s5btJXQcrf
+dJIuVvN2/3P4I5gzmXIu+CqZaMicrOK5ekbkBOzMaqXPUEwfzGG0UOg4ClWdU9c8
+76ksZvGu7La3kOFUgSey5X8DuuzqSjxcJeTX6eO8jIJCd1Tib6M1Go9TzpByHf6p
+nJQG6HCYv+71Wwpys6XTDspfDFZNuSpBJnKe341W2m48o3GhorM4iHgEGBEIACAW
+IQRXGeUOrFpLHdOQtywqdCdA4I5/jQUCYkiLhQIbDAAKCRAqdCdA4I5/jaJ6AP0X
+eybLJPdE2S4dOCzdDWkY3/Ge7zR+24dG7IjwqbShRgD/cN7sq1NgRQ7ykCPxh0ZO
+jJNKSYyrKuT+DCAd1uFvcTw=
+=SigC
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    2BE5D98F751F4136
 sub    C7FDDD147FA73F44
 -----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -13282,6 +13328,65 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    F3D1600878E85A3D
+uid    Netty Project Bot <netty-project-bot@users.noreply.github.com>
+
+sub    1C9F436B883DCCF6
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGAhOxEBEADdB5Jy2sSOndOMCTyk8IFIJYPogjXtN7CnyIlqr4jEB5G87TJf
+m7OxB95aIVS1vSA5ghCm88N1mKtW6jyYjgLFQbbyD9/X3ShVZjh8B2R4atL93SSK
+ppfSrQE3+EohYzu/X5agtzMhg4VplfY67yBUFXEqTucXpYumKLctrYtOUgDCgs4s
+4BixyAidsUxP9Uet2CsBiK7jlIe21EQz60QGvQ81pDaerwCxUsxtd4Fps+gSm6cY
+7Q+CrJRmV+rGpOt2f9NAyGdqqy71tjd5e7VC6GHyDxiB4xnDKQDGpfiMtGnxHPfe
+OaeYriCWQPpUIw7dg4eTVHKXlJ4FAc6W3Qdl0mlNKNIFizhcNxrie2FbLNxZYV+G
+B3GkDZt5Oas1O/iWcQt2QcalwTJWBY35kSl+uZilDAeU94vzuu1SQCZqmTtH82oa
+xp4eD4fqP5dB3qH/alao8IVlNRmbrEdbg2fZg4xVVmm+CF+gPnxswZRIptY2rsbb
+oEM8dWxakT5zvjox+v5J+qmEkE5WLlL/DlokOnJlAjJ3fkq6qGengQNjlrMIZjcL
+olHfr8gbYD2u4A7Dz9hls4fDz8OGqzHkSbNYm9hO9q5AWnqAWcSLPHkJ3mim91AW
+enWzfqoxNNR6L02mDvippqpfEoFTgqmZvYun8r1qTU5UaQnz3Od7QAf72wARAQAB
+tD5OZXR0eSBQcm9qZWN0IEJvdCA8bmV0dHktcHJvamVjdC1ib3RAdXNlcnMubm9y
+ZXBseS5naXRodWIuY29tPokCTgQTAQgAOBYhBA010/YAeGVRJpCOivPRYAh46Fo9
+BQJgITsRAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEPPRYAh46Fo9UWAP
+/RmQ5CplWlfZgkiILVT105i//T9pmUS4x2mW564pIB7RpQD8WVzt06p3wBVUvHtN
+gdfVHj+3mxdGLjCtcEoi8cFSvImsqM9X69823ZrsfJGKJxxerrn+b4crHLZqxSrw
+B4QwVdxsok/jyPOo1joZlv5QhGFFp5XcMqgw0UexSBZ1yhmy//40M5jVOvVvN1F+
+DwZp63/7Ll3dnIhegKH31FGCwy5tVyL3O2kGnCT2crF6QGcNDJB6KqGiBKbU5FLw
+rrbrTeIRuUu3CXS7oiem3sbrmO+NDYIwijGlqMHI2Nw4pNBnEHSm0RZO7I+GbZfy
+Fe4xF4CMH7xvtpIdDtZ9Sk1odk7MUYrfTbDIfmzPlDpaCy6XS0Xcd6QjUosaLj7P
+ImilSxYNeIbMyaAdLZQhq3iywacooOehgzAA4VPsdE6UW8YWMxntbJ080h10wMHl
+MqHK2QWwnX81Uo0fuPsqGTQKHu/WH82IPsSAp1Cek0l9Ye6vRacwC87dGSrDG+RR
+d6eNAbUTLiknbWbK9T+XC+qCeImpn87pbZN8uUOYeqwyzc12gY8KX4iu5gbMKebk
+3+sx5B4YOGsBMhjfbLK77j56yBTpSc7R7JBp7DZ+WvnlsxXGTbMG85MpUvmQHnmK
++CEQG0Cd6wC4vXRe8VLJScHGMjxDndJPGGyIiLv0IPgDuQINBGAhOxEBEADJSkJ3
+U5vrwpDYr4k2L1Cawqh9/02nAuazvNbl2ocjVHaDD51HJlm2DxwNg8obelCuVK1I
+klc8MLO0BcRILu3AKCK/6sVzy75r3oTH6yjZtOCWiPbmxXHxUjmyOrZW+ICaUy29
+PnXag3owpWz2dEE1xllDLAgs0zl9lWZGFvq94epBWfrj0vd0KL61ubwfLAhG+Kek
+PpUz3MwiT1kt96epBlkTtUGsH+u/RtGeDhbr8vdP9AX7JHr/UMZLGCajKy+70yFY
+CIfFBnu8veTldTVo05/fTNk5ADJOBZjcINkS0NGnpKmgL7xmQI8362Irg6mKihWL
+Ub/2DBpGya/Rp0j8Qw9OspsJGMHUo7rTHSm462noWXvjq2nNmu8Vb3Xj4JVJsIWT
+aoDdODJ/eRnrTci/eMS6lcoaVX3Yp5oatdF8/2X9ZoR5Pg1HYDRyiibZvGrZS5UA
+p0o7ajqit7l0Jlc9Md0nIkalD1rcSTqAWA2TUWyYCKBb4ec5v/9uw8rg672VcbVU
+oWb+gEIxN9hJfijyrW4CGMdRpTr68tEivgliYLDcSvLYZKRUldIKKpMntOtUKyUb
+SfwlJUKux6Hx2Ysi41zQD2mfg6/Mw6JhTa4lvbGrX9D6HxS2ct7o76O5uXWSDR5A
+TwmQsYug3OgVKIEo0UvKHogQFCWNlMwR6/Zy8wARAQABiQI2BBgBCAAgFiEEDTXT
+9gB4ZVEmkI6K89FgCHjoWj0FAmAhOxECGwwACgkQ89FgCHjoWj12GRAAnQ5g/B+K
+yCaGPokZcGh+KnieuUaS7txeS5fAzN40utUJA/JxwoG7AselbZ68QQIzG6XgNsFa
+5fEpJcC3o+mkjlVMvaNxPWRlLO7TMZ9N0AldwSCXdRLrEbx8Uw1omXvC5d80Qfkn
+D1sDikPZhAGLPLK0PfM4DxUbLedxweDLALWt2C1BpnPKF26jQN4ZBrUBcLlXtkPO
+rAcxmCXsyS1yh+iM5hNK+s+CJ98tMlDw1U6oMXuW8lp9DloJXL7y11ftCPNr7vf4
+J/KbafBkKTP6AZpl7lJQ1k/1hZxE+Wk2bSgRuq03wbs2SudBw2xLDx0hl6JT60pC
+adWvN/hhpWGyqOYZRAY7BxrHYEMLhx0fYwHH9d/aagTHipeYz+3S6C++C3pFXpa0
+0geg8ytpg/KC8d+6rRr13lCfbV50O7nQ74rAXBx/4e8l6W3pRG5mX+00Z+WAPY9e
+pujtHDT/94DmXZaJ35WeqSwUocXFUvJ2B6R7srxtuyPIuXlSJ8z+T2R5az0Jk2T7
+CqltUx/wQadosFq3jsvl+RtnBimwYIiS1UreX5mVbBZiGpTlaFNhxfdXW1XmmfGz
++31CPxEIzHA5kbJzzaB/ofX3IjRSQB+rpxetnGCXKnclUUyruTMY5XMGD/VGCOLw
+eOotdxJF6J5yWErznxlExP5YBIHvIQljCyU=
+=WKVz
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    F406F31BC1468EBA
 sub    4BB1ED965FF68B71
 -----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -13497,62 +13602,3 @@
 Pt2uco8an9pO9/oqU6vlZUr38w==
 =alQS
 -----END PGP PUBLIC KEY BLOCK-----
-
-
-pub    F3D1600878E85A3D
-uid    Netty Project Bot <netty-project-bot@users.noreply.github.com>
-
-sub    1C9F436B883DCCF6
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.68
-
-mQINBGAhOxEBEADdB5Jy2sSOndOMCTyk8IFIJYPogjXtN7CnyIlqr4jEB5G87TJf
-m7OxB95aIVS1vSA5ghCm88N1mKtW6jyYjgLFQbbyD9/X3ShVZjh8B2R4atL93SSK
-ppfSrQE3+EohYzu/X5agtzMhg4VplfY67yBUFXEqTucXpYumKLctrYtOUgDCgs4s
-4BixyAidsUxP9Uet2CsBiK7jlIe21EQz60QGvQ81pDaerwCxUsxtd4Fps+gSm6cY
-7Q+CrJRmV+rGpOt2f9NAyGdqqy71tjd5e7VC6GHyDxiB4xnDKQDGpfiMtGnxHPfe
-OaeYriCWQPpUIw7dg4eTVHKXlJ4FAc6W3Qdl0mlNKNIFizhcNxrie2FbLNxZYV+G
-B3GkDZt5Oas1O/iWcQt2QcalwTJWBY35kSl+uZilDAeU94vzuu1SQCZqmTtH82oa
-xp4eD4fqP5dB3qH/alao8IVlNRmbrEdbg2fZg4xVVmm+CF+gPnxswZRIptY2rsbb
-oEM8dWxakT5zvjox+v5J+qmEkE5WLlL/DlokOnJlAjJ3fkq6qGengQNjlrMIZjcL
-olHfr8gbYD2u4A7Dz9hls4fDz8OGqzHkSbNYm9hO9q5AWnqAWcSLPHkJ3mim91AW
-enWzfqoxNNR6L02mDvippqpfEoFTgqmZvYun8r1qTU5UaQnz3Od7QAf72wARAQAB
-tD5OZXR0eSBQcm9qZWN0IEJvdCA8bmV0dHktcHJvamVjdC1ib3RAdXNlcnMubm9y
-ZXBseS5naXRodWIuY29tPokCTgQTAQgAOBYhBA010/YAeGVRJpCOivPRYAh46Fo9
-BQJgITsRAhsDBQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEPPRYAh46Fo9UWAP
-/RmQ5CplWlfZgkiILVT105i//T9pmUS4x2mW564pIB7RpQD8WVzt06p3wBVUvHtN
-gdfVHj+3mxdGLjCtcEoi8cFSvImsqM9X69823ZrsfJGKJxxerrn+b4crHLZqxSrw
-B4QwVdxsok/jyPOo1joZlv5QhGFFp5XcMqgw0UexSBZ1yhmy//40M5jVOvVvN1F+
-DwZp63/7Ll3dnIhegKH31FGCwy5tVyL3O2kGnCT2crF6QGcNDJB6KqGiBKbU5FLw
-rrbrTeIRuUu3CXS7oiem3sbrmO+NDYIwijGlqMHI2Nw4pNBnEHSm0RZO7I+GbZfy
-Fe4xF4CMH7xvtpIdDtZ9Sk1odk7MUYrfTbDIfmzPlDpaCy6XS0Xcd6QjUosaLj7P
-ImilSxYNeIbMyaAdLZQhq3iywacooOehgzAA4VPsdE6UW8YWMxntbJ080h10wMHl
-MqHK2QWwnX81Uo0fuPsqGTQKHu/WH82IPsSAp1Cek0l9Ye6vRacwC87dGSrDG+RR
-d6eNAbUTLiknbWbK9T+XC+qCeImpn87pbZN8uUOYeqwyzc12gY8KX4iu5gbMKebk
-3+sx5B4YOGsBMhjfbLK77j56yBTpSc7R7JBp7DZ+WvnlsxXGTbMG85MpUvmQHnmK
-+CEQG0Cd6wC4vXRe8VLJScHGMjxDndJPGGyIiLv0IPgDuQINBGAhOxEBEADJSkJ3
-U5vrwpDYr4k2L1Cawqh9/02nAuazvNbl2ocjVHaDD51HJlm2DxwNg8obelCuVK1I
-klc8MLO0BcRILu3AKCK/6sVzy75r3oTH6yjZtOCWiPbmxXHxUjmyOrZW+ICaUy29
-PnXag3owpWz2dEE1xllDLAgs0zl9lWZGFvq94epBWfrj0vd0KL61ubwfLAhG+Kek
-PpUz3MwiT1kt96epBlkTtUGsH+u/RtGeDhbr8vdP9AX7JHr/UMZLGCajKy+70yFY
-CIfFBnu8veTldTVo05/fTNk5ADJOBZjcINkS0NGnpKmgL7xmQI8362Irg6mKihWL
-Ub/2DBpGya/Rp0j8Qw9OspsJGMHUo7rTHSm462noWXvjq2nNmu8Vb3Xj4JVJsIWT
-aoDdODJ/eRnrTci/eMS6lcoaVX3Yp5oatdF8/2X9ZoR5Pg1HYDRyiibZvGrZS5UA
-p0o7ajqit7l0Jlc9Md0nIkalD1rcSTqAWA2TUWyYCKBb4ec5v/9uw8rg672VcbVU
-oWb+gEIxN9hJfijyrW4CGMdRpTr68tEivgliYLDcSvLYZKRUldIKKpMntOtUKyUb
-SfwlJUKux6Hx2Ysi41zQD2mfg6/Mw6JhTa4lvbGrX9D6HxS2ct7o76O5uXWSDR5A
-TwmQsYug3OgVKIEo0UvKHogQFCWNlMwR6/Zy8wARAQABiQI2BBgBCAAgFiEEDTXT
-9gB4ZVEmkI6K89FgCHjoWj0FAmAhOxECGwwACgkQ89FgCHjoWj12GRAAnQ5g/B+K
-yCaGPokZcGh+KnieuUaS7txeS5fAzN40utUJA/JxwoG7AselbZ68QQIzG6XgNsFa
-5fEpJcC3o+mkjlVMvaNxPWRlLO7TMZ9N0AldwSCXdRLrEbx8Uw1omXvC5d80Qfkn
-D1sDikPZhAGLPLK0PfM4DxUbLedxweDLALWt2C1BpnPKF26jQN4ZBrUBcLlXtkPO
-rAcxmCXsyS1yh+iM5hNK+s+CJ98tMlDw1U6oMXuW8lp9DloJXL7y11ftCPNr7vf4
-J/KbafBkKTP6AZpl7lJQ1k/1hZxE+Wk2bSgRuq03wbs2SudBw2xLDx0hl6JT60pC
-adWvN/hhpWGyqOYZRAY7BxrHYEMLhx0fYwHH9d/aagTHipeYz+3S6C++C3pFXpa0
-0geg8ytpg/KC8d+6rRr13lCfbV50O7nQ74rAXBx/4e8l6W3pRG5mX+00Z+WAPY9e
-pujtHDT/94DmXZaJ35WeqSwUocXFUvJ2B6R7srxtuyPIuXlSJ8z+T2R5az0Jk2T7
-CqltUx/wQadosFq3jsvl+RtnBimwYIiS1UreX5mVbBZiGpTlaFNhxfdXW1XmmfGz
-+31CPxEIzHA5kbJzzaB/ofX3IjRSQB+rpxetnGCXKnclUUyruTMY5XMGD/VGCOLw
-eOotdxJF6J5yWErznxlExP5YBIHvIQljCyU=
-=WKVz
------END PGP PUBLIC KEY BLOCK-----
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c4aadd3..f9a0caf 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -91,8 +91,8 @@
             <trusting group="io.reactivex.rxjava3"/>
          </trusted-key>
          <trusted-key id="1dbb44e80f61493d6369b5fb95c15058a5eda4f1">
-            <trusting group="com.google.protobuf" name="protobuf-gradle-plugin"/>
             <trusting group="com.google.gradle"/>
+            <trusting group="com.google.protobuf" name="protobuf-gradle-plugin"/>
          </trusted-key>
          <trusted-key id="1f47744c9b6e14f2049c2857f1f111af65925306" group="io.github.classgraph" name="classgraph"/>
          <trusted-key id="1f8cf885d537a431" group="com.nhaarman.mockitokotlin2"/>
@@ -205,6 +205,7 @@
          <trusted-key id="5208812e1e4a6db0" group="com.gradle"/>
          <trusted-key id="55c7e5e701832382" group="org.snakeyaml"/>
          <trusted-key id="56b505dc8a29c69138a430b9429c8816dea04cdb" group="org.xerial" name="sqlite-jdbc"/>
+         <trusted-key id="5719e50eac5a4b1dd390b72c2a742740e08e7f8d" group="org.antlr"/>
          <trusted-key id="571a5291e827e1c7" group="net.java"/>
          <trusted-key id="5767f9cde920750621875079a40e24b5b408dbd5" group="org.robolectric"/>
          <trusted-key id="586654072ead6677" group="org.sonatype.oss"/>
@@ -263,6 +264,7 @@
          <trusted-key id="70cd19bfd9f6c330027d6f260315bfb7970a144f">
             <trusting group="com.sun.xml.bind"/>
             <trusting group="javax.xml.bind"/>
+            <trusting group="org.glassfish"/>
             <trusting group="org.glassfish.jaxb"/>
             <trusting group="org.jvnet.staxex"/>
             <trusting group="^com[.]sun($|([.].*))" regex="true"/>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
index 63f1c4e..5ccbdf2 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/lowlatency/GLFrontBufferedRendererTest.kt
@@ -16,11 +16,13 @@
 
 package androidx.graphics.lowlatency
 
+import android.app.UiAutomation
 import android.graphics.Color
 import android.hardware.HardwareBuffer
 import android.opengl.GLES20
 import android.opengl.Matrix
 import android.os.Build
+import android.view.SurfaceHolder
 import android.view.SurfaceView
 import androidx.annotation.RequiresApi
 import androidx.graphics.opengl.egl.EGLManager
@@ -31,6 +33,7 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
@@ -405,9 +408,8 @@
     fun testDoubleBufferedContentsNotPersisted() {
         val mOrthoMatrix = FloatArray(16)
         val mProjectionMatrix = FloatArray(16)
-        val mLines = FloatArray(4)
-        val mLineRenderer = LineRenderer()
         val screenWidth = FrontBufferedRendererTestActivity.WIDTH
+        val rectWidth = 10f
 
         val renderLatch = CountDownLatch(1)
         val firstDrawLatch = CountDownLatch(1)
@@ -419,7 +421,6 @@
                 transform: FloatArray,
                 param: Any
             ) {
-                mLineRenderer.initialize()
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
                 Matrix.orthoM(
                     mOrthoMatrix,
@@ -431,16 +432,14 @@
                     -1f,
                     1f
                 )
-                mLines[0] = screenWidth / 4 + (param as Float)
-                mLines[1] = 0f
-                mLines[2] = screenWidth / 4 + param
-                mLines[3] = 100f
+                val left = screenWidth / 4 + (param as Float) - rectWidth / 2
+                val top = 0f
+                val right = left + rectWidth / 2
+                val bottom = 100f
 
                 Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
-                mLineRenderer.drawLines(mProjectionMatrix, mLines)
-                assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
+                Rectangle().draw(mProjectionMatrix, Color.RED, left, top, right, bottom)
                 firstDrawLatch.countDown()
-                mLineRenderer.release()
             }
 
             override fun onDrawDoubleBufferedLayer(
@@ -450,7 +449,6 @@
                 transform: FloatArray,
                 params: Collection<Any>
             ) {
-                mLineRenderer.initialize()
                 GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
                 Matrix.orthoM(
                     mOrthoMatrix,
@@ -464,15 +462,14 @@
                 )
                 Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
                 for (param in params) {
-                    mLines[0] = screenWidth / 4 + (param as Float)
-                    mLines[1] = 0f
-                    mLines[2] = screenWidth / 4 + param
-                    mLines[3] = 100f
+                    val left = screenWidth / 4 + (param as Float) - rectWidth / 2
+                    val top = 0f
+                    val right = left + rectWidth / 2
+                    val bottom = 100f
 
-                    mLineRenderer.drawLines(mProjectionMatrix, mLines)
+                    Rectangle().draw(mProjectionMatrix, Color.RED, left, top, right, bottom)
                     assertEquals(GLES20.GL_NO_ERROR, GLES20.glGetError())
                 }
-                mLineRenderer.release()
             }
 
             override fun onDoubleBufferedLayerRenderComplete(
@@ -644,6 +641,126 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
+    fun test180DegreeRotationBufferTransform() {
+        val initialFrontBufferLatch = CountDownLatch(1)
+        val secondFrontBufferLatch = CountDownLatch(1)
+        var bufferTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        var surfaceView: SurfaceView? = null
+        val surfaceHolderCallbacks = object : SurfaceHolder.Callback {
+            override fun surfaceCreated(p0: SurfaceHolder) {
+                // NO-OP
+            }
+
+            override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) {
+                bufferTransform =
+                    BufferTransformHintResolver().getBufferTransformHint(surfaceView!!)
+            }
+
+            override fun surfaceDestroyed(p0: SurfaceHolder) {
+                // NO-OP
+            }
+        }
+        var configuredBufferTransform = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+        val callbacks = object : GLFrontBufferedRenderer.Callback<Any> {
+
+            val mOrthoMatrix = FloatArray(16)
+            val mProjectionMatrix = FloatArray(16)
+            var mRectangle: Rectangle? = null
+
+            private fun getSquare(): Rectangle = mRectangle ?: Rectangle().also { mRectangle = it }
+
+            override fun onDrawFrontBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                param: Any
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onDrawDoubleBufferedLayer(
+                eglManager: EGLManager,
+                bufferWidth: Int,
+                bufferHeight: Int,
+                transform: FloatArray,
+                params: Collection<Any>
+            ) {
+                GLES20.glViewport(0, 0, bufferWidth, bufferHeight)
+                Matrix.orthoM(
+                    mOrthoMatrix,
+                    0,
+                    0f,
+                    bufferWidth.toFloat(),
+                    0f,
+                    bufferHeight.toFloat(),
+                    -1f,
+                    1f
+                )
+                Matrix.multiplyMM(mProjectionMatrix, 0, mOrthoMatrix, 0, transform, 0)
+                getSquare().draw(mProjectionMatrix, Color.RED, 0f, 0f, 100f, 100f)
+            }
+
+            override fun onFrontBufferedLayerRenderComplete(
+                frontBufferedLayerSurfaceControl: SurfaceControlCompat,
+                transaction: SurfaceControlCompat.Transaction
+            ) {
+                configuredBufferTransform =
+                    transaction.mBufferTransforms[frontBufferedLayerSurfaceControl]
+                        ?: BufferTransformHintResolver.UNKNOWN_TRANSFORM
+                if (initialFrontBufferLatch.count == 0L) {
+                    secondFrontBufferLatch.countDown()
+                }
+                initialFrontBufferLatch.countDown()
+            }
+        }
+        var renderer: GLFrontBufferedRenderer<Any>? = null
+        try {
+            val scenario = ActivityScenario.launch(FrontBufferedRendererTestActivity::class.java)
+                .moveToState(Lifecycle.State.CREATED)
+                .onActivity {
+                    surfaceView = it.getSurfaceView()
+                    it.getSurfaceView().holder.addCallback(surfaceHolderCallbacks)
+                    renderer = GLFrontBufferedRenderer(it.getSurfaceView(), callbacks)
+                }
+
+            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+                renderer?.renderFrontBufferedLayer(Any())
+            }
+
+            assertTrue(initialFrontBufferLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            val automation = InstrumentationRegistry.getInstrumentation().uiAutomation
+            assertTrue(automation.setRotation(UiAutomation.ROTATION_FREEZE_180))
+            automation.waitForIdle(1000, 3000)
+
+            renderer?.renderFrontBufferedLayer(Any())
+
+            assertTrue(secondFrontBufferLatch.await(3000, TimeUnit.MILLISECONDS))
+
+            assertEquals(
+                BufferTransformer().invertBufferTransform(bufferTransform),
+                configuredBufferTransform
+            )
+        } finally {
+            renderer.blockingRelease(10000)
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.Q)
     private fun GLFrontBufferedRenderer<*>?.blockingRelease(timeoutMillis: Long = 3000) {
         if (this != null) {
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 1001bdb..6c30b4c 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
@@ -29,8 +29,6 @@
 import androidx.graphics.opengl.egl.EGLManager
 import androidx.graphics.opengl.egl.EGLSpec
 import androidx.graphics.surface.SurfaceControlCompat
-import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_270
-import androidx.graphics.surface.SurfaceControlCompat.Companion.BUFFER_TRANSFORM_ROTATE_90
 import androidx.opengl.EGLExt.Companion.EGL_ANDROID_NATIVE_FENCE_SYNC
 import androidx.opengl.EGLExt.Companion.EGL_KHR_FENCE_SYNC
 import java.util.concurrent.ConcurrentLinkedQueue
@@ -264,11 +262,6 @@
      */
     private val mHardwareBufferUsageFlags: Long
 
-    /**
-     * Calculates the corresponding projection based on buffer transform hints
-     */
-    private val mBufferTransform = BufferTransformer()
-
     init {
         mParentRenderLayer.setParentLayerCallbacks(mParentLayerCallback)
         val renderer = if (glRenderer == null) {
@@ -304,18 +297,8 @@
                 }
                 .build()
 
-            val transformHint = mParentRenderLayer.getBufferTransformHint()
-            val bufferWidth: Int
-            val bufferHeight: Int
-            if (transformHint == BUFFER_TRANSFORM_ROTATE_90 ||
-                transformHint == BUFFER_TRANSFORM_ROTATE_270
-            ) {
-                bufferWidth = height
-                bufferHeight = width
-            } else {
-                bufferWidth = width
-                bufferHeight = height
-            }
+            val bufferWidth = mParentRenderLayer.getBufferWidth()
+            val bufferHeight = mParentRenderLayer.getBufferHeight()
 
             // Create buffer pool for the multi-buffered layer
             // The flags here are identical to those used for buffers in the front buffered layer
@@ -338,11 +321,8 @@
             val frontBufferedLayerRenderer =
                 createFrontBufferedLayerRenderer(
                     frontBufferedSurfaceControl,
-                    width,
-                    height,
                     bufferWidth,
                     bufferHeight,
-                    transformHint,
                     mHardwareBufferUsageFlags
                 )
             mFrontBufferedRenderTarget = mGLRenderer.createRenderTarget(
@@ -506,15 +486,10 @@
 
     private fun createFrontBufferedLayerRenderer(
         frontBufferedLayerSurfaceControl: SurfaceControlCompat,
-        width: Int,
-        height: Int,
         bufferWidth: Int,
         bufferHeight: Int,
-        transformHint: Int,
         usageFlags: Long
     ): FrameBufferRenderer {
-        val inverseTransform = mBufferTransform.invertBufferTransform(transformHint)
-        mBufferTransform.computeTransform(width, height, inverseTransform)
         return FrameBufferRenderer(
             object : FrameBufferRenderer.RenderCallback {
                 private fun createFrontBufferLayer(usageFlags: Long): HardwareBuffer {
@@ -547,14 +522,15 @@
                     mActiveSegment.next { param ->
                         mCallback.onDrawFrontBufferedLayer(
                             eglManager,
-                            mBufferTransform.glWidth,
-                            mBufferTransform.glHeight,
-                            mBufferTransform.transform,
+                            mParentRenderLayer.getBufferWidth(),
+                            mParentRenderLayer.getBufferHeight(),
+                            mParentRenderLayer.getTransform(),
                             param
                         )
                     }
                 }
 
+                @SuppressLint("WrongConstant")
                 @WorkerThread
                 override fun onDrawComplete(
                     frameBuffer: FrameBuffer,
@@ -569,7 +545,8 @@
                             syncFenceCompat
                         )
                         .setVisibility(frontBufferedLayerSurfaceControl, true)
-                    if (transformHint != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
+                    val inverseTransform = mParentRenderLayer.getInverseBufferTransform()
+                    if (inverseTransform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
                         transaction.setBufferTransform(
                             frontBufferedLayerSurfaceControl,
                             inverseTransform
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
index b217dd9..a13a2a9 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/ParentRenderLayer.kt
@@ -32,11 +32,29 @@
 internal interface ParentRenderLayer<T> {
 
     /**
-     * Obtains a pre-rotation hint to configure buffer content. This is helpful to
-     * avoid unnecessary GPU composition for the purposes of rotating buffer content to
-     * match display orientation
+     * Returns the inverse of the pre-rotation hint to configure buffer content. This is helpful
+     * to avoid unnecessary GPU composition for the purposes of rotating buffer content to
+     * match display orientation. Because this value is already inverted from the buffer transform
+     * hint, consumers can pass the result of this method directly into
+     * SurfaceControl.Transaction#setBufferTransform to handle pre-rotation
      */
-    fun getBufferTransformHint(): Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+    fun getInverseBufferTransform(): Int = BufferTransformHintResolver.UNKNOWN_TRANSFORM
+
+    /**
+     * Return the suggested width used for buffers taking into account pre-rotation transformations
+     */
+    fun getBufferWidth(): Int
+
+    /**
+     * Return the suggested height used for buffers taking into account pre-rotation transformations
+     */
+    fun getBufferHeight(): Int
+
+    /**
+     * Return the 4 x 4 transformation matrix represented as a 1 dimensional
+     * float array of 16 values
+     */
+    fun getTransform(): FloatArray
 
     /**
      * Modify the provided [SurfaceControlCompat.Transaction] to reparent the provided
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
index c31a83f..70ebccf 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/SurfaceViewRenderLayer.kt
@@ -60,7 +60,7 @@
                 width: Int,
                 height: Int
             ) {
-                transformHint = getBufferTransformHint()
+                transformHint = mTransformResolver.getBufferTransformHint(surfaceView)
                 inverse = mBufferTransform.invertBufferTransform(transformHint)
                 mBufferTransform.computeTransform(width, height, inverse)
                 mParentSurfaceControl?.release()
@@ -74,9 +74,13 @@
         })
     }
 
-    override fun getBufferTransformHint(): Int {
-        return mTransformResolver.getBufferTransformHint(surfaceView)
-    }
+    override fun getInverseBufferTransform(): Int = inverse
+
+    override fun getBufferWidth(): Int = mBufferTransform.glWidth
+
+    override fun getBufferHeight(): Int = mBufferTransform.glHeight
+
+    override fun getTransform(): FloatArray = mBufferTransform.transform
 
     override fun buildReparentTransaction(
         child: SurfaceControlCompat,
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index 66a547e..905db04 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -189,6 +189,11 @@
      * An atomic set of changes to a set of [SurfaceControlCompat].
      */
     class Transaction : AutoCloseable {
+        /**
+         * internal mapping of buffer transforms used for testing purposes
+         */
+        internal val mBufferTransforms = HashMap<SurfaceControlCompat, Int>()
+
         private val mImpl = createImpl()
 
         /**
@@ -447,6 +452,7 @@
             surfaceControl: SurfaceControlCompat,
             @BufferTransform transformation: Int
         ): Transaction {
+            mBufferTransforms[surfaceControl] = transformation
             mImpl.setBufferTransform(surfaceControl.scImpl, transformation)
             return this
         }
@@ -457,6 +463,7 @@
          * called to release the transaction.
          */
         fun commit() {
+            mBufferTransforms.clear()
             mImpl.commit()
         }
 
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index 7b2b08d..081ecbd 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -722,23 +722,23 @@
   }
 
   public final class MenstruationFlowRecord implements androidx.health.connect.client.records.Record {
-    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public String? getFlow();
+    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public int getFlow();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final String? flow;
+    property public final int flow;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
+    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Companion Companion;
+    field public static final int FLOW_HEAVY = 3; // 0x3
+    field public static final int FLOW_LIGHT = 1; // 0x1
+    field public static final int FLOW_MEDIUM = 2; // 0x2
+    field public static final int FLOW_UNKNOWN = 0; // 0x0
   }
 
-  public static final class MenstruationFlowRecord.Flow {
-    field public static final String HEAVY = "heavy";
-    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Flow INSTANCE;
-    field public static final String LIGHT = "light";
-    field public static final String MEDIUM = "medium";
-    field public static final String SPOTTING = "spotting";
+  public static final class MenstruationFlowRecord.Companion {
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index 7b2b08d..081ecbd 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -722,23 +722,23 @@
   }
 
   public final class MenstruationFlowRecord implements androidx.health.connect.client.records.Record {
-    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public String? getFlow();
+    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public int getFlow();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final String? flow;
+    property public final int flow;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
+    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Companion Companion;
+    field public static final int FLOW_HEAVY = 3; // 0x3
+    field public static final int FLOW_LIGHT = 1; // 0x1
+    field public static final int FLOW_MEDIUM = 2; // 0x2
+    field public static final int FLOW_UNKNOWN = 0; // 0x0
   }
 
-  public static final class MenstruationFlowRecord.Flow {
-    field public static final String HEAVY = "heavy";
-    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Flow INSTANCE;
-    field public static final String LIGHT = "light";
-    field public static final String MEDIUM = "medium";
-    field public static final String SPOTTING = "spotting";
+  public static final class MenstruationFlowRecord.Companion {
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.Record {
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 4cb4ac9..a9c1a0f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -740,23 +740,23 @@
   }
 
   public final class MenstruationFlowRecord implements androidx.health.connect.client.records.InstantaneousRecord {
-    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional String? flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
-    method public String? getFlow();
+    ctor public MenstruationFlowRecord(java.time.Instant time, java.time.ZoneOffset? zoneOffset, optional int flow, optional androidx.health.connect.client.records.metadata.Metadata metadata);
+    method public int getFlow();
     method public androidx.health.connect.client.records.metadata.Metadata getMetadata();
     method public java.time.Instant getTime();
     method public java.time.ZoneOffset? getZoneOffset();
-    property public final String? flow;
+    property public final int flow;
     property public androidx.health.connect.client.records.metadata.Metadata metadata;
     property public java.time.Instant time;
     property public java.time.ZoneOffset? zoneOffset;
+    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Companion Companion;
+    field public static final int FLOW_HEAVY = 3; // 0x3
+    field public static final int FLOW_LIGHT = 1; // 0x1
+    field public static final int FLOW_MEDIUM = 2; // 0x2
+    field public static final int FLOW_UNKNOWN = 0; // 0x0
   }
 
-  public static final class MenstruationFlowRecord.Flow {
-    field public static final String HEAVY = "heavy";
-    field public static final androidx.health.connect.client.records.MenstruationFlowRecord.Flow INSTANCE;
-    field public static final String LIGHT = "light";
-    field public static final String MEDIUM = "medium";
-    field public static final String SPOTTING = "spotting";
+  public static final class MenstruationFlowRecord.Companion {
   }
 
   public final class NutritionRecord implements androidx.health.connect.client.records.IntervalRecord {
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
index f5a6f70..6321e59 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/datatype/RecordsTypeNameMap.kt
@@ -49,6 +49,7 @@
 import androidx.health.connect.client.records.HeightRecord
 import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
 import androidx.health.connect.client.records.MenstruationFlowRecord
 import androidx.health.connect.client.records.NutritionRecord
@@ -119,6 +120,7 @@
         "SleepSession" to SleepSessionRecord::class,
         "SleepStage" to SleepStageRecord::class,
         "SpeedSeries" to SpeedRecord::class, // Keep legacy Series suffix
+        "IntermenstrualBleeding" to IntermenstrualBleedingRecord::class,
         "Steps" to StepsRecord::class,
         "StepsCadenceSeries" to StepsCadenceRecord::class, // Keep legacy Series suffix
         "SwimmingStrokes" to SwimmingStrokesRecord::class,
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
index 80ac8cc..8330756 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/ProtoToRecordConverters.kt
@@ -52,6 +52,7 @@
 import androidx.health.connect.client.records.HeightRecord
 import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
 import androidx.health.connect.client.records.MealType
 import androidx.health.connect.client.records.MenstruationFlowRecord
@@ -328,7 +329,12 @@
                 )
             "Menstruation" ->
                 MenstruationFlowRecord(
-                    flow = getEnum("flow"),
+                    flow =
+                        mapEnum(
+                            "flow",
+                            MenstruationFlowRecord.FLOW_TYPE_STRING_TO_INT_MAP,
+                            MenstruationFlowRecord.FLOW_UNKNOWN
+                        ),
                     time = time,
                     zoneOffset = zoneOffset,
                     metadata = metadata
@@ -630,6 +636,12 @@
                     endZoneOffset = endZoneOffset,
                     metadata = metadata
                 )
+            "IntermenstrualBleeding" ->
+                IntermenstrualBleedingRecord(
+                    time = time,
+                    zoneOffset = zoneOffset,
+                    metadata = metadata
+                )
             "Steps" ->
                 StepsRecord(
                     count = getLong("count"),
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
index 575dfc8..aa31176 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/converters/records/RecordToProtoConverters.kt
@@ -52,6 +52,7 @@
 import androidx.health.connect.client.records.HeightRecord
 import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
 import androidx.health.connect.client.records.MealType
 import androidx.health.connect.client.records.MenstruationFlowRecord
@@ -246,6 +247,8 @@
                 .setDataType(protoDataType("HeartRateVariabilityTinn"))
                 .apply { putValues("heartRateVariability", doubleVal(heartRateVariabilityMillis)) }
                 .build()
+        is IntermenstrualBleedingRecord ->
+            instantaneousProto().setDataType(protoDataType("IntermenstrualBleeding")).build()
         is LeanBodyMassRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("LeanBodyMass"))
@@ -254,7 +257,11 @@
         is MenstruationFlowRecord ->
             instantaneousProto()
                 .setDataType(protoDataType("Menstruation"))
-                .apply { flow?.let { putValues("flow", enumVal(it)) } }
+                .apply {
+                    enumValFromInt(flow, MenstruationFlowRecord.FLOW_TYPE_INT_TO_STRING_MAP)?.let {
+                        putValues("flow", it)
+                    }
+                }
                 .build()
         is OvulationTestRecord ->
             instantaneousProto()
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
index e80113c..bd72f99 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/permission/HealthPermission.kt
@@ -47,6 +47,7 @@
 import androidx.health.connect.client.records.HeightRecord
 import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
 import androidx.health.connect.client.records.MenstruationFlowRecord
 import androidx.health.connect.client.records.NutritionRecord
@@ -223,6 +224,9 @@
         const val READ_HEART_RATE_VARIABILITY =
             "android.permission.health.READ_HEART_RATE_VARIABILITY"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
+        const val READ_INTERMENSTRUAL_BLEEDING =
+            "android.permission.health.READ_INTERMENSTRUAL_BLEEDING"
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val READ_OXYGEN_SATURATION = "android.permission.health.READ_OXYGEN_SATURATION"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val READ_RESPIRATORY_RATE = "android.permission.health.READ_RESPIRATORY_RATE"
@@ -270,6 +274,9 @@
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val WRITE_HIP_CIRCUMFERENCE = "android.permission.health.WRITE_HIP_CIRCUMFERENCE"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
+        const val WRITE_INTERMENSTRUAL_BLEEDING =
+            "android.permission.health.WRITE_INTERMENSTRUAL_BLEEDING"
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val WRITE_LEAN_BODY_MASS = "android.permission.health.WRITE_LEAN_BODY_MASS"
         @RestrictTo(RestrictTo.Scope.LIBRARY)
         const val WRITE_WAIST_CIRCUMFERENCE = "android.permission.health.WRITE_WAIST_CIRCUMFERENCE"
@@ -378,6 +385,8 @@
                 HipCircumferenceRecord::class to
                     READ_HIP_CIRCUMFERENCE.substringAfter(READ_PERMISSION_PREFIX),
                 HydrationRecord::class to READ_HYDRATION.substringAfter(READ_PERMISSION_PREFIX),
+                IntermenstrualBleedingRecord::class to
+                    READ_INTERMENSTRUAL_BLEEDING.substringAfter(READ_PERMISSION_PREFIX),
                 LeanBodyMassRecord::class to
                     READ_LEAN_BODY_MASS.substringAfter(READ_PERMISSION_PREFIX),
                 MenstruationFlowRecord::class to
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
index 1b74254..ccaa87b 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/BloodGlucoseRecord.kt
@@ -15,8 +15,8 @@
  */
 package androidx.health.connect.client.records
 
+import androidx.annotation.IntDef
 import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
 import androidx.health.connect.client.records.BloodGlucoseRecord.SpecimenSource
 import androidx.health.connect.client.records.metadata.Metadata
 import androidx.health.connect.client.units.BloodGlucose
@@ -141,15 +141,15 @@
      * @suppress
      */
     @Retention(AnnotationRetention.SOURCE)
-    @StringDef(
+    @IntDef(
         value =
             [
-                SpecimenSource.INTERSTITIAL_FLUID,
-                SpecimenSource.CAPILLARY_BLOOD,
-                SpecimenSource.PLASMA,
-                SpecimenSource.SERUM,
-                SpecimenSource.TEARS,
-                SpecimenSource.WHOLE_BLOOD,
+                SPECIMEN_SOURCE_INTERSTITIAL_FLUID,
+                SPECIMEN_SOURCE_CAPILLARY_BLOOD,
+                SPECIMEN_SOURCE_PLASMA,
+                SPECIMEN_SOURCE_SERUM,
+                SPECIMEN_SOURCE_TEARS,
+                SPECIMEN_SOURCE_WHOLE_BLOOD,
             ]
     )
     @RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -160,13 +160,13 @@
      * @suppress
      */
     @Retention(AnnotationRetention.SOURCE)
-    @StringDef(
+    @IntDef(
         value =
             [
-                RelationToMeal.GENERAL,
-                RelationToMeal.FASTING,
-                RelationToMeal.BEFORE_MEAL,
-                RelationToMeal.AFTER_MEAL,
+                RELATION_TO_MEAL_GENERAL,
+                RELATION_TO_MEAL_FASTING,
+                RELATION_TO_MEAL_BEFORE_MEAL,
+                RELATION_TO_MEAL_AFTER_MEAL,
             ]
     )
     annotation class RelationToMeals
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
index 5e1809e..3bb5153 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseEventRecord.kt
@@ -74,13 +74,13 @@
      */
     internal object EventType {
         /**
-         * Explicit pause during an workout, requested by the user (by clicking a pause button in
+         * Explicit pause during a workout, requested by the user (by clicking a pause button in
          * the session UI). Movement happening during pause should not contribute to session
          * metrics.
          */
         const val PAUSE = "pause"
         /**
-         * Auto-detected periods of rest during an workout. There should be no user movement
+         * Auto-detected periods of rest during a workout. There should be no user movement
          * detected during rest and any movement detected should finish rest event.
          */
         const val REST = "rest"
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/IntermenstrualBleedingRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/IntermenstrualBleedingRecord.kt
new file mode 100644
index 0000000..9de56a8
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/IntermenstrualBleedingRecord.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.health.connect.client.records
+
+import androidx.annotation.RestrictTo
+import androidx.health.connect.client.records.metadata.Metadata
+import java.time.Instant
+import java.time.ZoneOffset
+
+/** Captures an instance of user's intermenstrual bleeding, also known as spotting. */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class IntermenstrualBleedingRecord(
+    override val time: Instant,
+    override val zoneOffset: ZoneOffset?,
+    override val metadata: Metadata = Metadata.EMPTY,
+) : InstantaneousRecord {
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as IntermenstrualBleedingRecord
+
+        if (time != other.time) return false
+        if (zoneOffset != other.zoneOffset) return false
+        if (metadata != other.metadata) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = time.hashCode()
+        result = 31 * result + (zoneOffset?.hashCode() ?: 0)
+        result = 31 * result + metadata.hashCode()
+        return result
+    }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MenstruationFlowRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MenstruationFlowRecord.kt
index c7d2aa5..6d49491 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MenstruationFlowRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/MenstruationFlowRecord.kt
@@ -15,25 +15,21 @@
  */
 package androidx.health.connect.client.records
 
+import androidx.annotation.IntDef
 import androidx.annotation.RestrictTo
-import androidx.annotation.StringDef
 import androidx.health.connect.client.records.metadata.Metadata
 import java.time.Instant
 import java.time.ZoneOffset
 
 /**
- * Captures a description of how heavy a user's menstrual flow was (spotting, light, medium, or
- * heavy). Each record represents a description of how heavy the user's menstrual bleeding was.
+ * Captures a description of how heavy a user's menstrual flow was (light, medium, or heavy). Each
+ * record represents a description of how heavy the user's menstrual bleeding was.
  */
 public class MenstruationFlowRecord(
     override val time: Instant,
     override val zoneOffset: ZoneOffset?,
-    /**
-     * How heavy the user's menstrual flow was. Optional field. Allowed values: [Flow].
-     *
-     * @see Flow
-     */
-    @property:Flows public val flow: String? = null,
+    /** How heavy the user's menstrual flow was. Optional field. */
+    @property:Flows public val flow: Int = FLOW_UNKNOWN,
     override val metadata: Metadata = Metadata.EMPTY,
 ) : InstantaneousRecord {
     override fun equals(other: Any?): Boolean {
@@ -49,36 +45,35 @@
     }
 
     override fun hashCode(): Int {
-        var result = 0
-        result = 31 * result + flow.hashCode()
+        var result = flow
         result = 31 * result + time.hashCode()
         result = 31 * result + (zoneOffset?.hashCode() ?: 0)
         result = 31 * result + metadata.hashCode()
         return result
     }
 
-    /** How heavy the user's menstruation flow was. */
-    public object Flow {
-        const val SPOTTING = "spotting"
-        const val LIGHT = "light"
-        const val MEDIUM = "medium"
-        const val HEAVY = "heavy"
-    }
+    companion object {
+        const val FLOW_UNKNOWN = 0
+        const val FLOW_LIGHT = 1
+        const val FLOW_MEDIUM = 2
+        const val FLOW_HEAVY = 3
 
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmField
+        val FLOW_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
+            mapOf("light" to FLOW_LIGHT, "medium" to FLOW_MEDIUM, "heavy" to FLOW_HEAVY)
+
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @JvmField
+        val FLOW_TYPE_INT_TO_STRING_MAP: Map<Int, String> =
+            FLOW_TYPE_STRING_TO_INT_MAP.entries.associateBy({ it.value }, { it.key })
+    }
     /**
      * How heavy the user's menstruation flow was.
      * @suppress
      */
     @Retention(AnnotationRetention.SOURCE)
-    @StringDef(
-        value =
-            [
-                MenstruationFlowRecord.Flow.SPOTTING,
-                MenstruationFlowRecord.Flow.LIGHT,
-                MenstruationFlowRecord.Flow.MEDIUM,
-                MenstruationFlowRecord.Flow.HEAVY,
-            ]
-    )
+    @IntDef(value = [FLOW_UNKNOWN, FLOW_LIGHT, FLOW_MEDIUM, FLOW_HEAVY])
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     annotation class Flows
 }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index bcd4c96..0291436 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -50,10 +50,10 @@
 import androidx.health.connect.client.records.HeightRecord
 import androidx.health.connect.client.records.HipCircumferenceRecord
 import androidx.health.connect.client.records.HydrationRecord
+import androidx.health.connect.client.records.IntermenstrualBleedingRecord
 import androidx.health.connect.client.records.LeanBodyMassRecord
 import androidx.health.connect.client.records.MealType
 import androidx.health.connect.client.records.MenstruationFlowRecord
-import androidx.health.connect.client.records.MenstruationFlowRecord.Flow
 import androidx.health.connect.client.records.NutritionRecord
 import androidx.health.connect.client.records.OvulationTestRecord
 import androidx.health.connect.client.records.OxygenSaturationRecord
@@ -468,6 +468,19 @@
     }
 
     @Test
+    fun testIntermenstrualBleeding() {
+        val data =
+            IntermenstrualBleedingRecord(
+                time = START_TIME,
+                zoneOffset = END_ZONE_OFFSET,
+                metadata = TEST_METADATA
+            )
+
+        checkProtoAndRecordTypeNameMatch(data)
+        assertThat(toRecord(data.toProto())).isEqualTo(data)
+    }
+
+    @Test
     fun testLeanBodyMass() {
         val data =
             LeanBodyMassRecord(
@@ -485,7 +498,7 @@
     fun testMenstruation() {
         val data =
             MenstruationFlowRecord(
-                flow = Flow.HEAVY,
+                flow = MenstruationFlowRecord.FLOW_HEAVY,
                 time = START_TIME,
                 zoneOffset = END_ZONE_OFFSET,
                 metadata = TEST_METADATA
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveListenerConfig.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveListenerConfig.kt
index c7df7b5..e43d2d2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveListenerConfig.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/PassiveListenerConfig.kt
@@ -55,6 +55,14 @@
             .toSet()
     )
 
+    internal fun isValidPassiveGoal(): Boolean {
+        // Check if the registered goals are also tracked
+        for (passiveGoal: PassiveGoal in dailyGoals) {
+            if (!dataTypes.contains(passiveGoal.dataTypeCondition.dataType)) return false
+        }
+        return true
+    }
+
     /** Builder for [PassiveListenerConfig] instances. */
     public class Builder {
         private var dataTypes: Set<DataType<*, *>> = emptySet()
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
index 4cf36a7..ab50f15 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClient.kt
@@ -17,6 +17,7 @@
 package androidx.health.services.client.impl
 
 import android.content.Context
+import android.os.RemoteException
 import androidx.annotation.RestrictTo
 import androidx.annotation.VisibleForTesting
 import androidx.core.content.ContextCompat
@@ -71,10 +72,20 @@
     ): ListenableFuture<Void> {
         return executeWithVersionCheck(
             { remoteService, resultFuture ->
-                remoteService.registerPassiveListenerService(
-                    PassiveListenerServiceRegistrationRequest(packageName, service.name, config),
-                    StatusCallback(resultFuture)
-                )
+                if (config.isValidPassiveGoal()) {
+                    remoteService.registerPassiveListenerService(
+                        PassiveListenerServiceRegistrationRequest(
+                            packageName,
+                            service.name,
+                            config
+                        ),
+                        StatusCallback(resultFuture)
+                    )
+                } else {
+                    resultFuture.setException(
+                        RemoteException("DataType for the requested passive goal is not tracked")
+                    )
+                }
             },
             /* minApiVersion= */ 4
         )
@@ -100,11 +111,17 @@
             PassiveListenerCallbackCache.INSTANCE.getOrCreate(packageName, executor, callback)
         val future =
             registerListener(callbackStub.listenerKey) { service, result: SettableFuture<Void?> ->
-                service.registerPassiveListenerCallback(
-                    PassiveListenerCallbackRegistrationRequest(packageName, config),
-                    callbackStub,
-                    StatusCallback(result)
-                )
+                if (config.isValidPassiveGoal()) {
+                    service.registerPassiveListenerCallback(
+                        PassiveListenerCallbackRegistrationRequest(packageName, config),
+                        callbackStub,
+                        StatusCallback(result)
+                    )
+                } else {
+                    result.setException(
+                        RemoteException("DataType for the requested passive goal is not tracked")
+                    )
+                }
             }
         Futures.addCallback(
             future,
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
index a4479a2..fd4487f 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
@@ -28,11 +28,15 @@
 import androidx.health.services.client.data.DataType
 import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
 import androidx.health.services.client.data.DataTypeCondition
+import androidx.health.services.client.data.ExerciseInfo
+import androidx.health.services.client.data.ExerciseTrackedStatus
+import androidx.health.services.client.data.ExerciseType
 import androidx.health.services.client.data.HealthEvent
 import androidx.health.services.client.data.HealthEvent.Type.Companion.FALL_DETECTED
 import androidx.health.services.client.data.PassiveGoal
 import androidx.health.services.client.data.PassiveMonitoringUpdate
 import androidx.health.services.client.data.UserActivityInfo
+import androidx.health.services.client.data.UserActivityState.Companion.USER_ACTIVITY_EXERCISE
 import androidx.health.services.client.data.UserActivityState.Companion.USER_ACTIVITY_PASSIVE
 import androidx.health.services.client.impl.IPassiveListenerService
 import androidx.health.services.client.impl.event.PassiveListenerEvent
@@ -131,6 +135,41 @@
     }
 
     @Test
+    fun receivesUserActivityStateWithExerciseInfo() {
+        context.bindService(
+            Intent(context, FakeService::class.java),
+            connection,
+            Context.BIND_AUTO_CREATE
+        )
+        val listenerEvent = PassiveListenerEvent.createPassiveUpdateResponse(
+            PassiveMonitoringUpdateResponse(
+                PassiveMonitoringUpdate(
+                    DataPointContainer(listOf()),
+                    listOf(
+                        UserActivityInfo(
+                            USER_ACTIVITY_EXERCISE,
+                            ExerciseInfo(
+                                ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS,
+                                ExerciseType.RUNNING
+                            ),
+                            42.instant()
+                        )
+                    )
+                )
+            )
+        )
+
+        stub.onPassiveListenerEvent(listenerEvent)
+
+        val activityInfo = service.userActivityReceived!!
+        assertThat(activityInfo.userActivityState).isEqualTo(USER_ACTIVITY_EXERCISE)
+        assertThat(activityInfo.stateChangeTime).isEqualTo(42.instant())
+        assertThat(activityInfo.exerciseInfo!!.exerciseType).isEqualTo(ExerciseType.RUNNING)
+        assertThat(activityInfo.exerciseInfo!!.exerciseTrackedStatus)
+            .isEqualTo(ExerciseTrackedStatus.OWNED_EXERCISE_IN_PROGRESS)
+    }
+
+    @Test
     fun receivesGoalCompleted() {
         context.bindService(
             Intent(context, FakeService::class.java),
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
index 13e1f40..93121ba 100644
--- a/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/impl/ServiceBackedPassiveMonitoringClientTest.kt
@@ -20,6 +20,7 @@
 import android.content.ComponentName
 import android.content.Intent
 import android.os.Looper
+import android.os.RemoteException
 import androidx.health.services.client.PassiveListenerCallback
 import androidx.health.services.client.PassiveListenerService
 import androidx.health.services.client.data.ComparisonType.Companion.GREATER_THAN
@@ -92,7 +93,7 @@
     }
 
     @Test
-    fun registersPassiveListenerService() {
+    fun registersPassiveListenerService_success() {
         val config = PassiveListenerConfig(
             dataTypes = setOf(STEPS_DAILY, CALORIES_DAILY),
             shouldUserActivityInfoBeRequested = true,
@@ -115,6 +116,34 @@
     }
 
     @Test
+    fun registersPassiveListenerService_fail() {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(
+                PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
+            ),
+            healthEventTypes = setOf()
+        )
+
+        val future = client.setPassiveListenerServiceAsync(FakeListenerService::class.java, config)
+        shadowOf(Looper.getMainLooper()).idle()
+
+        // Return value of future.get() is not used, but verifying no exceptions are thrown.
+        var exception: Exception? = null
+        try {
+            future.get()
+        } catch (e: Exception) {
+            exception = e
+        }
+
+        assertThat(exception).isNotNull()
+        assertThat(exception?.cause).isInstanceOf(RemoteException::class.java)
+        assertThat(exception).hasMessageThat()
+            .contains("DataType for the requested passive goal is not tracked")
+    }
+
+    @Test
     fun registersPassiveListenerCallback() {
         val config = PassiveListenerConfig(
             dataTypes = setOf(STEPS_DAILY),
@@ -136,6 +165,27 @@
     }
 
     @Test
+    fun registersPassiveListenerCallback_fail() {
+        val config = PassiveListenerConfig(
+            dataTypes = setOf(CALORIES_DAILY),
+            shouldUserActivityInfoBeRequested = true,
+            dailyGoals = setOf(
+                PassiveGoal(DataTypeCondition(STEPS_DAILY, 87, GREATER_THAN))
+            ),
+            healthEventTypes = setOf()
+        )
+        val callback = FakeCallback()
+
+        client.setPassiveListenerCallback(config, callback)
+        shadowOf(Looper.getMainLooper()).idle()
+
+        assertThat(fakeService.registerCallbackRequests).hasSize(0)
+        assertThat(callback.onRegistrationFailedThrowables).hasSize(1)
+        assertThat(callback.onRegistrationFailedThrowables[0]).hasMessageThat()
+            .contains("DataType for the requested passive goal is not tracked")
+    }
+
+    @Test
     fun callbackReceivesDataPointsAndUserActivityInfo() {
         shadowOf(Looper.getMainLooper()).idle() // ?????
         val config = PassiveListenerConfig(
diff --git a/libraryversions.toml b/libraryversions.toml
index cc12935..5147f09 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -16,8 +16,9 @@
 CAMERA = "1.3.0-alpha01"
 CAMERA_PIPE = "1.0.0-alpha01"
 CARDVIEW = "1.1.0-alpha01"
-CAR_APP = "1.3.0-beta02"
-COLLECTION = "1.3.0-dev01"
+CAR_APP = "1.3.0-rc01"
+COLLECTION = "1.3.0-alpha03"
+COLLECTION_KMP = "1.3.0-dev01"
 COMPOSE = "1.4.0-alpha02"
 COMPOSE_COMPILER = "1.4.0-alpha01"
 COMPOSE_MATERIAL3 = "1.1.0-alpha02"
@@ -43,7 +44,8 @@
 CURSORADAPTER = "1.1.0-alpha01"
 CUSTOMVIEW = "1.2.0-alpha03"
 CUSTOMVIEW_POOLINGCONTAINER = "1.1.0-alpha01"
-DATASTORE = "1.1.0-dev01"
+DATASTORE = "1.1.0-alpha01"
+DATASTORE_KMP = "1.1.0-dev01"
 DOCUMENTFILE = "1.1.0-alpha02"
 DRAGANDDROP = "1.1.0-alpha01"
 DRAWERLAYOUT = "1.2.0-alpha02"
@@ -100,7 +102,7 @@
 RESOURCEINSPECTION = "1.1.0-alpha01"
 ROOM = "2.6.0-alpha01"
 SAVEDSTATE = "1.3.0-alpha01"
-SECURITY = "1.1.0-alpha05"
+SECURITY = "1.1.0-alpha04"
 SECURITY_APP_AUTHENTICATOR = "1.0.0-alpha03"
 SECURITY_APP_AUTHENTICATOR_TESTING = "1.0.0-alpha02"
 SECURITY_BIOMETRIC = "1.0.0-alpha01"
@@ -131,7 +133,7 @@
 VIEWPAGER = "1.1.0-alpha02"
 VIEWPAGER2 = "1.2.0-alpha01"
 WEAR = "1.3.0-alpha04"
-WEAR_COMPOSE = "1.1.0-rc01"
+WEAR_COMPOSE = "1.2.0-alpha01"
 WEAR_INPUT = "1.2.0-alpha03"
 WEAR_INPUT_TESTING = "1.2.0-alpha03"
 WEAR_ONGOING = "1.1.0-alpha01"
@@ -162,7 +164,7 @@
 CAMERA = { group = "androidx.camera", atomicGroupVersion = "versions.CAMERA" }
 CARDVIEW = { group = "androidx.cardview", atomicGroupVersion = "versions.CARDVIEW" }
 CAR_APP = { group = "androidx.car.app", atomicGroupVersion = "versions.CAR_APP" }
-COLLECTION = { group = "androidx.collection", atomicGroupVersion = "versions.COLLECTION" }
+COLLECTION = { group = "androidx.collection", atomicGroupVersion = "versions.COLLECTION", multiplatformGroupVersion = "versions.COLLECTION_KMP" }
 COMPOSE_ANIMATION = { group = "androidx.compose.animation", atomicGroupVersion = "versions.COMPOSE" }
 COMPOSE_COMPILER = { group = "androidx.compose.compiler", atomicGroupVersion = "versions.COMPOSE_COMPILER" }
 COMPOSE_DESKTOP = { group = "androidx.compose.desktop", atomicGroupVersion = "versions.COMPOSE" }
@@ -180,7 +182,7 @@
 CREDENTIALS = { group = "androidx.credentials", atomicGroupVersion = "versions.CREDENTIALS" }
 CURSORADAPTER = { group = "androidx.cursoradapter", atomicGroupVersion = "versions.CURSORADAPTER" }
 CUSTOMVIEW = { group = "androidx.customview" }
-DATASTORE = { group = "androidx.datastore", atomicGroupVersion = "versions.DATASTORE" }
+DATASTORE = { group = "androidx.datastore", atomicGroupVersion = "versions.DATASTORE", multiplatformGroupVersion = "versions.DATASTORE_KMP" }
 DOCUMENTFILE = { group = "androidx.documentfile", atomicGroupVersion = "versions.DOCUMENTFILE" }
 DRAGANDDROP = { group = "androidx.draganddrop", atomicGroupVersion = "versions.DRAGANDDROP" }
 DRAWERLAYOUT = { group = "androidx.drawerlayout", atomicGroupVersion = "versions.DRAWERLAYOUT" }
diff --git a/lifecycle/lifecycle-common/build.gradle b/lifecycle/lifecycle-common/build.gradle
index 70a07f9..061f3b0 100644
--- a/lifecycle/lifecycle-common/build.gradle
+++ b/lifecycle/lifecycle-common/build.gradle
@@ -15,14 +15,17 @@
  */
 
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 plugins {
     id("AndroidXPlugin")
     id("java-library")
+    id("kotlin")
 }
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
+    api(libs.kotlinStdlib)
 
     testImplementation(libs.junit)
     testImplementation(libs.mockitoCore4)
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.java b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.java
deleted file mode 100644
index cd2e072..0000000
--- a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.lifecycle;
-
-
-import androidx.annotation.NonNull;
-
-class CompositeGeneratedAdaptersObserver implements LifecycleEventObserver {
-
-    private final GeneratedAdapter[] mGeneratedAdapters;
-
-    CompositeGeneratedAdaptersObserver(GeneratedAdapter[] generatedAdapters) {
-        mGeneratedAdapters = generatedAdapters;
-    }
-
-    @Override
-    public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
-        MethodCallsLogger logger = new MethodCallsLogger();
-        for (GeneratedAdapter mGenerated: mGeneratedAdapters) {
-            mGenerated.callMethods(source, event, false, logger);
-        }
-        for (GeneratedAdapter mGenerated: mGeneratedAdapters) {
-            mGenerated.callMethods(source, event, true, logger);
-        }
-    }
-}
diff --git a/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt
new file mode 100644
index 0000000..a2ee026
--- /dev/null
+++ b/lifecycle/lifecycle-common/src/main/java/androidx/lifecycle/CompositeGeneratedAdaptersObserver.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.lifecycle
+
+internal class CompositeGeneratedAdaptersObserver(
+    private val generatedAdapters: Array<GeneratedAdapter>
+) :
+    LifecycleEventObserver {
+    override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
+        val logger = MethodCallsLogger()
+        for (adapter in generatedAdapters) {
+            adapter.callMethods(source, event, false, logger)
+        }
+        for (adapter in generatedAdapters) {
+            adapter.callMethods(source, event, true, logger)
+        }
+    }
+}
\ No newline at end of file
diff --git a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java
index 13e58d8..1fe6ced 100644
--- a/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java
+++ b/lifecycle/lifecycle-livedata/src/main/java/androidx/lifecycle/MediatorLiveData.java
@@ -86,8 +86,8 @@
     }
 
     /**
-     * Starts to listen the given {@code source} LiveData, {@code onChanged} observer will be called
-     * when {@code source} value was changed.
+     * Starts to listen to the given {@code source} LiveData, {@code onChanged} observer will be
+     * called when {@code source} value was changed.
      * <p>
      * {@code onChanged} callback will be called only when this {@code MediatorLiveData} is active.
      * <p> If the given LiveData is already added as a source but with a different Observer,
diff --git a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
index 57423dc..312cadd 100644
--- a/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
+++ b/lint-checks/src/main/java/androidx/build/lint/AndroidXIssueRegistry.kt
@@ -67,6 +67,8 @@
                 NullabilityAnnotationsDetector.ISSUE,
                 IgnoreClassLevelDetector.ISSUE,
                 ExperimentalPropertyAnnotationDetector.ISSUE,
+                // MissingJvmDefaultWithCompatibilityDetector is intentionally left out of the
+                // registry, see comments on the class for more details.
             )
         }
     }
diff --git a/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
new file mode 100644
index 0000000..ebcb9f9
--- /dev/null
+++ b/lint-checks/src/main/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetector.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.build.lint
+
+import com.android.tools.lint.client.api.UElementHandler
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Incident
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.LocationType
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.android.tools.lint.detector.api.isKotlin
+import com.intellij.psi.PsiJvmModifiersOwner
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.UMethod
+
+/**
+ * This lint check is meant to help maintain binary compatibility in a one-time transition to using
+ * `-Xjvm-default=all`. Applicable interfaces which existed before `-Xjvm-default=all` was used must
+ * be annotated with @JvmDefaultWithCompatibility. However, after the initial change, new interfaces
+ * should not use @JvmDefaultWithCompatibility.
+ *
+ * Because this check is only meant to be used once, it should not be added to the issue registry.
+ */
+class MissingJvmDefaultWithCompatibilityDetector : Detector(), SourceCodeScanner {
+
+    override fun getApplicableUastTypes() = listOf(UClass::class.java)
+
+    override fun createUastHandler(context: JavaContext): UElementHandler {
+        return InterfaceChecker(context)
+    }
+
+    private inner class InterfaceChecker(val context: JavaContext) : UElementHandler() {
+        override fun visitClass(node: UClass) {
+            if (!isKotlin(node)) return
+            if (!node.isInterface) return
+            if (node.annotatedWithAnyOf(
+                    // If the interface is not stable, it doesn't need the annotation
+                    BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS +
+                        // If the interface already has the annotation, it doesn't need it again
+                        JVM_DEFAULT_WITH_COMPATIBILITY)
+            ) return
+
+            val stableMethods = node.stableMethods()
+            if (stableMethods.any { it.hasDefaultImplementation() }) {
+                val reason = "This interface must be annotated with @JvmDefaultWithCompatibility " +
+                    "because it has a stable method with a default implementation"
+                reportIncident(node, reason)
+                return
+            }
+
+            if (stableMethods.any { it.hasParameterWithDefaultValue() }) {
+                val reason = "This interface must be annotated with @JvmDefaultWithCompatibility " +
+                    "because it has a stable method with a parameter with a default value"
+                reportIncident(node, reason)
+                return
+            }
+
+            // This only checks the interfaces that this interface directly extends, which means if
+            // A extends B extends C and C is @JvmDefaultWithCompatibility, there will need to be
+            // two passes of running the check to annotate A and B.
+            if (node.interfaces.any {
+                    it.annotatedWithAnyOf(listOf(JVM_DEFAULT_WITH_COMPATIBILITY))
+            }) {
+                val reason = "This interface must be annotated with @JvmDefaultWithCompatibility " +
+                    "because it implements an interface which uses this annotation"
+                reportIncident(node, reason)
+                return
+            }
+        }
+
+        private fun reportIncident(node: UClass, reason: String) {
+            val location = context.getLocation(node, LocationType.ALL)
+            val fix = fix()
+                .name("Annotate with @JvmDefaultWithCompatibility")
+                .annotate(JVM_DEFAULT_WITH_COMPATIBILITY)
+                .range(location)
+                .autoFix()
+                .build()
+
+            val incident = Incident(context)
+                .fix(fix)
+                .issue(ISSUE)
+                .location(location)
+                .message(reason)
+                .scope(node)
+
+            context.report(incident)
+        }
+
+        /**
+         * Returns a list of the class's stable methods (methods not labelled as experimental).
+         */
+        private fun UClass.stableMethods(): List<UMethod> =
+            methods.filter {
+                !it.annotatedWithAnyOf(BanInappropriateExperimentalUsage.APPLICABLE_ANNOTATIONS)
+            }
+
+        /**
+         * Checks if the element is annotated with any of the provided (fully qualified) annotation
+         * names. This uses `PsiJvmModifiersOwner` because it seems to be the one common parent of
+         * `UClass` and `UMethod` with an `annotations` property.
+         */
+        private fun PsiJvmModifiersOwner.annotatedWithAnyOf(
+            qualifiedAnnotationNames: List<String>
+        ): Boolean = annotations.any { qualifiedAnnotationNames.contains(it.qualifiedName) }
+
+        private fun UMethod.hasDefaultImplementation(): Boolean =
+            uastBody != null
+
+        private fun UMethod.hasParameterWithDefaultValue(): Boolean =
+            uastParameters.any { param -> param.uastInitializer != null }
+    }
+
+    companion object {
+        val ISSUE = Issue.create(
+            "MissingJvmDefaultWithCompatibility",
+            "The @JvmDefaultWithCompatibility needs to be used with on applicable " +
+                "interfaces when `-Xjvm-default=all` is turned on to preserve compatibility.",
+            "Libraries that pass `-Xjvm-default=all` to the Kotlin compiler must " +
+                "use the @JvmDefaultWithCompatibility annotation on previously existing " +
+                "interfaces with stable methods with default implementations or default parameter" +
+                " values, and interfaces that extend other @JvmDefaultWithCompatibility " +
+                "interfaces. See go/androidx-api-guidelines#kotlin-jvm-default for more details.",
+            Category.CORRECTNESS, 5, Severity.ERROR,
+            Implementation(
+                MissingJvmDefaultWithCompatibilityDetector::class.java,
+                Scope.JAVA_FILE_SCOPE
+            )
+        )
+
+        const val JVM_DEFAULT_WITH_COMPATIBILITY = "kotlin.jvm.JvmDefaultWithCompatibility"
+    }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetectorTest.kt b/lint-checks/src/test/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetectorTest.kt
new file mode 100644
index 0000000..746a473
--- /dev/null
+++ b/lint-checks/src/test/java/androidx/build/lint/MissingJvmDefaultWithCompatibilityDetectorTest.kt
@@ -0,0 +1,261 @@
+/*
+ * 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.build.lint
+
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MissingJvmDefaultWithCompatibilityDetectorTest : AbstractLintDetectorTest(
+    useDetector = MissingJvmDefaultWithCompatibilityDetector(),
+    useIssues = listOf(MissingJvmDefaultWithCompatibilityDetector.ISSUE),
+
+) {
+    @Test
+    fun `Test lint for interface with stable default method`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithDefaultMethod {
+                    fun methodWithoutDefaultImplementation(foo: Int): String
+                    fun methodWithDefaultImplementation(): Int = 3
+                }
+            """)
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/java/androidx/InterfaceWithDefaultMethod.kt:4: Error: This interface must be annotated with @JvmDefaultWithCompatibility because it has a stable method with a default implementation [MissingJvmDefaultWithCompatibility]
+                interface InterfaceWithDefaultMethod {
+                ^
+1 errors, 0 warnings
+        """
+
+        val expectedFixDiffs = """
+Autofix for src/java/androidx/InterfaceWithDefaultMethod.kt line 4: Annotate with @JvmDefaultWithCompatibility:
+@@ -4 +4
++                 @JvmDefaultWithCompatibility
+        """
+        /* ktlint-enable max-line-length */
+
+        check(*input)
+            .expect(expected)
+            .expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test lint for interface with stable method with default parameter`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithMethodWithDefaultParameterValue {
+                    fun methodWithDefaultParameterValue(foo: Int = 3): Int
+                }
+            """)
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/java/androidx/InterfaceWithMethodWithDefaultParameterValue.kt:4: Error: This interface must be annotated with @JvmDefaultWithCompatibility because it has a stable method with a parameter with a default value [MissingJvmDefaultWithCompatibility]
+                interface InterfaceWithMethodWithDefaultParameterValue {
+                ^
+1 errors, 0 warnings
+        """
+
+        val expectedFixDiffs = """
+Autofix for src/java/androidx/InterfaceWithMethodWithDefaultParameterValue.kt line 4: Annotate with @JvmDefaultWithCompatibility:
+@@ -4 +4
++                 @JvmDefaultWithCompatibility
+        """
+        /* ktlint-enable max-line-length */
+
+        check(*input)
+            .expect(expected)
+            .expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test lint for interface implementing @JvmDefaultWithCompatibility interface`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                import kotlin.jvm.JvmDefaultWithCompatibility
+
+                @JvmDefaultWithCompatibility
+                interface InterfaceWithAnnotation {
+                    fun foo(bar: Int = 3): Int
+                }
+            """),
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithoutAnnotation: InterfaceWithAnnotation {
+                    fun baz(): Int
+                }
+            """),
+            Stubs.JvmDefaultWithCompatibility
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/java/androidx/InterfaceWithoutAnnotation.kt:4: Error: This interface must be annotated with @JvmDefaultWithCompatibility because it implements an interface which uses this annotation [MissingJvmDefaultWithCompatibility]
+                interface InterfaceWithoutAnnotation: InterfaceWithAnnotation {
+                ^
+1 errors, 0 warnings
+        """
+
+        val expectedFixDiffs = """
+Autofix for src/java/androidx/InterfaceWithoutAnnotation.kt line 4: Annotate with @JvmDefaultWithCompatibility:
+@@ -4 +4
++                 @JvmDefaultWithCompatibility
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input)
+            .expect(expected)
+            .expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test lint does not apply to interface implementing @JvmDefaultWithCompatibility`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                import kotlin.jvm.JvmDefaultWithCompatibility
+
+                @JvmDefaultWithCompatibility
+                interface InterfaceWithAnnotation {
+                    fun foo(bar: Int = 3): Int = 4
+                }
+            """),
+            Stubs.JvmDefaultWithCompatibility
+        )
+
+        check(*input)
+            .expectClean()
+    }
+
+    @Test
+    fun `Test lint does not apply to unstable interface`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                @RequiresOptIn
+                interface UnstableInterface {
+                    fun foo(bar: Int = 3): Int = 4
+                }
+            """),
+            Stubs.OptIn
+        )
+
+        check(*input)
+            .expectClean()
+    }
+
+    @Test
+    fun `Test lint does not apply to interface with no stable methods`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithoutStableMethods {
+                    @RequiresOptIn
+                    fun unstableMethod(foo: Int = 3): Int = 4
+                }
+            """),
+            Stubs.OptIn
+        )
+
+        check(*input)
+            .expectClean()
+    }
+
+    @Test
+    fun `Test lint does apply to interface with one unstable method and one stable method`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithStableAndUnstableMethods {
+                    @RequiresOptIn
+                    fun unstableMethod(foo: Int = 3): Int = 4
+                    fun stableMethod(foo: Int = 3): Int = 4
+                }
+            """),
+            Stubs.OptIn
+        )
+
+        /* ktlint-disable max-line-length */
+        val expected = """
+src/java/androidx/InterfaceWithStableAndUnstableMethods.kt:4: Error: This interface must be annotated with @JvmDefaultWithCompatibility because it has a stable method with a default implementation [MissingJvmDefaultWithCompatibility]
+                interface InterfaceWithStableAndUnstableMethods {
+                ^
+1 errors, 0 warnings
+        """
+
+        val expectedFixDiffs = """
+Autofix for src/java/androidx/InterfaceWithStableAndUnstableMethods.kt line 4: Annotate with @JvmDefaultWithCompatibility:
+@@ -4 +4
++                 @JvmDefaultWithCompatibility
+        """.trimIndent()
+        /* ktlint-enable max-line-length */
+
+        check(*input)
+            .expect(expected)
+            .expectFixDiffs(expectedFixDiffs)
+    }
+
+    @Test
+    fun `Test lint does not apply to interface with no default methods`() {
+        val input = arrayOf(
+            kotlin("""
+                package java.androidx
+
+                interface InterfaceWithoutDefaults {
+                    fun methodWithoutDefaults(foo: Int): Int
+                }
+            """)
+        )
+
+        check(*input)
+            .expectClean()
+    }
+
+    @Test
+    fun `Test lint does not apply to Java file`() {
+        val input = arrayOf(
+            java("""
+                package java.androidx;
+
+                interface JavaInterface {
+                    static int staticMethodInInterface() {
+                        return 3;
+                    }
+                }
+            """)
+        )
+
+        check(*input)
+            .expectClean()
+    }
+}
\ No newline at end of file
diff --git a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
index ecabcd7..685f57c 100644
--- a/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
+++ b/lint-checks/src/test/java/androidx/build/lint/Stubs.kt
@@ -299,6 +299,16 @@
 )
             """
         )
+
+        val JvmDefaultWithCompatibility = TestFiles.kotlin(
+            """
+package kotlin.jvm
+
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.CLASS)
+annotation class JvmDefaultWithCompatibility
+            """.trimIndent()
+        )
         /* ktlint-enable max-line-length */
     }
 }
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index 31e3381..6f87eac 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -47,6 +47,7 @@
     testImplementation(libs.kotlinStdlib)
     testImplementation(libs.kotlinCoroutinesTest)
 
+    androidTestImplementation(libs.kotlinTestJunit)
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
     androidTestImplementation(libs.testRunner)
diff --git a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
index b440cbf..a4da5ed 100644
--- a/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
+++ b/navigation/navigation-common/src/androidTest/java/androidx/navigation/NavDeepLinkTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import kotlin.test.assertFailsWith
 import org.junit.Test
 import java.io.UnsupportedEncodingException
 
@@ -1380,9 +1381,22 @@
             .contains("name2")
     }
 
-    @Test(expected = IllegalArgumentException::class)
+    @Test
     fun deepLinkNoRepeatedQueryParamsInPattern() {
         val deepLinkArgument = "$DEEP_LINK_EXACT_HTTPS/users?myarg={myarg}&myarg={myarg}"
-        NavDeepLink(deepLinkArgument)
+        val deepLink = NavDeepLink(deepLinkArgument)
+        val message = assertFailsWith<IllegalArgumentException> {
+            // query params are parsed lazily, need to run getMatchingArguments to resolve it
+            deepLink.getMatchingArguments(
+                Uri.parse(deepLinkArgument),
+                emptyMap()
+            )
+        }.message
+        assertThat(message).isEqualTo(
+            "Query parameter myarg must only be present once in $deepLinkArgument. " +
+                "To support repeated query parameters, use an array type for your " +
+                "argument and the pattern provided in your URI will be used to " +
+                "parse each query parameter instance."
+        )
     }
 }
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
index ca38df6..07fa702 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDeepLink.kt
@@ -47,30 +47,42 @@
      */
     public val mimeType: String?
 ) {
+    // path
     private val pathArgs = mutableListOf<String>()
-    private val paramArgMap = mutableMapOf<String, ParamQuery>()
-    private val fragArgs = mutableListOf<String>()
-
-    private var patternFinalRegex: String? = null
-    private val pattern by lazy {
-        patternFinalRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
+    private var pathRegex: String? = null
+    private val pathPattern by lazy {
+        pathRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
     }
-    private var isParameterizedQuery = false
+
+    // query
+    private val isParameterizedQuery by lazy {
+        uriPattern != null && Uri.parse(uriPattern).query != null
+    }
+    private val queryArgsMap by lazy(LazyThreadSafetyMode.NONE) { parseQuery() }
     private var isSingleQueryParamValueOnly = false
 
-    private var fragmentFinalRegex: String? = null
-    private val fragmentPattern by lazy {
-        fragmentFinalRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
+    // fragment
+    private val fragArgsAndRegex: Pair<MutableList<String>, String>? by
+        lazy(LazyThreadSafetyMode.NONE) { parseFragment() }
+    private val fragArgs by lazy(LazyThreadSafetyMode.NONE) {
+        fragArgsAndRegex?.first ?: mutableListOf()
+    }
+    private val fragRegex by lazy(LazyThreadSafetyMode.NONE) {
+        fragArgsAndRegex?.second
+    }
+    private val fragPattern by lazy {
+        fragRegex?.let { Pattern.compile(it, Pattern.CASE_INSENSITIVE) }
     }
 
-    private var mimeTypeFinalRegex: String? = null
+    // mime
+    private var mimeTypeRegex: String? = null
     private val mimeTypePattern by lazy {
-        mimeTypeFinalRegex?.let { Pattern.compile(it) }
+        mimeTypeRegex?.let { Pattern.compile(it) }
     }
 
     /** Arguments present in the deep link, including both path and query arguments. */
     internal val argumentsNames: List<String>
-        get() = pathArgs + paramArgMap.values.flatMap { it.arguments } + fragArgs
+        get() = pathArgs + queryArgsMap.values.flatMap { it.arguments } + fragArgs
 
     public var isExactDeepLink: Boolean = false
         /** @suppress */
@@ -120,9 +132,9 @@
 
     private fun matchUri(uri: Uri?): Boolean {
         // If the null status of both are not the same return false.
-        return if (uri == null == (pattern != null)) {
+        return if (uri == null == (pathPattern != null)) {
             false
-        } else uri == null || pattern!!.matcher(uri.toString()).matches()
+        } else uri == null || pathPattern!!.matcher(uri.toString()).matches()
         // If both are null return true, otherwise see if they match
     }
 
@@ -162,7 +174,7 @@
         arguments: Map<String, NavArgument?>
     ): Bundle? {
         // first check overall uri pattern for quick return if general pattern does not match
-        val matcher = pattern?.matcher(deepLink.toString()) ?: return null
+        val matcher = pathPattern?.matcher(deepLink.toString()) ?: return null
         if (!matcher.matches()) {
             return null
         }
@@ -193,7 +205,7 @@
         // Base condition of a matching fragment is a complete match on regex pattern. If a
         // required fragment arg is present while regex does not match, this will be caught later
         // on as a non-match when we check for presence of required args in the bundle.
-        val matcher = fragmentPattern?.matcher(fragment.toString()) ?: return
+        val matcher = fragPattern?.matcher(fragment.toString()) ?: return
         if (!matcher.matches()) return
 
         this.fragArgs.mapIndexed { index, argumentName ->
@@ -236,7 +248,7 @@
         bundle: Bundle,
         arguments: Map<String, NavArgument?>
     ): Boolean {
-        paramArgMap.forEach { entry ->
+        queryArgsMap.forEach { entry ->
             val paramName = entry.key
             val storedParam = entry.value
 
@@ -533,22 +545,21 @@
         }
         // we need to specifically escape any .* instances to ensure
         // they are still treated as wildcards in our final regex
-        patternFinalRegex = uriRegex.toString().replace(".*", "\\E.*\\Q")
+        pathRegex = uriRegex.toString().replace(".*", "\\E.*\\Q")
     }
 
-    private fun parseQuery() {
-        if (uriPattern == null || Uri.parse(uriPattern).query == null) return
+    private fun parseQuery(): MutableMap<String, ParamQuery> {
+        val paramArgMap = mutableMapOf<String, ParamQuery>()
+        if (!isParameterizedQuery) return paramArgMap
         val uri = Uri.parse(uriPattern)
 
-        isParameterizedQuery = true
-
         for (paramName in uri.queryParameterNames) {
             val argRegex = StringBuilder()
             val queryParams = uri.getQueryParameters(paramName)
             require(queryParams.size <= 1) {
-                "Query parameter $paramName must only be present once in $uriPattern." +
-                    "To support repeated query parameters, use an array type for your" +
-                    "argument and the pattern provided in your URI will be used to" +
+                "Query parameter $paramName must only be present once in $uriPattern. " +
+                    "To support repeated query parameters, use an array type for your " +
+                    "argument and the pattern provided in your URI will be used to " +
                     "parse each query parameter instance."
             }
             val queryParam = queryParams.firstOrNull()
@@ -580,15 +591,17 @@
             param.paramRegex = argRegex.toString().replace(".*", "\\E.*\\Q")
             paramArgMap[paramName] = param
         }
+        return paramArgMap
     }
 
-    private fun parseFragment() {
-        if (uriPattern == null || Uri.parse(uriPattern).fragment == null) return
+    private fun parseFragment(): Pair<MutableList<String>, String>? {
+        if (uriPattern == null || Uri.parse(uriPattern).fragment == null) return null
 
+        val fragArgs = mutableListOf<String>()
         val fragment = Uri.parse(uriPattern).fragment
         val fragRegex = StringBuilder()
         buildRegex(fragment!!, fragArgs, fragRegex)
-        fragmentFinalRegex = fragRegex.toString()
+        return fragArgs to fragRegex.toString()
     }
 
     private fun parseMime() {
@@ -606,17 +619,14 @@
         )
 
         // the matching pattern can have the exact name or it can be wildcard literal (*)
-        val mimeTypeRegex = "^(${splitMimeType.type}|[*]+)/(${splitMimeType.subType}|[*]+)$"
-        println("cfok inside mimeTypeRegex $mimeTypeRegex")
+        val regex = "^(${splitMimeType.type}|[*]+)/(${splitMimeType.subType}|[*]+)$"
 
         // if the deep link type or subtype is wildcard, allow anything
-        mimeTypeFinalRegex = mimeTypeRegex.replace("*|[*]", "[\\s\\S]")
+        mimeTypeRegex = regex.replace("*|[*]", "[\\s\\S]")
     }
 
     init {
         parsePath()
-        parseQuery()
-        parseFragment()
         parseMime()
     }
 }
diff --git a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
index 32a49e7..c550abe 100644
--- a/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
+++ b/navigation/navigation-dynamic-features-fragment/src/main/java/androidx/navigation/dynamicfeatures/fragment/ui/AbstractProgressFragment.kt
@@ -24,8 +24,8 @@
 import androidx.activity.result.IntentSenderRequest
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.fragment.app.Fragment
-import androidx.fragment.app.viewModels
 import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
 import androidx.navigation.dynamicfeatures.Constants
 import androidx.navigation.dynamicfeatures.DynamicExtras
 import androidx.navigation.dynamicfeatures.DynamicInstallMonitor
@@ -51,8 +51,12 @@
         private const val TAG = "AbstractProgress"
     }
 
-    private val installViewModel: InstallViewModel by viewModels {
-        InstallViewModel.FACTORY
+    private val installViewModel: InstallViewModel by lazy {
+        ViewModelProvider(
+            viewModelStore,
+            InstallViewModel.FACTORY,
+            defaultViewModelCreationExtras
+        )[InstallViewModel::class.java]
     }
     private val destinationId by lazy {
         requireArguments().getInt(Constants.DESTINATION_ID)
diff --git a/navigation/navigation-safe-args-gradle-plugin/build.gradle b/navigation/navigation-safe-args-gradle-plugin/build.gradle
index a66edde..f44ca8d 100644
--- a/navigation/navigation-safe-args-gradle-plugin/build.gradle
+++ b/navigation/navigation-safe-args-gradle-plugin/build.gradle
@@ -24,7 +24,7 @@
 }
 
 dependencies {
-    implementation("com.android.tools.build:gradle:7.0.4")
+    implementation("com.android.tools.build:gradle:7.3.0")
     implementation(libs.kotlinGradlePluginz)
     api(project(":navigation:navigation-safe-args-generator"))
     api(gradleApi())
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
index 9a5a17e..ae44366 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/NavigationUI.kt
@@ -16,6 +16,7 @@
 package androidx.navigation.ui
 
 import android.os.Bundle
+import android.util.Log
 import android.view.Menu
 import android.view.MenuItem
 import android.view.View
@@ -46,6 +47,8 @@
  * navigation patterns like a navigation drawer or bottom nav bar with your [NavController].
  */
 public object NavigationUI {
+    private const val TAG = "NavigationUI"
+
     /**
      * Attempt to navigate to the [NavDestination] associated with the given MenuItem. This
      * MenuItem should have been added via one of the helper methods in this class.
@@ -94,6 +97,13 @@
             // Return true only if the destination we've navigated to matches the MenuItem
             navController.currentDestination?.matchDestination(item.itemId) == true
         } catch (e: IllegalArgumentException) {
+            val name = NavDestination.getDisplayName(navController.context, item.itemId)
+            Log.i(
+                TAG,
+                "Ignoring onNavDestinationSelected for MenuItem $name as it cannot be found " +
+                    "from the current destination ${navController.currentDestination}",
+                e
+            )
             false
         }
     }
@@ -158,6 +168,13 @@
             // Return true only if the destination we've navigated to matches the MenuItem
             navController.currentDestination?.matchDestination(item.itemId) == true
         } catch (e: IllegalArgumentException) {
+            val name = NavDestination.getDisplayName(navController.context, item.itemId)
+            Log.i(
+                TAG,
+                "Ignoring onNavDestinationSelected for MenuItem $name as it cannot be found " +
+                    "from the current destination ${navController.currentDestination}",
+                e
+            )
             false
         }
     }
diff --git a/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt b/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
index bb67b7a..47fe02e4 100644
--- a/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
+++ b/paging/paging-common/src/test/kotlin/androidx/paging/PageEventTest.kt
@@ -30,6 +30,7 @@
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
 import kotlin.test.assertSame
+import org.junit.Before
 
 internal fun <T : Any> adjacentInsertEvent(
     isPrepend: Boolean,
@@ -278,26 +279,36 @@
 
     @RunWith(Parameterized::class)
     class StaticPagingData(
-        private val original: PagingData<String>
+        private val data: List<String>
     ) {
         companion object {
             @JvmStatic
-            @Parameterized.Parameters(name = "original = {0}")
+            @Parameterized.Parameters(name = "data = {0}")
             fun initParameters() = listOf(
-                PagingData.from(listOf("a", "b", "c")),
-                PagingData.empty(),
+                listOf("a", "b", "c"),
+                emptyList(),
             )
         }
 
         private val differ = TestPagingDataDiffer<String>(DirectDispatcher)
+        private lateinit var pagingData: PagingData<String>
+
+        @Before
+        fun init() {
+            pagingData = if (data.isNotEmpty()) {
+                PagingData.from(data)
+            } else {
+                PagingData.empty()
+            }
+        }
 
         @Test
         fun map() = runTest(UnconfinedTestDispatcher()) {
             val transform = { it: String -> it + it }
-            differ.collectFrom(original)
+            differ.collectFrom(pagingData)
             val originalItems = differ.snapshot().items
             val expectedItems = originalItems.map(transform)
-            val transformedPagingData = original.map { transform(it) }
+            val transformedPagingData = pagingData.map { transform(it) }
             differ.collectFrom(transformedPagingData)
             assertEquals(expectedItems, differ.snapshot().items)
         }
@@ -305,10 +316,10 @@
         @Test
         fun flatMap() = runTest(UnconfinedTestDispatcher()) {
             val transform = { it: String -> listOf(it, it) }
-            differ.collectFrom(original)
+            differ.collectFrom(pagingData)
             val originalItems = differ.snapshot().items
             val expectedItems = originalItems.flatMap(transform)
-            val transformedPagingData = original.flatMap { transform(it) }
+            val transformedPagingData = pagingData.flatMap { transform(it) }
             differ.collectFrom(transformedPagingData)
             assertEquals(expectedItems, differ.snapshot().items)
         }
@@ -316,10 +327,10 @@
         @Test
         fun filter() = runTest(UnconfinedTestDispatcher()) {
             val predicate = { it: String -> it != "b" }
-            differ.collectFrom(original)
+            differ.collectFrom(pagingData)
             val originalItems = differ.snapshot().items
             val expectedItems = originalItems.filter(predicate)
-            val transformedPagingData = original.filter { predicate(it) }
+            val transformedPagingData = pagingData.filter { predicate(it) }
             differ.collectFrom(transformedPagingData)
             assertEquals(expectedItems, differ.snapshot().items)
         }
@@ -329,7 +340,7 @@
             val transform = { left: String?, right: String? ->
                 if (left == null || right == null) null else "|"
             }
-            differ.collectFrom(original)
+            differ.collectFrom(pagingData)
             val originalItems = differ.snapshot().items
             val expectedItems = originalItems.flatMapIndexed { index, s ->
                 val result = mutableListOf<String>()
@@ -343,7 +354,7 @@
                 }
                 result
             }
-            val transformedPagingData = original.insertSeparators { left, right ->
+            val transformedPagingData = pagingData.insertSeparators { left, right ->
                 transform(left, right)
             }
             differ.collectFrom(transformedPagingData)
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 36dbbfb..2aaaadc 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -37,45 +37,37 @@
     fun compileServiceInterface_ok() {
         val inputTestDataDir = File("src/test/test-data/testinterface/input")
         val outputTestDataDir = File("src/test/test-data/testinterface/output")
-        val sources = loadSourcesFromDirectory(inputTestDataDir)
-        val expectedOutput = loadSourcesFromDirectory(outputTestDataDir)
+        val inputSources = loadSourcesFromDirectory(inputTestDataDir)
+        val expectedKotlinSources = loadSourcesFromDirectory(outputTestDataDir)
         val provider = PrivacySandboxKspCompiler.Provider()
-        // Check that compilation is successful
-        assertThat(
-            compileAll(
-                sources,
-                symbolProcessorProviders = listOf(provider),
-                processorOptions = getProcessorOptions(),
-            )
-        ).also {
-            it.generatesExactlySources(
-                "com/mysdk/IMyInterface.java",
-                "com/mysdk/IMySecondInterface.java",
-                "com/mysdk/IMySdk.java",
-                "com/mysdk/ICancellationSignal.java",
-                "com/mysdk/IMyInterfaceTransactionCallback.java",
-                "com/mysdk/IMySecondInterfaceTransactionCallback.java",
-                "com/mysdk/IStringTransactionCallback.java",
-                "com/mysdk/IUnitTransactionCallback.java",
-                "com/mysdk/AbstractSandboxedSdkProvider.kt",
-                "com/mysdk/MyInterfaceStubDelegate.kt",
-                "com/mysdk/MySecondInterfaceStubDelegate.kt",
-                "com/mysdk/MySdkStubDelegate.kt",
-                "com/mysdk/TransportCancellationCallback.kt",
-                "com/mysdk/ResponseConverter.kt",
-                "com/mysdk/RequestConverter.kt",
-                "com/mysdk/ParcelableRequest.java",
-                "com/mysdk/ParcelableResponse.java",
-                "com/mysdk/IResponseTransactionCallback.java",
-                "com/mysdk/MyCallbackClientProxy.kt",
-                "com/mysdk/IMyCallback.java",
-                "com/mysdk/PrivacySandboxThrowableParcelConverter.kt",
-                "com/mysdk/ParcelableStackFrame.java",
-                "com/mysdk/PrivacySandboxThrowableParcel.java",
-            )
-        }.also {
-            it.generatesSourcesWithContents(expectedOutput)
-        }
+
+        val result = compileAll(
+            inputSources,
+            symbolProcessorProviders = listOf(provider),
+            processorOptions = getProcessorOptions(),
+        )
+        assertThat(result).succeeds()
+
+        val expectedAidlFilepath = listOf(
+            "com/mysdk/ICancellationSignal.java",
+            "com/mysdk/IMyCallback.java",
+            "com/mysdk/IMyInterface.java",
+            "com/mysdk/IMyInterfaceTransactionCallback.java",
+            "com/mysdk/IMySdk.java",
+            "com/mysdk/IMySecondInterface.java",
+            "com/mysdk/IMySecondInterfaceTransactionCallback.java",
+            "com/mysdk/IResponseTransactionCallback.java",
+            "com/mysdk/IStringTransactionCallback.java",
+            "com/mysdk/IUnitTransactionCallback.java",
+            "com/mysdk/ParcelableRequest.java",
+            "com/mysdk/ParcelableResponse.java",
+            "com/mysdk/ParcelableStackFrame.java",
+            "com/mysdk/PrivacySandboxThrowableParcel.java",
+        )
+        assertThat(result).hasAllExpectedGeneratedSourceFilesAndContent(
+            expectedKotlinSources,
+            expectedAidlFilepath
+        )
     }
 
     @Test
@@ -91,7 +83,7 @@
                     processorOptions = getProcessorOptions(),
                 )
             )
-        ).generatesExactlySources()
+        ).hasNoGeneratedSourceFiles()
     }
 
     @Test
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
index a647718..860b155 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/BaseApiGeneratorTest.kt
@@ -18,10 +18,9 @@
 
 import androidx.privacysandbox.tools.core.Metadata
 import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
+import androidx.privacysandbox.tools.testing.hasAllExpectedGeneratedSourceFilesAndContent
 import androidx.privacysandbox.tools.testing.loadSourcesFromDirectory
 import androidx.room.compiler.processing.util.Source
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
 import java.io.File
 import java.nio.file.Files
 import kotlin.io.path.Path
@@ -55,19 +54,11 @@
 
     @Test
     fun generatedApi_hasExpectedContents() {
-        val expectedSources = loadSourcesFromDirectory(outputDirectory)
-        assertThat(generatedSources.map(Source::relativePath))
-            .containsExactlyElementsIn(
-                expectedSources.map(Source::relativePath) + relativePathsToExpectedAidlClasses
-            )
-
-        val outputSourceMap = generatedSources.associateBy(Source::relativePath)
-        for (expected in expectedSources) {
-            assertWithMessage(
-                "Contents of generated file %s don't match goldens.",
-                expected.relativePath
-            ).that(outputSourceMap[expected.relativePath]?.contents)
-                .isEqualTo(expected.contents)
-        }
+        val expectedKotlinSources = loadSourcesFromDirectory(outputDirectory)
+        hasAllExpectedGeneratedSourceFilesAndContent(
+            generatedSources,
+            expectedKotlinSources,
+            relativePathsToExpectedAidlClasses
+        )
     }
 }
\ No newline at end of file
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index dec603f..f2a5114 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -64,6 +64,26 @@
 val TestCompilationResult.resourceOutputDir: File
     get() = outputClasspath.first().parentFile.resolve("ksp-compiler/resourceOutputDir")
 
+fun hasAllExpectedGeneratedSourceFilesAndContent(
+    actualKotlinSources: List<Source>,
+    expectedKotlinSources: List<Source>,
+    expectedAidlFilepath: List<String>
+) {
+    val expectedRelativePaths =
+        expectedKotlinSources.map(Source::relativePath) + expectedAidlFilepath
+    assertThat(actualKotlinSources.map(Source::relativePath))
+        .containsExactlyElementsIn(expectedRelativePaths)
+
+    val actualRelativePathMap = actualKotlinSources.associateBy(Source::relativePath)
+    for (expectedKotlinSource in expectedKotlinSources) {
+        assertWithMessage(
+            "Contents of generated file ${expectedKotlinSource.relativePath} don't " +
+                "match golden."
+        ).that(actualRelativePathMap[expectedKotlinSource.relativePath]?.contents)
+            .isEqualTo(expectedKotlinSource.contents)
+    }
+}
+
 class CompilationResultSubject(private val result: TestCompilationResult) {
     fun succeeds() {
         assertWithMessage(
@@ -73,21 +93,19 @@
         ).isTrue()
     }
 
-    fun generatesExactlySources(vararg sourcePaths: String) {
-        succeeds()
-        assertThat(result.generatedSources.map(Source::relativePath))
-            .containsExactlyElementsIn(sourcePaths)
+    fun hasAllExpectedGeneratedSourceFilesAndContent(
+        expectedKotlinSources: List<Source>,
+        expectedAidlFilepath: List<String>
+    ) {
+        hasAllExpectedGeneratedSourceFilesAndContent(
+            result.generatedSources,
+            expectedKotlinSources,
+            expectedAidlFilepath
+        )
     }
 
-    fun generatesSourcesWithContents(sources: List<Source>) {
-        succeeds()
-        val contentsByFile = result.generatedSources.associate { it.relativePath to it.contents }
-        for (source in sources) {
-            assertWithMessage("File ${source.relativePath} was not generated")
-                .that(contentsByFile).containsKey(source.relativePath)
-            assertWithMessage("Contents of file ${source.relativePath} don't match.")
-                .that(contentsByFile[source.relativePath]).isEqualTo(source.contents)
-        }
+    fun hasNoGeneratedSourceFiles() {
+        assertThat(result.generatedSources).isEmpty()
     }
 
     fun fails() {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
index 6f3ad36..222db27 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/XTypeName.kt
@@ -25,6 +25,12 @@
 import com.squareup.kotlinpoet.FLOAT_ARRAY
 import com.squareup.kotlinpoet.INT_ARRAY
 import com.squareup.kotlinpoet.LONG_ARRAY
+import com.squareup.kotlinpoet.MUTABLE_COLLECTION
+import com.squareup.kotlinpoet.MUTABLE_ITERABLE
+import com.squareup.kotlinpoet.MUTABLE_LIST
+import com.squareup.kotlinpoet.MUTABLE_MAP
+import com.squareup.kotlinpoet.MUTABLE_MAP_ENTRY
+import com.squareup.kotlinpoet.MUTABLE_SET
 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
 import com.squareup.kotlinpoet.SHORT_ARRAY
 import com.squareup.kotlinpoet.asClassName
@@ -257,6 +263,8 @@
  * [kotlin.collections.MutableList] then the non-mutable [XClassName] is returned due to the
  * mutable interfaces only existing at compile-time, see:
  * https://youtrack.jetbrains.com/issue/KT-11754.
+ *
+ * If the mutable [XClassName] is needed, use [asMutableClassName].
  */
 fun KClass<*>.asClassName(): XClassName {
     val jClassName = if (this.java.isPrimitive) {
@@ -272,6 +280,38 @@
     )
 }
 
+/**
+ * Creates a mutable [XClassName] from the receiver [KClass]
+ *
+ * This is a workaround for:
+ * https://github.com/square/kotlinpoet/issues/279
+ * https://youtrack.jetbrains.com/issue/KT-11754
+ *
+ * When the receiver [KClass] is a Kotlin interop collection, such as [kotlin.collections.List]
+ * then the returned [XClassName] contains the corresponding JavaPoet class name. See:
+ * https://kotlinlang.org/docs/reference/java-interop.html#mapped-types.
+ *
+ * When the receiver [KClass] is a Kotlin mutable collection, such as
+ * [kotlin.collections.MutableList] then the returned [XClassName] contains the corresponding
+ * KotlinPoet class name.
+ *
+ * If an equivalent interop [XClassName] mapping for a Kotlin mutable Kotlin collection receiver
+ * [KClass] is not found, the method will error out.
+ */
+fun KClass<*>.asMutableClassName(): XClassName {
+    val java = JClassName.get(this.java)
+    val kotlin = when (this) {
+        Iterator::class -> MUTABLE_ITERABLE
+        Collection::class -> MUTABLE_COLLECTION
+        List::class -> MUTABLE_LIST
+        Set::class -> MUTABLE_SET
+        Map::class -> MUTABLE_MAP
+        Map.Entry::class -> MUTABLE_MAP_ENTRY
+        else -> error("No equivalent mutable Kotlin interop found for `$this`.")
+    }
+    return XClassName(java, kotlin, XNullability.NONNULL)
+}
+
 private fun getBoxedJClassName(klass: Class<*>): JClassName = when (klass) {
     java.lang.Void.TYPE -> JTypeName.VOID.box()
     java.lang.Boolean.TYPE -> JTypeName.BOOLEAN.box()
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt b/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
similarity index 98%
rename from room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
rename to room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
index bcdf483..9ef5f62 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/ext/javapoet_ext.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/ext/codegenpoet_ext.kt
@@ -27,6 +27,7 @@
 import androidx.room.compiler.codegen.XTypeName
 import androidx.room.compiler.codegen.XTypeSpec
 import androidx.room.compiler.codegen.asClassName
+import androidx.room.compiler.codegen.asMutableClassName
 import com.squareup.javapoet.ArrayTypeName
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.CodeBlock
@@ -153,9 +154,14 @@
     val INT_SPARSE_ARRAY: ClassName = ClassName.get(COLLECTION_PACKAGE, "SparseArrayCompat")
 }
 
+object KotlinCollectionTypeNames {
+    val MUTABLE_LIST = List::class.asMutableClassName()
+}
+
 object CommonTypeNames {
     val ARRAYS = ClassName.get("java.util", "Arrays")
     val LIST = ClassName.get("java.util", "List")
+    val ARRAY_LIST = XClassName.get("java.util", "ArrayList")
     val MAP = ClassName.get("java.util", "Map")
     val SET = ClassName.get("java.util", "Set")
     val STRING = ClassName.get("java.lang", "String")
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 56f97ad..2ef7f52 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
@@ -16,34 +16,38 @@
 
 package androidx.room.solver.query.result
 
+import androidx.room.compiler.codegen.XCodeBlock
 import androidx.room.compiler.processing.XType
-import androidx.room.ext.L
-import androidx.room.ext.T
+import androidx.room.ext.CommonTypeNames.ARRAY_LIST
+import androidx.room.ext.KotlinCollectionTypeNames.MUTABLE_LIST
 import androidx.room.solver.CodeGenScope
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import java.util.ArrayList
 
 class ListQueryResultAdapter(
     private val typeArg: XType,
     private val rowAdapter: RowAdapter
 ) : QueryResultAdapter(listOf(rowAdapter)) {
     override fun convert(outVarName: String, cursorVarName: String, scope: CodeGenScope) {
-        scope.builder().apply {
+        scope.builder.apply {
             rowAdapter.onCursorReady(cursorVarName = cursorVarName, scope = scope)
-            val collectionType = ParameterizedTypeName
-                .get(ClassName.get(List::class.java), typeArg.typeName)
-            val arrayListType = ParameterizedTypeName
-                .get(ClassName.get(ArrayList::class.java), typeArg.typeName)
-            addStatement(
-                "final $T $L = new $T($L.getCount())",
-                collectionType, outVarName, arrayListType, cursorVarName
+            val listTypeName = MUTABLE_LIST.parametrizedBy(typeArg.asTypeName())
+            addLocalVariable(
+                name = outVarName,
+                typeName = listTypeName,
+                assignExpr = XCodeBlock.ofNewInstance(
+                    language,
+                    ARRAY_LIST.parametrizedBy(typeArg.asTypeName()),
+                    "%L.getCount()",
+                    cursorVarName
+                )
             )
             val tmpVarName = scope.getTmpVar("_item")
-            beginControlFlow("while($L.moveToNext())", cursorVarName).apply {
-                addStatement("final $T $L", typeArg.typeName, tmpVarName)
+            beginControlFlow("while (%L.moveToNext())", cursorVarName).apply {
+                addLocalVariable(
+                    name = tmpVarName,
+                    typeName = typeArg.asTypeName()
+                )
                 rowAdapter.convert(tmpVarName, cursorVarName, scope)
-                addStatement("$L.add($L)", outVarName, tmpVarName)
+                addStatement("%L.add(%L)", outVarName, tmpVarName)
             }
             endControlFlow()
         }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
index 49fa7f1..c58df04 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/writer/KotlinCodeGenTest.kt
@@ -954,6 +954,61 @@
         )
     }
 
+    @Test
+    fun queryResultAdapter_list() {
+        val testName = object {}.javaClass.enclosingMethod!!.name
+        val dbSource = Source.kotlin(
+            "MyDatabase.kt",
+            """
+            import androidx.room.*
+
+            @Database(entities = [MyEntity::class, MyNullableEntity::class], version = 1, exportSchema = false)
+            abstract class MyDatabase : RoomDatabase() {
+                abstract fun getDao(): MyDao
+            }
+            """.trimIndent()
+        )
+
+        val src = Source.kotlin(
+            "MyDao.kt",
+            """
+            import androidx.room.*
+
+            @Dao
+            interface MyDao {
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfList(): List<MyEntity>
+
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfNullableList(): List<MyEntity>?
+
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfNullableEntityList(): List<MyNullableEntity?>
+
+              @Query("SELECT * FROM MyEntity")
+              fun queryOfNullableListWithNullableEntity(): List<MyNullableEntity>?
+            }
+
+            @Entity
+            data class MyEntity(
+                @PrimaryKey
+                val pk: Int,
+                val other: String
+            )
+            @Entity
+            data class MyNullableEntity(
+                @PrimaryKey
+                val pk: Int?,
+                val other: String?
+            )
+            """.trimIndent()
+        )
+        runTest(
+            sources = listOf(src, dbSource),
+            expectedFilePath = getTestGoldenPath(testName)
+        )
+    }
+
     private fun getTestGoldenPath(testName: String): String {
         return "kotlinCodeGen/$testName.kt"
     }
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
index fe908dc..a8b3ea9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/ComplexDao.java
@@ -60,7 +60,7 @@
             final int _cursorIndexOfFullName = 0;
             final int _cursorIndexOfId = 1;
             final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final ComplexDao.FullName _item;
                 _item = new ComplexDao.FullName();
                 if (_cursor.isNull(_cursorIndexOfFullName)) {
@@ -195,7 +195,7 @@
             final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
             final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
             final List<User> _result = new ArrayList<User>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final User _item_1;
                 _item_1 = new User();
                 _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
@@ -307,7 +307,7 @@
         final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
         try {
             final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Integer _item_1;
                 if (_cursor.isNull(0)) {
                     _item_1 = null;
@@ -375,7 +375,7 @@
         final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
         try {
             final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Integer _item_3;
                 if (_cursor.isNull(0)) {
                     _item_3 = null;
@@ -468,7 +468,7 @@
                     final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
                     final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
                     final List<User> _result = new ArrayList<User>(_cursor.getCount());
-                    while(_cursor.moveToNext()) {
+                    while (_cursor.moveToNext()) {
                         final User _item_1;
                         _item_1 = new User();
                         _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
@@ -512,7 +512,7 @@
             final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
             final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
             final List<Child1> _result = new ArrayList<Child1>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Child1 _item;
                 final int _tmpId;
                 _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -556,7 +556,7 @@
             final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
             final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
             final List<Child2> _result = new ArrayList<Child2>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Child2 _item;
                 final int _tmpId;
                 _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -603,7 +603,7 @@
                     final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
                     final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
                     final List<Child1> _result = new ArrayList<Child1>(_cursor.getCount());
-                    while(_cursor.moveToNext()) {
+                    while (_cursor.moveToNext()) {
                         final Child1 _item;
                         final int _tmpId;
                         _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -646,7 +646,7 @@
             final int _cursorIndexOfUid = 0;
             final int _cursorIndexOfName = 1;
             final List<UserSummary> _result = new ArrayList<UserSummary>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final UserSummary _item;
                 _item = new UserSummary();
                 _item.uid = _cursor.getInt(_cursorIndexOfUid);
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 875dfea..c2c1f0d 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -60,7 +60,7 @@
             final int _cursorIndexOfFullName = 0;
             final int _cursorIndexOfId = 1;
             final List<ComplexDao.FullName> _result = new ArrayList<ComplexDao.FullName>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final ComplexDao.FullName _item;
                 _item = new ComplexDao.FullName();
                 _item.fullName = _cursor.getString(_cursorIndexOfFullName);
@@ -167,7 +167,7 @@
             final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
             final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
             final List<User> _result = new ArrayList<User>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final User _item_1;
                 _item_1 = new User();
                 _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
@@ -271,7 +271,7 @@
         final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
         try {
             final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Integer _item_1;
                 if (_cursor.isNull(0)) {
                     _item_1 = null;
@@ -339,7 +339,7 @@
         final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
         try {
             final List<Integer> _result = new ArrayList<Integer>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Integer _item_3;
                 if (_cursor.isNull(0)) {
                     _item_3 = null;
@@ -424,7 +424,7 @@
                     final int _cursorIndexOfLastName = CursorUtil.getColumnIndexOrThrow(_cursor, "lastName");
                     final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "ageColumn");
                     final List<User> _result = new ArrayList<User>(_cursor.getCount());
-                    while(_cursor.moveToNext()) {
+                    while (_cursor.moveToNext()) {
                         final User _item_1;
                         _item_1 = new User();
                         _item_1.uid = _cursor.getInt(_cursorIndexOfUid);
@@ -460,7 +460,7 @@
             final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
             final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
             final List<Child1> _result = new ArrayList<Child1>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Child1 _item;
                 final int _tmpId;
                 _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -496,7 +496,7 @@
             final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
             final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
             final List<Child2> _result = new ArrayList<Child2>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final Child2 _item;
                 final int _tmpId;
                 _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -535,7 +535,7 @@
                     final int _cursorIndexOfSerial = CursorUtil.getColumnIndexOrThrow(_cursor, "serial");
                     final int _cursorIndexOfCode = CursorUtil.getColumnIndexOrThrow(_cursor, "code");
                     final List<Child1> _result = new ArrayList<Child1>(_cursor.getCount());
-                    while(_cursor.moveToNext()) {
+                    while (_cursor.moveToNext()) {
                         final Child1 _item;
                         final int _tmpId;
                         _tmpId = _cursor.getInt(_cursorIndexOfId);
@@ -570,7 +570,7 @@
             final int _cursorIndexOfUid = 0;
             final int _cursorIndexOfName = 1;
             final List<UserSummary> _result = new ArrayList<UserSummary>(_cursor.getCount());
-            while(_cursor.moveToNext()) {
+            while (_cursor.moveToNext()) {
                 final UserSummary _item;
                 _item = new UserSummary();
                 _item.uid = _cursor.getInt(_cursorIndexOfUid);
diff --git a/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
new file mode 100644
index 0000000..98c21c1
--- /dev/null
+++ b/room/room-compiler/src/test/test-data/kotlinCodeGen/queryResultAdapter_list.kt
@@ -0,0 +1,146 @@
+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 javax.`annotation`.processing.Generated
+import kotlin.Int
+import kotlin.String
+import kotlin.Suppress
+import kotlin.collections.List
+import kotlin.collections.MutableList
+import kotlin.jvm.JvmStatic
+
+@Generated(value = ["androidx.room.RoomProcessor"])
+@Suppress(names = ["unchecked", "deprecation"])
+public class MyDao_Impl : MyDao {
+    private val __db: RoomDatabase
+
+    public constructor(__db: RoomDatabase) {
+        this.__db = __db
+    }
+
+    public override fun queryOfList(): List<MyEntity> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _result: MutableList<MyEntity> = ArrayList<MyEntity>(_cursor.getCount())
+            while (_cursor.moveToNext()) {
+                val _item: MyEntity
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _item = MyEntity(_tmpPk,_tmpOther)
+                _result.add(_item)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun queryOfNullableList(): List<MyEntity>? {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _result: MutableList<MyEntity> = ArrayList<MyEntity>(_cursor.getCount())
+            while (_cursor.moveToNext()) {
+                val _item: MyEntity
+                val _tmpPk: Int
+                _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                val _tmpOther: String
+                _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                _item = MyEntity(_tmpPk,_tmpOther)
+                _result.add(_item)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun queryOfNullableEntityList(): List<MyNullableEntity?> {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _result: MutableList<MyNullableEntity?> = ArrayList<MyNullableEntity?>(_cursor.getCount())
+            while (_cursor.moveToNext()) {
+                val _item: MyNullableEntity?
+                val _tmpPk: Int?
+                if (_cursor.isNull(_cursorIndexOfPk)) {
+                    _tmpPk = null
+                } else {
+                    _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                }
+                val _tmpOther: String?
+                if (_cursor.isNull(_cursorIndexOfOther)) {
+                    _tmpOther = null
+                } else {
+                    _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                }
+                _item = MyNullableEntity(_tmpPk,_tmpOther)
+                _result.add(_item)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public override fun queryOfNullableListWithNullableEntity(): List<MyNullableEntity>? {
+        val _sql: String = "SELECT * FROM MyEntity"
+        val _statement: RoomSQLiteQuery = acquire(_sql, 0)
+        __db.assertNotSuspendingTransaction()
+        val _cursor: Cursor = query(__db, _statement, false, null)
+        try {
+            val _cursorIndexOfPk: Int = getColumnIndexOrThrow(_cursor, "pk")
+            val _cursorIndexOfOther: Int = getColumnIndexOrThrow(_cursor, "other")
+            val _result: MutableList<MyNullableEntity> = ArrayList<MyNullableEntity>(_cursor.getCount())
+            while (_cursor.moveToNext()) {
+                val _item: MyNullableEntity
+                val _tmpPk: Int?
+                if (_cursor.isNull(_cursorIndexOfPk)) {
+                    _tmpPk = null
+                } else {
+                    _tmpPk = _cursor.getInt(_cursorIndexOfPk)
+                }
+                val _tmpOther: String?
+                if (_cursor.isNull(_cursorIndexOfOther)) {
+                    _tmpOther = null
+                } else {
+                    _tmpOther = _cursor.getString(_cursorIndexOfOther)
+                }
+                _item = MyNullableEntity(_tmpPk,_tmpOther)
+                _result.add(_item)
+            }
+            return _result
+        } finally {
+            _cursor.close()
+            _statement.release()
+        }
+    }
+
+    public companion object {
+        @JvmStatic
+        public fun getRequiredConverters(): List<Class<*>> = emptyList()
+    }
+}
\ No newline at end of file
diff --git a/security/security-crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java b/security/security-crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java
index 0dfdf03..2d1beec 100644
--- a/security/security-crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java
+++ b/security/security-crypto/src/androidTest/java/androidx/security/crypto/EncryptedFileTest.java
@@ -50,7 +50,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.security.InvalidKeyException;
 import java.security.KeyStore;
 import java.util.ArrayList;
 
@@ -124,11 +123,6 @@
         mMasterKey = new MasterKey.Builder(mContext)
                 .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
                 .build();
-
-        keyStore.deleteEntry(EncryptedFileTest.SECOND_MASTER_KEY_ALIAS);
-        mSecondMasterKey = new MasterKey.Builder(mContext, SECOND_MASTER_KEY_ALIAS)
-                .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
-                .build();
     }
 
     @Test
@@ -355,44 +349,6 @@
         assertTrue("Keyset should have existed.", containsKeyset);
     }
 
-    @Test(expected = InvalidKeyException.class)
-    public void testTwoMasterKeys() throws Exception {
-        new EncryptedFile.Builder(
-                mContext,
-                new File(mContext.getFilesDir(), TestFileName.ENCRYPTED_FILE_1.toString()),
-                mMasterKey,
-                EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
-                .build();
-
-        // This will fail because implicitly we are decrypting the keyset created for mMasterKey
-        // with this new mSecondMasterKey
-        new EncryptedFile.Builder(
-                mContext,
-                new File(mContext.getFilesDir(), TestFileName.ENCRYPTED_FILE_2.toString()),
-                mSecondMasterKey,
-                EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
-                .build();
-    }
-
-    @Test
-    public void testTwoMasterKeysAndTwoKeysets() throws Exception {
-        new EncryptedFile.Builder(
-                mContext,
-                new File(mContext.getFilesDir(), TestFileName.ENCRYPTED_FILE_1.toString()),
-                mMasterKey,
-                EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
-                .build();
-
-        // This should succeed because mSecondMasterKey gets its own keyset
-        new EncryptedFile.Builder(
-                mContext,
-                new File(mContext.getFilesDir(), TestFileName.ENCRYPTED_FILE_2.toString()),
-                mSecondMasterKey,
-                EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB)
-                .setKeysetAlias("second_keyset")
-                .build();
-    }
-
     @SuppressWarnings("deprecation")
     @Test
     public void tinkTest() throws Exception {
diff --git a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
index 9027376..9ecf09b 100644
--- a/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
+++ b/security/security-crypto/src/main/java/androidx/security/crypto/EncryptedFile.java
@@ -30,7 +30,6 @@
 import com.google.crypto.tink.KeysetHandle;
 import com.google.crypto.tink.StreamingAead;
 import com.google.crypto.tink.integration.android.AndroidKeysetManager;
-import com.google.crypto.tink.shaded.protobuf.InvalidProtocolBufferException;
 import com.google.crypto.tink.streamingaead.StreamingAeadConfig;
 
 import java.io.File;
@@ -43,7 +42,6 @@
 import java.io.OutputStream;
 import java.nio.channels.FileChannel;
 import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
 
 /**
  * Class used to create and read encrypted files.
@@ -223,24 +221,15 @@
                     .withSharedPref(mContext, mKeysetAlias, mKeysetPrefName)
                     .withMasterKeyUri(KEYSTORE_PATH_URI + mMasterKeyAlias);
 
-            AndroidKeysetManager keysetManager;
-
-            try {
-                // Building the keyset manager involves shared pref filesystem operations. To
-                // control access to this global state in multi-threaded contexts we need to
-                // ensure mutual exclusion of the build() function.
-                synchronized (sLock) {
-                    keysetManager = keysetManagerBuilder.build();
-                }
-            } catch (InvalidProtocolBufferException e) {
-                throw new InvalidKeyException("Used the wrong key (\"" + mMasterKeyAlias + "\") to "
-                        + "decrypt the keyset (\"" + mKeysetAlias + "\"). If you are using "
-                        + "multiple master keys you must call setKeysetPrefName() or "
-                        + "setKeysetAlias() to differentiate.",
-                        e);
+            // Building the keyset manager involves shared pref filesystem operations. To control
+            // access to this global state in multi-threaded contexts we need to ensure mutual
+            // exclusion of the build() function.
+            AndroidKeysetManager androidKeysetManager;
+            synchronized (sLock) {
+                androidKeysetManager = keysetManagerBuilder.build();
             }
 
-            KeysetHandle streamingAeadKeysetHandle = keysetManager.getKeysetHandle();
+            KeysetHandle streamingAeadKeysetHandle = androidKeysetManager.getKeysetHandle();
             StreamingAead streamingAead =
                     streamingAeadKeysetHandle.getPrimitive(StreamingAead.class);
 
diff --git a/settings.gradle b/settings.gradle
index bddb08f..78a99ac 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -612,7 +612,6 @@
 includeProject(":datastore:datastore-core", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-core-okio", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-compose-samples", [BuildType.COMPOSE])
-includeProject(":datastore:datastore-multiprocess", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-preferences", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-core", [BuildType.MAIN, BuildType.KMP])
 includeProject(":datastore:datastore-preferences-proto", [BuildType.MAIN, BuildType.KMP])
@@ -630,6 +629,7 @@
 includeProject(":dynamicanimation:dynamicanimation", [BuildType.MAIN])
 includeProject(":dynamicanimation:dynamicanimation-ktx", [BuildType.MAIN])
 includeProject(":emoji2:emoji2-emojipicker", [BuildType.MAIN])
+includeProject(":emoji2:emoji2-emojipicker:emoji2-emojipicker-samples", "emoji2/emoji2-emojipicker/samples",  [BuildType.MAIN])
 includeProject(":emoji:emoji", [BuildType.MAIN])
 includeProject(":emoji:emoji-appcompat", [BuildType.MAIN])
 includeProject(":emoji:emoji-bundled", [BuildType.MAIN])
diff --git a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
index 357de26..0b841d5 100644
--- a/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
+++ b/slice/slice-view/src/androidTest/java/androidx/slice/SliceViewManagerTest.java
@@ -117,7 +117,10 @@
                 .scheme(ContentResolver.SCHEME_CONTENT)
                 .authority(mContext.getPackageName())
                 .build();
-        Uri longerUri = uri.buildUpon().appendPath("something").build();
+        Uri longerUri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_CONTENT)
+                .authority(mContext.getPackageName())
+                .appendPath("something").build();
         try {
             mViewManager.pinSlice(uri);
             mViewManager.pinSlice(longerUri);
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTest.java
index 1bef873..ad2999f 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/BySelectorTest.java
@@ -81,22 +81,22 @@
         launchTestActivity(BySelectorTestActivity.class);
 
         // String as the exact content description.
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc("The button desc.")));
-        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc("button")));
+        assertTrue(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc("The is\nthe desc.")));
+        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc("desc")));
 
         // Pattern of the content description.
         assertTrue(mDevice.hasObject(
-                By.res(TEST_APP, "desc_family").desc(Pattern.compile(".*button.*"))));
-        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc(Pattern.compile(
-                ".*not_button.*"))));
+                By.res(TEST_APP, "desc_family").desc(Pattern.compile(".*desc.*", Pattern.DOTALL))));
+        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").desc(
+                Pattern.compile(".*not_desc.*", Pattern.DOTALL))));
     }
 
     @Test
     public void testDescContains() {
         launchTestActivity(BySelectorTestActivity.class);
 
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "desc_family").descContains("button")));
-        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").descContains("not_button")));
+        assertTrue(mDevice.hasObject(By.res(TEST_APP, "desc_family").descContains("desc")));
+        assertFalse(mDevice.hasObject(By.res(TEST_APP, "desc_family").descContains("not_desc")));
     }
 
     @Test
@@ -150,14 +150,14 @@
         launchTestActivity(BySelectorTestActivity.class);
 
         // Single string as the exact content of the text.
-        assertTrue(mDevice.hasObject(By.res(TEST_APP, "text_family").text("This is the text.")));
+        assertTrue(mDevice.hasObject(By.res(TEST_APP, "text_family").text("This is\nthe text.")));
         assertFalse(mDevice.hasObject(By.res(TEST_APP, "text_family").text("the text")));
 
         // Pattern of the text.
         assertTrue(mDevice.hasObject(
-                By.res(TEST_APP, "text_family").text(Pattern.compile(".*text.*"))));
-        assertFalse(mDevice.hasObject(
-                By.res(TEST_APP, "text_family").text(Pattern.compile(".*nottext.*"))));
+                By.res(TEST_APP, "text_family").text(Pattern.compile(".*text.*", Pattern.DOTALL))));
+        assertFalse(mDevice.hasObject(By.res(TEST_APP, "text_family").text(
+                Pattern.compile(".*nottext.*", Pattern.DOTALL))));
     }
 
     @Test
diff --git a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
index 5d92fd9..6764117 100644
--- a/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
+++ b/test/uiautomator/integration-tests/testapp/src/androidTest/java/androidx/test/uiautomator/testapp/MultiWindowTest.java
@@ -36,6 +36,7 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.uiautomator.By;
 import androidx.test.uiautomator.BySelector;
+import androidx.test.uiautomator.Configurator;
 import androidx.test.uiautomator.UiObject2;
 import androidx.test.uiautomator.Until;
 
@@ -59,6 +60,20 @@
     }
 
     @Test
+    @SdkSuppress(minSdkVersion = 21)
+    public void testMultiWindow_reconnected() {
+        Configurator configurator = Configurator.getInstance();
+        int initialFlags = configurator.getUiAutomationFlags();
+        // Update the UiAutomation flags to force the underlying connection to be recreated.
+        configurator.setUiAutomationFlags(5);
+        try {
+            assertTrue(mDevice.hasObject(By.res("com.android.systemui", "status_bar")));
+        } finally {
+            configurator.setUiAutomationFlags(initialFlags);
+        }
+    }
+
+    @Test
     @SdkSuppress(minSdkVersion = 24)
     public void testMultiWindow_pictureInPicture() {
         BySelector defaultMode = By.res(TEST_APP, "pip_mode").text("Default Mode");
diff --git a/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_test_activity.xml b/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_test_activity.xml
index f983ce0..2130bff 100644
--- a/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_test_activity.xml
+++ b/test/uiautomator/integration-tests/testapp/src/main/res/layout/byselector_test_activity.xml
@@ -30,25 +30,6 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:text="clazz" />
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
-
-        <Button
-            android:id="@+id/desc_family"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:contentDescription="The button desc."
-            android:text="desc_family" />
-    </LinearLayout>
-
-    <LinearLayout
-        android:layout_width="fill_parent"
-        android:layout_height="wrap_content"
-        android:orientation="horizontal">
 
         <Button
             android:id="@+id/res"
@@ -63,10 +44,18 @@
         android:orientation="horizontal">
 
         <Button
-            android:id="@+id/text_family"
+            android:id="@+id/desc_family"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="This is the text." />
+            android:contentDescription="The is\nthe desc."
+            android:text="desc_family" />
+
+        <Button
+            android:id="@+id/text_family"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:text="This is\nthe text."
+            android:lineSpacingMultiplier="0.5" />
     </LinearLayout>
 
     <LinearLayout
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 d987cee..684466e 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
@@ -16,8 +16,6 @@
 
 package androidx.test.uiautomator;
 
-import android.view.accessibility.AccessibilityNodeInfo;
-
 import androidx.annotation.NonNull;
 
 import java.util.LinkedList;
@@ -159,7 +157,7 @@
      * if its content description exactly matches the {@code contentDescription} parameter and all
      * other criteria for this selector are met.
      *
-     * @param contentDescription The exact value to match.
+     * @param contentDescription The exact value to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector desc(@NonNull String contentDescription) {
@@ -173,13 +171,14 @@
      * if its content description contains the {@code substring} parameter and all other criteria
      * for this selector are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descContains(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
+        return desc(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
+                Pattern.DOTALL));
     }
 
     /**
@@ -187,13 +186,14 @@
      * if its content description starts with the {@code substring} parameter and all other criteria
      * for this selector are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descStartsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
+        return desc(
+                Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
     }
 
     /**
@@ -201,13 +201,14 @@
      * if its content description ends with the {@code substring} parameter and all other criteria
      * for this selector are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector descEndsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return desc(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
+        return desc(
+                Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
     }
 
     /**
@@ -314,7 +315,7 @@
      * text value exactly matches the {@code textValue} parameter and all other criteria for this
      * selector are met.
      *
-     * @param textValue The exact value to match.
+     * @param textValue The exact value to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector text(@NonNull String textValue) {
@@ -328,13 +329,14 @@
      * text value contains the {@code substring} parameter and all other criteria for this selector
      * are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textContains(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
+        return text(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring)),
+                Pattern.DOTALL));
     }
 
     /**
@@ -342,13 +344,14 @@
      * text value starts with the {@code substring} parameter and all other criteria for this
      * selector are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textStartsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
+        return text(
+                Pattern.compile(String.format("^%s.*$", Pattern.quote(substring)), Pattern.DOTALL));
     }
 
     /**
@@ -356,13 +359,14 @@
      * text value ends with the {@code substring} parameter and all other criteria for this selector
      * are met.
      *
-     * @param substring The substring to match.
+     * @param substring The substring to match (case-sensitive).
      * @return A reference to this {@link BySelector}.
      */
     public @NonNull BySelector textEndsWith(@NonNull String substring) {
         checkNotNull(substring, "substring cannot be null");
 
-        return text(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
+        return text(
+                Pattern.compile(String.format("^.*%s$", Pattern.quote(substring)), Pattern.DOTALL));
     }
 
     /** Sets the text value criteria for matching. A UI element will be considered a match if its
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 ee4b9dc..2377af4 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
@@ -17,11 +17,9 @@
 package androidx.test.uiautomator;
 
 import android.accessibilityservice.AccessibilityService;
-import android.app.Instrumentation;
 import android.app.Service;
 import android.app.UiAutomation;
 import android.app.UiAutomation.AccessibilityEventFilter;
-import android.content.Context;
 import android.graphics.Point;
 import android.os.Build;
 import android.os.PowerManager;
@@ -59,7 +57,7 @@
     private final KeyCharacterMap mKeyCharacterMap =
             KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
 
-    private final Instrumentation mInstrumentation;
+    private final UiDevice mDevice;
 
     private static final long REGULAR_CLICK_LENGTH = 100;
 
@@ -68,8 +66,8 @@
     // Inserted after each motion event injection.
     private static final int MOTION_EVENT_INJECTION_DELAY_MILLIS = 5;
 
-    public InteractionController(Instrumentation instrumentation) {
-        mInstrumentation = instrumentation;
+    InteractionController(UiDevice device) {
+        mDevice = device;
     }
 
     /**
@@ -687,7 +685,8 @@
      * @return true if the screen is ON else false
      */
     public boolean isScreenOn() {
-        PowerManager pm = (PowerManager)getContext().getSystemService(Service.POWER_SERVICE);
+        PowerManager pm = (PowerManager) mDevice.getInstrumentation().getContext().getSystemService(
+                Service.POWER_SERVICE);
         return pm.isScreenOn();
     }
 
@@ -847,14 +846,6 @@
     }
 
     UiAutomation getUiAutomation() {
-        return UiDevice.getUiAutomation(getInstrumentation());
-    }
-
-    Context getContext() {
-        return getInstrumentation().getContext();
-    }
-
-    Instrumentation getInstrumentation() {
-        return mInstrumentation;
+        return mDevice.getUiAutomation();
     }
 }
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 0a3ea48..452757f 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
@@ -16,7 +16,6 @@
 
 package androidx.test.uiautomator;
 
-import android.app.Instrumentation;
 import android.app.UiAutomation.OnAccessibilityEventListener;
 import android.os.SystemClock;
 import android.util.Log;
@@ -43,7 +42,7 @@
     static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);
     private static final boolean VERBOSE = Log.isLoggable(LOG_TAG, Log.VERBOSE);
 
-    private final Instrumentation mInstrumentation;
+    private final UiDevice mDevice;
 
     final Object mLock = new Object();
 
@@ -90,9 +89,9 @@
         }
     };
 
-    public QueryController(Instrumentation instrumentation) {
-        mInstrumentation = instrumentation;
-        UiDevice.getUiAutomation(instrumentation).setOnAccessibilityEventListener(mEventListener);
+    QueryController(UiDevice device) {
+        mDevice = device;
+        mDevice.getUiAutomation().setOnAccessibilityEventListener(mEventListener);
     }
 
     /**
@@ -179,7 +178,7 @@
         long waitInterval = 250;
         AccessibilityNodeInfo rootNode = null;
         for (int x = 0; x < maxRetry; x++) {
-            rootNode = UiDevice.getUiAutomation(getInstrumentation()).getRootInActiveWindow();
+            rootNode = mDevice.getUiAutomation().getRootInActiveWindow();
             if (rootNode != null) {
                 return rootNode;
             }
@@ -529,8 +528,7 @@
      */
     public void waitForIdle(long timeout) {
         try {
-            UiDevice.getUiAutomation(getInstrumentation())
-                    .waitForIdle(QUIET_TIME_TO_BE_CONSIDERED_IDLE_STATE, timeout);
+            mDevice.getUiAutomation().waitForIdle(QUIET_TIME_TO_BE_CONSIDERED_IDLE_STATE, timeout);
         } catch (TimeoutException e) {
             Log.w(LOG_TAG, "Could not detect idle state.");
         }
@@ -546,8 +544,4 @@
             l.append(String.format(". . [%d]: %s", mPatternCounter, str));
         return l.toString();
     }
-
-    private Instrumentation getInstrumentation() {
-        return mInstrumentation;
-    }
 }
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 962f0c9..18177fe 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
@@ -85,6 +85,10 @@
     private final DisplayManager mDisplayManager;
     private final WaitMixin<UiDevice> mWaitMixin = new WaitMixin<>(this);
 
+    // Track accessibility service flags to determine when the underlying connection has changed.
+    private int mCachedServiceFlags = -1;
+    private boolean mCompressed = false;
+
     // Lazily created UI context per display, used to access UI components/configurations.
     private final Map<Integer, Context> mUiContexts = new HashMap<>();
 
@@ -96,17 +100,10 @@
     /** Private constructor. Clients should use {@link UiDevice#getInstance(Instrumentation)}. */
     UiDevice(Instrumentation instrumentation) {
         mInstrumentation = instrumentation;
-        mQueryController = new QueryController(instrumentation);
-        mInteractionController = new InteractionController(instrumentation);
+        mQueryController = new QueryController(this);
+        mInteractionController = new InteractionController(this);
         mDisplayManager = (DisplayManager) instrumentation.getContext().getSystemService(
                 Service.DISPLAY_SERVICE);
-
-        // Enable multi-window support by subscribing to window information for API 21+.
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
-            AccessibilityServiceInfo info = getUiAutomation().getServiceInfo();
-            info.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
-            getUiAutomation().setServiceInfo(info);
-        }
     }
 
     boolean isInWatcherContext() {
@@ -223,12 +220,8 @@
      * @param compressed true to enable compression; else, false to disable
      */
     public void setCompressedLayoutHeirarchy(boolean compressed) {
-        AccessibilityServiceInfo info = getUiAutomation().getServiceInfo();
-        if (compressed)
-            info.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
-        else
-            info.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
-        getUiAutomation().setServiceInfo(info);
+        mCompressed = compressed;
+        mCachedServiceFlags = -1; // Reset cached accessibility service flags to force an update.
     }
 
     /**
@@ -1107,21 +1100,39 @@
         return context;
     }
 
-    static UiAutomation getUiAutomation(final Instrumentation instrumentation) {
+    UiAutomation getUiAutomation() {
+        UiAutomation uiAutomation;
         int flags = Configurator.getInstance().getUiAutomationFlags();
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
-            return Api24Impl.getUiAutomation(instrumentation, flags);
+            uiAutomation = Api24Impl.getUiAutomation(getInstrumentation(), flags);
         } else {
-            // Custom flags not supported prior to N.
             if (flags != Configurator.DEFAULT_UIAUTOMATION_FLAGS) {
-                Log.w(LOG_TAG, "UiAutomation flags not supported prior to N - ignoring.");
+                Log.w(LOG_TAG, "UiAutomation flags not supported prior to API 24");
             }
-            return instrumentation.getUiAutomation();
+            uiAutomation = getInstrumentation().getUiAutomation();
         }
-    }
 
-    UiAutomation getUiAutomation() {
-        return getUiAutomation(getInstrumentation());
+        // Verify and update the accessibility service flags if necessary. These might get reset
+        // if the underlying UiAutomationConnection is recreated.
+        AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo();
+        if (serviceInfo.flags != mCachedServiceFlags) {
+            // Enable multi-window support for API 21+.
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS;
+            }
+            // Enable or disable hierarchy compression.
+            if (mCompressed) {
+                serviceInfo.flags &= ~AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+            } else {
+                serviceInfo.flags |= AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;
+            }
+            Log.d(LOG_TAG,
+                    String.format("Setting accessibility service flags: %d", serviceInfo.flags));
+            uiAutomation.setServiceInfo(serviceInfo);
+            mCachedServiceFlags = serviceInfo.flags;
+        }
+
+        return uiAutomation;
     }
 
     QueryController getQueryController() {
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index 9bf790b..54dd5e5 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -29,9 +29,12 @@
 import android.util.Log;
 import android.view.Display;
 import android.view.KeyEvent;
+import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityWindowInfo;
+import android.widget.Checkable;
+import android.widget.TextView;
 
 import androidx.annotation.DoNotInline;
 import androidx.annotation.NonNull;
@@ -42,43 +45,41 @@
 import java.util.List;
 
 /**
- * A {@link UiObject2} represents a UI element. Unlike {@link UiObject}, it is bound to a particular
- * view instance and can become stale if the underlying view object is destroyed. As a result, it
- * may be necessary to call {@link UiDevice#findObject(BySelector)} to obtain a new
- * {@link UiObject2} instance if the UI changes significantly.
+ * Represents a UI element, and exposes methods for performing gestures (clicks, swipes) or
+ * searching through its children.
+ *
+ * <p>Unlike {@link UiObject}, {@link UiObject2} is bound to a particular view instance and can
+ * become stale if the underlying view object is destroyed. As a result, it may be necessary
+ * to call {@link UiDevice#findObject(BySelector)} to obtain a new {@link UiObject2} instance if the
+ * UI changes significantly.
  */
 public class UiObject2 implements Searchable {
 
     private static final String TAG = UiObject2.class.getSimpleName();
 
-    private UiDevice mDevice;
-    private GestureController mGestureController;
-    private BySelector mSelector;  // Hold this mainly for debugging
+    // Default gesture speeds and timeouts.
+    private static final int DEFAULT_SWIPE_SPEED = 5_000; // dp/s
+    private static final int DEFAULT_SCROLL_SPEED = 5_000; // dp/s
+    private static final int DEFAULT_FLING_SPEED = 7_500; // dp/s
+    private static final int DEFAULT_DRAG_SPEED = 2_500; // dp/s
+    private static final int DEFAULT_PINCH_SPEED = 2_500; // dp/s
+    private static final long SCROLL_TIMEOUT = 1_000; // ms
+    private static final long FLING_TIMEOUT = 5_000; // ms; longer as motion may continue.
+
+    private final UiDevice mDevice;
+    private final BySelector mSelector;
+    private final GestureController mGestureController;
+    private final WaitMixin<UiObject2> mWaitMixin = new WaitMixin<>(this);
+    private final int mDisplayId;
+    private final float mDisplayDensity;
     private AccessibilityNodeInfo mCachedNode;
-    private int mDisplayId;
-    private float mDisplayDensity;
 
-    // Margins
-    private int mMarginLeft   = 5;
-    private int mMarginTop    = 5;
-    private int mMarginRight  = 5;
+    // Margins used for gestures (avoids touching too close to the object's edge).
+    private int mMarginLeft = 5;
+    private int mMarginTop = 5;
+    private int mMarginRight = 5;
     private int mMarginBottom = 5;
 
-    // Default gesture speeds
-    private static final int DEFAULT_SWIPE_SPEED  = 5000;
-    private static final int DEFAULT_SCROLL_SPEED = 5000;
-    private static final int DEFAULT_FLING_SPEED = 7500;
-    private static final int DEFAULT_DRAG_SPEED = 2500;
-    private static final int DEFAULT_PINCH_SPEED = 2500;
-    // Short, since we should stop scrolling after the gesture completes.
-    private final long SCROLL_TIMEOUT = 1000;
-    // Longer, since we may continue to scroll after the gesture completes.
-    private final long FLING_TIMEOUT = 5000;
-
-    // Get wait functionality from a mixin
-    private WaitMixin<UiObject2> mWaitMixin = new WaitMixin<UiObject2>(this);
-
-
     /** Package-private constructor. Used by {@link UiDevice#findObject(BySelector)}. */
     UiObject2(UiDevice device, BySelector selector, AccessibilityNodeInfo cachedNode) {
         mDevice = device;
@@ -99,7 +100,6 @@
         mDisplayDensity = (float) densityDpi / DisplayMetrics.DENSITY_DEFAULT;
     }
 
-    /** {@inheritDoc} */
     @Override
     public boolean equals(Object object) {
         if (this == object) {
@@ -116,7 +116,6 @@
         }
     }
 
-    /** {@inheritDoc} */
     @Override
     public int hashCode() {
         return getAccessibilityNodeInfo().hashCode();
@@ -128,7 +127,6 @@
         mCachedNode = null;
     }
 
-
     // Settings
 
     /** Sets the margins used for gestures in pixels. */
@@ -144,28 +142,27 @@
         mMarginBottom = bottom;
     }
 
-
     // Wait functions
 
     /**
-     * Waits for given the {@code condition} to be met.
+     * Waits for a {@code condition} to be met.
      *
-     * @param condition The {@link UiObject2Condition} to evaluate.
-     * @param timeout Maximum amount of time to wait in milliseconds.
-     * @return The final result returned by the {@code condition}, or null if the {@code condition}
-     * was not met before the {@code timeout}.
+     * @param condition The {@link UiObject2Condition} to wait for.
+     * @param timeout   The maximum time in milliseconds to wait for.
+     * @return The final result returned by the {@code condition}, or {@code null} if the {@code
+     * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull UiObject2Condition<U> condition, long timeout) {
         return mWaitMixin.wait(condition, timeout);
     }
 
     /**
-     * Waits for given the {@code condition} to be met.
+     * Waits for a {@code condition} to be met.
      *
      * @param condition The {@link SearchCondition} to evaluate.
-     * @param timeout Maximum amount of time to wait in milliseconds.
-     * @return The final result returned by the {@code condition}, or null if the {@code condition}
-     * was not met before the {@code timeout}.
+     * @param timeout   The maximum time in milliseconds to wait for.
+     * @return The final result returned by the {@code condition}, or {@code null} if the {@code
+     * condition} was not met before the {@code timeout}.
      */
     public <U> U wait(@NonNull SearchCondition<U> condition, long timeout) {
         return mWaitMixin.wait(condition, timeout);
@@ -173,7 +170,7 @@
 
     // Search functions
 
-    /** Returns this object's parent, or null if it has no parent. */
+    /** Returns this object's parent, or {@code null} if it has no parent. */
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public UiObject2 getParent() {
         AccessibilityNodeInfo parent = getAccessibilityNodeInfo().getParent();
@@ -191,7 +188,7 @@
         return findObjects(By.depth(1));
     }
 
-    /** Returns whether there is a match for the given criteria under this object. */
+    /** Returns {@code true} if there is a nested element which matches the {@code selector}. */
     @Override
     public boolean hasObject(@NonNull BySelector selector) {
         AccessibilityNodeInfo node =
@@ -204,8 +201,8 @@
     }
 
     /**
-     * Searches all elements under this object and returns the first object to match the criteria,
-     * or null if no matching objects are found.
+     * Searches all elements under this object and returns the first one to match the {@code
+     * selector}, or {@code null} if no matching objects are found.
      */
     @Override
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
@@ -215,34 +212,34 @@
         return node != null ? new UiObject2(getDevice(), selector, node) : null;
     }
 
-    /** Searches all elements under this object and returns all objects that match the criteria. */
+    /**
+     * Searches all elements under this object and returns those that match the {@code selector}.
+     */
     @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(getDevice(), selector, getAccessibilityNodeInfo())) {
-
             ret.add(new UiObject2(getDevice(), selector, node));
         }
-
         return ret;
     }
 
-
     // Attribute accessors
 
+    /** Returns the ID of the display containing this object. */
     public int getDisplayId() {
         return mDisplayId;
     }
 
-    /** Returns the visible bounds of this object in screen coordinates. */
+    /** Returns this object's visible bounds. */
     @NonNull
     public Rect getVisibleBounds() {
         return getVisibleBounds(getAccessibilityNodeInfo());
     }
 
-    /** Returns the visible bounds of this object with the margins removed. */
+    /** Returns this object's visible bounds with the margins removed. */
     private Rect getVisibleBoundsForGestures() {
         Rect ret = getVisibleBounds();
         ret.left = ret.left + mMarginLeft;
@@ -252,12 +249,7 @@
         return ret;
     }
 
-    /**
-     * Clips the point to the visible bounds of this objects with the margins removed.
-     *
-     * @param point The point which may be clipped in the {@link #getVisibleBoundsForGestures()}.
-     * @return false if the {@code point} is clipped.
-     */
+    /** Updates a {@code point} to ensure it is within this object's visible bounds. */
     private boolean clipToGestureBounds(Point point) {
         final Rect bounds = getVisibleBoundsForGestures();
         if (bounds.contains(point.x, point.y)) {
@@ -268,7 +260,7 @@
         return false;
     }
 
-    /** Returns the visible bounds of {@code node} in screen coordinates. */
+    /** Returns the visible bounds of a {@code node}. */
     @SuppressWarnings("RectIntersectReturnValueIgnored")
     private Rect getVisibleBounds(AccessibilityNodeInfo node) {
         // Get the object bounds in screen coordinates
@@ -279,7 +271,7 @@
         final int displayId = getDisplayId();
         if (displayId == Display.DEFAULT_DISPLAY) {
             final Rect screen =
-                new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
+                    new Rect(0, 0, getDevice().getDisplayWidth(), getDevice().getDisplayHeight());
             ret.intersect(screen);
         } else {
             final DisplayManager dm =
@@ -306,8 +298,8 @@
         }
 
         // Find the visible bounds of our first scrollable ancestor
-        AccessibilityNodeInfo ancestor = null;
-        for (ancestor = node.getParent(); ancestor != null; ancestor = ancestor.getParent()) {
+        for (AccessibilityNodeInfo ancestor = node.getParent(); ancestor != null;
+                ancestor = ancestor.getParent()) {
             // If this ancestor is scrollable
             if (ancestor.isScrollable()) {
                 // Trim any portion of the bounds that are hidden by the non-visible portion of our
@@ -321,24 +313,25 @@
         return ret;
     }
 
-    /** Returns a point in the center of the visible bounds of this object. */
+    /** Returns a point in the center of this object's visible bounds. */
     @NonNull
     public Point getVisibleCenter() {
         Rect bounds = getVisibleBounds();
         return new Point(bounds.centerX(), bounds.centerY());
     }
 
-    /**
-     * Returns the class name of the underlying {@link android.view.View} represented by this
-     * object.
-     */
+    /** Returns the class name of this object's underlying {@link View}. */
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public String getClassName() {
         CharSequence chars = getAccessibilityNodeInfo().getClassName();
         return chars != null ? chars.toString() : null;
     }
 
-    /** Returns the content description for this object. */
+    /**
+     * Returns this object's content description.
+     *
+     * @see View#getContentDescription()
+     */
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public String getContentDescription() {
         CharSequence chars = getAccessibilityNodeInfo().getContentDescription();
@@ -352,84 +345,119 @@
         return chars != null ? chars.toString() : null;
     }
 
-    /** Returns the fully qualified resource name for this object's id. */
+    /** Returns the fully qualified resource name for this object's ID. */
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public String getResourceName() {
         CharSequence chars = getAccessibilityNodeInfo().getViewIdResourceName();
         return chars != null ? chars.toString() : null;
     }
 
-    /** Returns the text value for this object. */
+    /**
+     * Returns this object's text content.
+     *
+     * @see TextView#getText()
+     */
     @SuppressLint("UnknownNullness") // Avoid unnecessary null checks from nullable testing APIs.
     public String getText() {
         CharSequence chars = getAccessibilityNodeInfo().getText();
         return chars != null ? chars.toString() : null;
     }
 
-    /** Returns whether this object is checkable. */
+    /**
+     * Returns {@code true} if this object is checkable.
+     *
+     * @see Checkable
+     */
     public boolean isCheckable() {
         return getAccessibilityNodeInfo().isCheckable();
     }
 
-    /** Returns whether this object is checked. */
+    /**
+     * Returns {@code true} if this object is checked.
+     *
+     * @see Checkable#isChecked()
+     */
     public boolean isChecked() {
         return getAccessibilityNodeInfo().isChecked();
     }
 
-    /** Returns whether this object is clickable. */
+    /**
+     * Returns {@code true} if this object is clickable.
+     *
+     * @see View#isClickable()
+     */
     public boolean isClickable() {
         return getAccessibilityNodeInfo().isClickable();
     }
 
-    /** Returns whether this object is enabled. */
+    /**
+     * Returns {@code true} if this object is enabled.
+     *
+     * @see TextView#isEnabled()
+     */
     public boolean isEnabled() {
         return getAccessibilityNodeInfo().isEnabled();
     }
 
-    /** Returns whether this object is focusable. */
+    /**
+     * Returns {@code true} if this object is focusable.
+     *
+     * @see View#isFocusable()
+     */
     public boolean isFocusable() {
         return getAccessibilityNodeInfo().isFocusable();
     }
 
-    /** Returns whether this object is focused. */
+    /**
+     * Returns {@code true} if this object is focused.
+     *
+     * @see View#isFocused()
+     */
     public boolean isFocused() {
         return getAccessibilityNodeInfo().isFocused();
     }
 
-    /** Returns whether this object is long clickable. */
+    /**
+     * Returns {@code true} if this object is long clickable.
+     *
+     * @see View#isLongClickable()
+     */
     public boolean isLongClickable() {
         return getAccessibilityNodeInfo().isLongClickable();
     }
 
-    /** Returns whether this object is scrollable. */
+    /** Returns {@code true} if this object is scrollable. */
     public boolean isScrollable() {
         return getAccessibilityNodeInfo().isScrollable();
     }
 
-    /** Returns whether this object is selected. */
+    /**
+     * Returns {@code true} if this object is selected.
+     *
+     * @see View#isSelected()
+     */
     public boolean isSelected() {
         return getAccessibilityNodeInfo().isSelected();
     }
 
-
     // Actions
 
-    /** Clears the text content if this object is an editable field. */
+    /** Clears this object's text content if it is an editable field. */
     public void clear() {
         setText("");
     }
 
-    /** Clicks on this object. */
+    /** Clicks on this object's center. */
     public void click() {
-        Log.v(TAG, String.format("click(center=%s)", getVisibleCenter()));
-        mGestureController.performGesture(Gestures.click(getVisibleCenter(), getDisplayId()));
+        Point center = getVisibleCenter();
+        Log.v(TAG, String.format("click(center=%s)", center));
+        mGestureController.performGesture(Gestures.click(center, getDisplayId()));
     }
 
     /**
-     * Clicks on the {@code point} of this object.
+     * Clicks on a {@code point} within this object's visible bounds.
      *
-     * @param point The point to click. Clipped to the visible bounds of this object with gesture
-     *        margins removed.
+     * @param point The point to click (clipped to ensure it is within the visible bounds).
      */
     public void click(@NonNull Point point) {
         clipToGestureBounds(point);
@@ -437,21 +465,18 @@
         mGestureController.performGesture(Gestures.click(point, getDisplayId()));
     }
 
-    /** Performs a click on this object that lasts for {@code duration} milliseconds. */
+    /** Clicks on this object's center for {@code duration} milliseconds. */
     public void click(long duration) {
-        Log.v(TAG, String.format("click(center=%s,duration=%d)",
-                getVisibleCenter(), duration));
-        mGestureController.performGesture(
-                Gestures.click(getVisibleCenter(), duration, getDisplayId()));
+        Point center = getVisibleCenter();
+        Log.v(TAG, String.format("click(center=%s,duration=%d)", center, duration));
+        mGestureController.performGesture(Gestures.click(center, duration, getDisplayId()));
     }
 
     /**
-     * Performs a click on the {@code point} of this object that lasts for {@code duration}
-     * milliseconds.
+     * Clicks on a {@code point} within this object's visible bounds.
      *
-     * @param point The point to click. Clipped to the visible bounds of this object with gesture
-     *        margins removed.
-     * @param duration The duration in milliseconds to press {@code point}.
+     * @param point    The point to click (clipped to ensure it is within the visible bounds).
+     * @param duration The click duration in milliseconds.
      */
     public void click(@NonNull Point point, long duration) {
         clipToGestureBounds(point);
@@ -459,22 +484,26 @@
         mGestureController.performGesture(Gestures.click(point, duration, getDisplayId()));
     }
 
-    /** Clicks on this object, and waits for the given condition to become true. */
+    /**
+     * Clicks on this object's center, and waits for a {@code condition} to be met.
+     *
+     * @param condition The {@link EventCondition} to wait for.
+     * @param timeout   The maximum time in milliseconds to wait for.
+     */
     public <U> U clickAndWait(@NonNull EventCondition<U> condition, long timeout) {
-        Log.v(TAG, String.format("clickAndWait(center=%s,timeout=%d)",
-                getVisibleCenter(), timeout));
+        Point center = getVisibleCenter();
+        Log.v(TAG, String.format("clickAndWait(center=%s,timeout=%d)", center, timeout));
         return mGestureController.performGestureAndWait(condition, timeout,
-                Gestures.click(getVisibleCenter(), getDisplayId()));
+                Gestures.click(center, getDisplayId()));
     }
 
     /**
-     * Clicks on the {@code point} of this object, and waits for the given {@code condition} to
-     * become true.
+     * Clicks on a {@code point} within this object's visible bounds, and waits for a {@code
+     * condition} to be met.
      *
-     * @param point The point to click. Clipped to the visible bounds of this object with gesture
-     *        margins removed.
+     * @param point     The point to click (clipped to ensure it is within the visible bounds).
      * @param condition The {@link EventCondition} to wait for.
-     * @param timeout The duration in milliseconds waiting for {@code condition} before timed out.
+     * @param timeout   The maximum time in milliseconds to wait for.
      */
     public <U> U clickAndWait(@NonNull Point point, @NonNull EventCondition<U> condition,
             long timeout) {
@@ -485,35 +514,34 @@
     }
 
     /**
-     * Drags this object to the specified location.
+     * Drags this object to the specified point.
      *
-     * @param dest The end point that this object should be dragged to.
+     * @param dest The end point to drag this object to.
      */
     public void drag(@NonNull Point dest) {
-        drag(dest, (int)(DEFAULT_DRAG_SPEED * mDisplayDensity));
+        drag(dest, (int) (DEFAULT_DRAG_SPEED * mDisplayDensity));
     }
 
     /**
-     * Drags this object to the specified location.
+     * Drags this object to the specified point.
      *
-     * @param dest The end point that this object should be dragged to.
+     * @param dest  The end point to drag this object to.
      * @param speed The speed at which to perform this gesture in pixels per second.
      */
     public void drag(@NonNull Point dest, int speed) {
         if (speed < 0) {
             throw new IllegalArgumentException("Speed cannot be negative");
         }
-        Log.v(TAG, String.format("drag(start=%s,dest=%s,speed=%d)",
-                getVisibleCenter(), dest, speed));
-        mGestureController.performGesture(
-                Gestures.drag(getVisibleCenter(), dest, speed, getDisplayId()));
+        Point center = getVisibleCenter();
+        Log.v(TAG, String.format("drag(start=%s,dest=%s,speed=%d)", center, dest, speed));
+        mGestureController.performGesture(Gestures.drag(center, dest, speed, getDisplayId()));
     }
 
-    /** Performs a long click on this object. */
+    /** Performs a long click on this object's center. */
     public void longClick() {
-        Log.v(TAG, String.format("longClick(center=%s)",
-                getVisibleCenter()));
-        mGestureController.performGesture(Gestures.longClick(getVisibleCenter(), getDisplayId()));
+        Point center = getVisibleCenter();
+        Log.v(TAG, String.format("longClick(center=%s)", center));
+        mGestureController.performGesture(Gestures.longClick(center, getDisplayId()));
     }
 
     /**
@@ -522,14 +550,14 @@
      * @param percent The size of the pinch as a percentage of this object's size.
      */
     public void pinchClose(float percent) {
-        pinchClose(percent, (int)(DEFAULT_PINCH_SPEED * mDisplayDensity));
+        pinchClose(percent, (int) (DEFAULT_PINCH_SPEED * mDisplayDensity));
     }
 
     /**
      * Performs a pinch close gesture on this object.
      *
      * @param percent The size of the pinch as a percentage of this object's size.
-     * @param speed The speed at which to perform this gesture in pixels per second.
+     * @param speed   The speed at which to perform this gesture in pixels per second.
      */
     public void pinchClose(float percent, int speed) {
         if (percent < 0.0f || percent > 1.0f) {
@@ -538,10 +566,11 @@
         if (speed < 0) {
             throw new IllegalArgumentException("Speed cannot be negative");
         }
+        Rect bounds = getVisibleBoundsForGestures();
         Log.v(TAG, String.format("pinchClose(bounds=%s,percent=%f,speed=%d)",
-                getVisibleBoundsForGestures(), percent, speed));
+                bounds, percent, speed));
         mGestureController.performGesture(
-                Gestures.pinchClose(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+                Gestures.pinchClose(bounds, percent, speed, getDisplayId()));
     }
 
     /**
@@ -550,14 +579,14 @@
      * @param percent The size of the pinch as a percentage of this object's size.
      */
     public void pinchOpen(float percent) {
-        pinchOpen(percent, (int)(DEFAULT_PINCH_SPEED * mDisplayDensity));
+        pinchOpen(percent, (int) (DEFAULT_PINCH_SPEED * mDisplayDensity));
     }
 
     /**
      * Performs a pinch open gesture on this object.
      *
      * @param percent The size of the pinch as a percentage of this object's size.
-     * @param speed The speed at which to perform this gesture in pixels per second.
+     * @param speed   The speed at which to perform this gesture in pixels per second.
      */
     public void pinchOpen(float percent, int speed) {
         if (percent < 0.0f || percent > 1.0f) {
@@ -566,28 +595,29 @@
         if (speed < 0) {
             throw new IllegalArgumentException("Speed cannot be negative");
         }
+        Rect bounds = getVisibleBoundsForGestures();
         Log.v(TAG, String.format("pinchOpen(bounds=%s,percent=%f,speed=%d)",
-                getVisibleBoundsForGestures(), percent, speed));
+                bounds, percent, speed));
         mGestureController.performGesture(
-                Gestures.pinchOpen(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+                Gestures.pinchOpen(bounds, percent, speed, getDisplayId()));
     }
 
     /**
      * Performs a swipe gesture on this object.
      *
      * @param direction The direction in which to swipe.
-     * @param percent The length of the swipe as a percentage of this object's size.
+     * @param percent   The length of the swipe as a percentage of this object's size.
      */
     public void swipe(@NonNull Direction direction, float percent) {
-        swipe(direction, percent, (int)(DEFAULT_SWIPE_SPEED * mDisplayDensity));
+        swipe(direction, percent, (int) (DEFAULT_SWIPE_SPEED * mDisplayDensity));
     }
 
     /**
      * Performs a swipe gesture on this object.
      *
      * @param direction The direction in which to swipe.
-     * @param percent The length of the swipe as a percentage of this object's size.
-     * @param speed The speed at which to perform this gesture in pixels per second.
+     * @param percent   The length of the swipe as a percentage of this object's size.
+     * @param speed     The speed at which to perform this gesture in pixels per second.
      */
     public void swipe(@NonNull Direction direction, float percent, int speed) {
         if (percent < 0.0f || percent > 1.0f) {
@@ -607,20 +637,20 @@
      * Performs a scroll gesture on this object.
      *
      * @param direction The direction in which to scroll.
-     * @param percent The distance to scroll as a percentage of this object's visible size.
-     * @return Whether the object can still scroll in the given direction.
+     * @param percent   The distance to scroll as a percentage of this object's visible size.
+     * @return {@code true} if the object can still scroll in the given direction.
      */
     public boolean scroll(@NonNull Direction direction, final float percent) {
-        return scroll(direction, percent, (int)(DEFAULT_SCROLL_SPEED * mDisplayDensity));
+        return scroll(direction, percent, (int) (DEFAULT_SCROLL_SPEED * mDisplayDensity));
     }
 
     /**
      * Performs a scroll gesture on this object.
      *
      * @param direction The direction in which to scroll.
-     * @param percent The distance to scroll as a percentage of this object's visible size.
-     * @param speed The speed at which to perform this gesture in pixels per second.
-     * @return Whether the object can still scroll in the given direction.
+     * @param percent   The distance to scroll as a percentage of this object's visible size.
+     * @param speed     The speed at which to perform this gesture in pixels per second.
+     * @return {@code true} if the object can still scroll in the given direction.
      */
     public boolean scroll(@NonNull Direction direction, float percent, final int speed) {
         if (percent < 0.0f) {
@@ -638,7 +668,7 @@
         Log.v(TAG, String.format("scroll(bounds=%s,direction=%s,percent=%f,speed=%d)",
                 direction, bounds, percent, speed));
         for (; percent > 0.0f; percent -= 1.0f) {
-            float segment = percent > 1.0f ? 1.0f : percent;
+            float segment = Math.min(percent, 1.0f);
             PointerGesture swipe = Gestures.swipeRect(
                     bounds, swipeDirection, segment, speed, getDisplayId()).pause(250);
 
@@ -656,18 +686,18 @@
      * Performs a fling gesture on this object.
      *
      * @param direction The direction in which to fling.
-     * @return Whether the object can still scroll in the given direction.
+     * @return {@code true} if the object can still scroll in the given direction.
      */
     public boolean fling(@NonNull Direction direction) {
-        return fling(direction, (int)(DEFAULT_FLING_SPEED * mDisplayDensity));
+        return fling(direction, (int) (DEFAULT_FLING_SPEED * mDisplayDensity));
     }
 
     /**
      * Performs a fling gesture on this object.
      *
      * @param direction The direction in which to fling.
-     * @param speed The speed at which to perform this gesture in pixels per second.
-     * @return Whether the object can still scroll in the given direction.
+     * @param speed     The speed at which to perform this gesture in pixels per second.
+     * @return {@code true} if the object can still scroll in the given direction.
      */
     public boolean fling(@NonNull Direction direction, final int speed) {
         ViewConfiguration vc = ViewConfiguration.get(getDevice().getUiContext(getDisplayId()));
@@ -691,6 +721,7 @@
 
     /**
      * Set the text content by sending individual key codes.
+     *
      * @hide
      */
     public void legacySetText(@Nullable String text) {
@@ -721,7 +752,7 @@
         }
     }
 
-    /** Sets the text content if this object is an editable field. */
+    /** Sets this object's text content if it is an editable field. */
     public void setText(@Nullable String text) {
         AccessibilityNodeInfo node = getAccessibilityNodeInfo();
 
@@ -764,10 +795,10 @@
         }
     }
 
-
     /**
-     * Returns an up-to-date {@link AccessibilityNodeInfo} corresponding to the {@link
-     * android.view.View} that this object represents.
+     * Returns an up-to-date {@link AccessibilityNodeInfo} corresponding to this object's
+     * underlying {@link View}. Note that this method can be expensive as it wait for the device to
+     * be idle and tries multiple time to refresh the {@link AccessibilityNodeInfo}.
      */
     private AccessibilityNodeInfo getAccessibilityNodeInfo() {
         if (mCachedNode == 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 074b7aa..53c6471 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
@@ -130,7 +130,7 @@
         if (regex == null) {
             throw new IllegalArgumentException("regex cannot be null");
         }
-        return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex));
+        return buildSelector(SELECTOR_TEXT_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
     /**
@@ -255,7 +255,7 @@
         if (regex == null) {
             throw new IllegalArgumentException("regex cannot be null");
         }
-        return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex));
+        return buildSelector(SELECTOR_DESCRIPTION_REGEX, Pattern.compile(regex, Pattern.DOTALL));
     }
 
     /**
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
index 1a0d622..c680d4f 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Until.java
@@ -249,43 +249,43 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> descMatches(@NonNull String regex) {
-        return descMatches(Pattern.compile(regex));
+        return descMatches(Pattern.compile(regex, Pattern.DOTALL));
     }
 
     /**
      * Returns a condition that is satisfied when the object's content description exactly matches
-     * the given string.
+     * the given string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> descEquals(@NonNull String contentDescription) {
-        return descMatches(Pattern.compile(Pattern.quote(contentDescription)));
+        return descMatches(Pattern.quote(contentDescription));
     }
 
     /**
      * Returns a condition that is satisfied when the object's content description contains the
-     * given string.
+     * given string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> descContains(@NonNull String substring) {
-        return descMatches(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
+        return descMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
     }
 
     /**
      * Returns a condition that is satisfied when the object's content description starts with the
-     * given string.
+     * given string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> descStartsWith(@NonNull String substring) {
-        return descMatches(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
+        return descMatches(String.format("^%s.*$", Pattern.quote(substring)));
     }
 
     /**
      * Returns a condition that is satisfied when the object's content description ends with the
-     * given string.
+     * given string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> descEndsWith(@NonNull String substring) {
-        return descMatches(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
+        return descMatches(String.format("^.*%s$", Pattern.quote(substring)));
     }
 
     /**
@@ -307,7 +307,7 @@
      */
     @NonNull
     public static UiObject2Condition<Boolean> textMatches(@NonNull String regex) {
-        return textMatches(Pattern.compile(regex));
+        return textMatches(Pattern.compile(regex, Pattern.DOTALL));
     }
 
     /**
@@ -326,37 +326,38 @@
 
     /**
      * Returns a condition that is satisfied when the object's text value exactly matches the given
-     * string.
+     * string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> textEquals(@NonNull String text) {
-        return textMatches(Pattern.compile(Pattern.quote(text)));
+        return textMatches(Pattern.quote(text));
     }
 
     /**
-     * Returns a condition that is satisfied when the object's text value contains the given string.
+     * Returns a condition that is satisfied when the object's text value contains the given string
+     * (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> textContains(@NonNull String substring) {
-        return textMatches(Pattern.compile(String.format("^.*%s.*$", Pattern.quote(substring))));
+        return textMatches(String.format("^.*%s.*$", Pattern.quote(substring)));
     }
 
     /**
      * Returns a condition that is satisfied when the object's text value starts with the given
-     * string.
+     * string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> textStartsWith(@NonNull String substring) {
-        return textMatches(Pattern.compile(String.format("^%s.*$", Pattern.quote(substring))));
+        return textMatches(String.format("^%s.*$", Pattern.quote(substring)));
     }
 
     /**
      * Returns a condition that is satisfied when the object's text value ends with the given
-     * string.
+     * string (case-sensitive).
      */
     @NonNull
     public static UiObject2Condition<Boolean> textEndsWith(@NonNull String substring) {
-        return textMatches(Pattern.compile(String.format("^.*%s$", Pattern.quote(substring))));
+        return textMatches(String.format("^.*%s$", Pattern.quote(substring)));
     }
 
 
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index 2f41557..716a32a 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -24,31 +24,74 @@
 }
 
 // Disable multi-platform.
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project, /* isMultiplatformEnabled= */false)
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
 
 dependencies {
+    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
 
-    api("androidx.compose.foundation:foundation:1.3.0")
-    api("androidx.compose.ui:ui:1.3.0")
-    api("androidx.compose.ui:ui-text:1.3.0")
-    api("androidx.compose.runtime:runtime:1.3.0")
+        implementation(libs.kotlinStdlib)
+        implementation(project(":compose:foundation:foundation-layout"))
+        implementation(project(":compose:ui:ui-util"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
-    implementation(libs.kotlinStdlib)
-    implementation("androidx.compose.foundation:foundation-layout:1.3.0")
-    implementation("androidx.compose.ui:ui-util:1.3.0")
-    implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+        testImplementation(libs.testRules)
+        testImplementation(libs.testRunner)
+        testImplementation(libs.junit)
 
-    testImplementation(libs.testRules)
-    testImplementation(libs.testRunner)
-    testImplementation(libs.junit)
+        androidTestImplementation(project(":compose:ui:ui-test"))
+        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
+        androidTestImplementation(libs.testRunner)
+        androidTestImplementation(libs.kotlinTest)
 
-    androidTestImplementation(project(":compose:ui:ui-test"))
-    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(project(":compose:test-utils"))
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.kotlinTest)
+        samples(project(":wear:compose:compose-foundation-samples"))
+    }
+}
 
-    samples(project(":wear:compose:compose-foundation-samples"))
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+    androidXComposeMultiplatform {
+        android()
+        desktop()
+    }
+
+    kotlin {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                api(project(":compose:foundation:foundation"))
+                api(project(":compose:ui:ui"))
+                api(project(":compose:ui:ui-text"))
+                api(project(":compose:runtime:runtime"))
+
+                implementation(libs.kotlinStdlib)
+                implementation(project(":compose:foundation:foundation-layout"))
+                implementation(project(":compose:ui:ui-util"))
+            }
+            jvmMain.dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+
+            commonTest.dependencies {
+                implementation(kotlin("test-junit"))
+            }
+            androidAndroidTest.dependencies {
+                implementation(project(":compose:ui:ui-test"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
+                implementation(libs.testRunner)
+            }
+        }
+    }
+    dependencies {
+        samples(project(":wear:compose:compose-foundation-samples"))
+    }
 }
 
 android {
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 2cfc587..d19e7c5 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -25,36 +25,89 @@
 }
 
 // Disable multi-platform.
-AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project, /* isMultiplatformEnabled= */false)
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
 
 dependencies {
+    if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+        api(project(":compose:foundation:foundation"))
+        api(project(":compose:ui:ui"))
+        api(project(":compose:ui:ui-text"))
+        api(project(":compose:runtime:runtime"))
 
-    api("androidx.compose.foundation:foundation:1.3.0")
-    api("androidx.compose.ui:ui:1.3.0")
-    api("androidx.compose.ui:ui-text:1.3.0")
-    api("androidx.compose.runtime:runtime:1.3.0")
+        implementation(libs.kotlinStdlib)
+        implementation(project(":compose:animation:animation"))
+        implementation(project(":compose:material:material"))
+        implementation(project(":compose:material:material-ripple"))
+        implementation(project(":compose:ui:ui-util"))
+        implementation(project(":wear:compose:compose-foundation"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
-    implementation(libs.kotlinStdlib)
-    implementation("androidx.compose.animation:animation:1.3.0")
-    implementation("androidx.compose.material:material:1.3.0")
-    implementation("androidx.compose.material:material-ripple:1.3.0")
-    implementation("androidx.compose.ui:ui-util:1.3.0")
-    implementation(project(":wear:compose:compose-foundation"))
-    implementation("androidx.profileinstaller:profileinstaller:1.2.0")
+        androidTestImplementation(project(":compose:ui:ui-test"))
+        androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+        androidTestImplementation(project(":compose:test-utils"))
 
-    androidTestImplementation(project(":compose:ui:ui-test"))
-    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
-    androidTestImplementation(project(":compose:test-utils"))
+        androidTestImplementation(project(":test:screenshot:screenshot"))
+        androidTestImplementation(libs.testRunner)
+        androidTestImplementation(libs.truth)
 
-    androidTestImplementation(project(":test:screenshot:screenshot"))
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.truth)
+        testImplementation(libs.testRules)
+        testImplementation(libs.testRunner)
+        testImplementation(libs.junit)
 
-    testImplementation(libs.testRules)
-    testImplementation(libs.testRunner)
-    testImplementation(libs.junit)
+        samples(project(":wear:compose:compose-material-samples"))
+    }
+}
 
-    samples(project(":wear:compose:compose-material-samples"))
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+    androidXComposeMultiplatform {
+        android()
+        desktop()
+    }
+
+    kotlin {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                implementation(libs.kotlinStdlibCommon)
+
+                api(project(":compose:foundation:foundation"))
+                api(project(":compose:ui:ui"))
+                api(project(":compose:ui:ui-text"))
+                api(project(":compose:runtime:runtime"))
+                api("androidx.annotation:annotation:1.1.0")
+
+                implementation(project(":compose:animation:animation"))
+                implementation(project(":compose:material:material"))
+                implementation(project(":compose:material:material-ripple"))
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":wear:compose:compose-foundation"))
+            }
+            jvmMain.dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+
+            commonTest.dependencies {
+                implementation(kotlin("test-junit"))
+            }
+            androidAndroidTest.dependencies {
+                implementation(libs.testExtJunit)
+                implementation(libs.testRules)
+                implementation(libs.testRunner)
+                implementation(libs.truth)
+                implementation(project(":compose:ui:ui-util"))
+                implementation(project(":compose:ui:ui-test"))
+                implementation(project(":compose:ui:ui-test-junit4"))
+                implementation(project(":compose:test-utils"))
+                implementation(project(":test:screenshot:screenshot"))
+            }
+        }
+    }
+    dependencies {
+        samples(project(":wear:compose:compose-material-samples"))
+    }
 }
 
 android {
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
new file mode 100644
index 0000000..b43f0d2
--- /dev/null
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/ScrollAwayTest.kt
@@ -0,0 +1,213 @@
+/*
+ * 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.compose.material
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.WithTouchSlop
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertIsNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeUp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+public class ScrollAwayTest {
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun hidesTimeTextWithScalingLazyColumn() {
+        lateinit var scrollState: ScalingLazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                rememberScalingLazyListState(
+                    initialCenterItemIndex = 1,
+                    initialCenterItemScrollOffset = 0
+                )
+            ScalingLazyColumnTest(itemIndex = 1, offset = 0.dp, scrollState)
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsNotDisplayed()
+    }
+
+    @Test
+    fun showsTimeTextWithScalingLazyColumnIfItemIndexInvalid() {
+        val scrollAwayItemIndex = 10
+        lateinit var scrollState: ScalingLazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                rememberScalingLazyListState(
+                    initialCenterItemIndex = 1,
+                    initialCenterItemScrollOffset = 0
+                )
+            ScalingLazyColumnTest(itemIndex = scrollAwayItemIndex, offset = 0.dp, scrollState)
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        // b/256166359 - itemIndex > number of items in the list.
+        // ScrollAway should default to always showing TimeText
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsDisplayed()
+    }
+
+    @Composable
+    private fun ScalingLazyColumnTest(
+        itemIndex: Int,
+        offset: Dp,
+        scrollState: ScalingLazyListState
+    ) {
+        WithTouchSlop(0f) {
+            Scaffold(
+                modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
+                timeText = {
+                    TimeText(
+                        modifier = Modifier
+                            .scrollAway(
+                                scrollState = scrollState,
+                                itemIndex = itemIndex,
+                                offset = offset,
+                            )
+                            .testTag(TIME_TEXT_TAG)
+                    )
+                },
+            ) {
+                ScalingLazyColumn(
+                    contentPadding = PaddingValues(10.dp),
+                    state = scrollState,
+                    autoCentering = AutoCenteringParams(itemIndex = 1, itemOffset = 0),
+                    modifier = Modifier.testTag(SCROLL_TAG)
+                ) {
+                    item {
+                        ListHeader { Text("Chips") }
+                    }
+
+                    items(5) { i ->
+                        ChipTest(Modifier.fillParentMaxHeight(0.5f), i)
+                    }
+                }
+            }
+        }
+    }
+
+    @Test
+    fun hidesTimeTextWithLazyColumn() {
+        lateinit var scrollState: LazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                rememberLazyListState(
+                    initialFirstVisibleItemIndex = 1,
+                )
+
+            LazyColumnTest(itemIndex = 1, offset = 0.dp, scrollState)
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsNotDisplayed()
+    }
+
+    @Test
+    fun showsTimeTextWithLazyColumnIfItemIndexInvalid() {
+        val scrollAwayItemIndex = 10
+        lateinit var scrollState: LazyListState
+        rule.setContentWithTheme {
+            scrollState =
+                rememberLazyListState(
+                    initialFirstVisibleItemIndex = 1,
+                )
+            LazyColumnTest(
+                itemIndex = scrollAwayItemIndex, offset = 0.dp, scrollState)
+        }
+
+        rule.onNodeWithTag(SCROLL_TAG).performTouchInput { swipeUp() }
+
+        // b/256166359 - itemIndex > number of items in the list.
+        // ScrollAway should default to always showing TimeText
+        rule.onNodeWithTag(TIME_TEXT_TAG).assertIsDisplayed()
+    }
+
+    @Composable
+    private fun LazyColumnTest(
+        itemIndex: Int,
+        offset: Dp,
+        scrollState: LazyListState
+    ) {
+        WithTouchSlop(0f) {
+            Scaffold(
+                modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.background),
+                timeText = {
+                    TimeText(
+                        modifier = Modifier
+                            .scrollAway(
+                                scrollState = scrollState,
+                                itemIndex = itemIndex,
+                                offset = offset,
+                            )
+                            .testTag(TIME_TEXT_TAG)
+                    )
+                },
+            ) {
+                LazyColumn(
+                    state = scrollState,
+                    modifier = Modifier.testTag(SCROLL_TAG)
+                ) {
+                    item {
+                        ListHeader { Text("Chips") }
+                    }
+
+                    items(5) { i ->
+                        ChipTest(Modifier.fillParentMaxHeight(0.5f), i)
+                    }
+                }
+            }
+        }
+    }
+
+    @Composable
+    private fun ChipTest(modifier: Modifier, i: Int) {
+        Chip(
+            modifier = modifier,
+            onClick = { },
+            colors = ChipDefaults.primaryChipColors(),
+            border = ChipDefaults.chipBorder()
+        ) {
+            Text(text = "Chip $i")
+        }
+    }
+}
+
+private const val SCROLL_TAG = "ScrollTag"
+private const val TIME_TEXT_TAG = "TimeTextTag"
\ No newline at end of file
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
index 87deb2d..3574a40 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/ScrollAway.kt
@@ -61,11 +61,12 @@
     scrollState: LazyListState,
     itemIndex: Int = 0,
     offset: Dp = 0.dp,
-): Modifier = scrollAway {
-    scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
-        -it.offset - offset.toPx()
+): Modifier =
+    scrollAway(itemIndex < scrollState.layoutInfo.totalItemsCount) {
+        scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+            -it.offset - offset.toPx()
+        }
     }
-}
 
 /**
  * Scroll an item vertically in/out of view based on a [ScalingLazyListState].
@@ -81,21 +82,29 @@
     scrollState: ScalingLazyListState,
     itemIndex: Int = 1,
     offset: Dp = 0.dp,
-): Modifier = scrollAway {
-    scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
-        -it.offset - offset.toPx()
+): Modifier =
+    scrollAway(itemIndex < scrollState.layoutInfo.totalItemsCount) {
+        scrollState.layoutInfo.visibleItemsInfo.find { it.index == itemIndex }?.let {
+            -it.offset - offset.toPx()
+        }
     }
-}
 
-private fun Modifier.scrollAway(yPxFn: Density.() -> Float?): Modifier =
+private fun Modifier.scrollAway(valid: Boolean = true, yPxFn: Density.() -> Float?): Modifier =
     this.then(
         object : LayoutModifier {
             override fun MeasureScope.measure(
                 measurable: Measurable,
                 constraints: Constraints
             ): MeasureResult {
+                val placeable = measurable.measure(constraints)
                 val yPx = yPxFn()
-                if (yPx == null) {
+                if (!valid) {
+                    // For invalid inputs, don't scroll the content away - just show it.
+                    return layout(placeable.width, placeable.height) {
+                        placeable.placeRelative(0, 0)
+                    }
+                } else if (yPx == null) {
+                    // For valid inputs, but no y offset provided, hide the content.
                     return object : MeasureResult {
                         override val width = 0
                         override val height = 0
@@ -103,7 +112,7 @@
                         override fun placeChildren() {}
                     }
                 } else {
-                    val placeable = measurable.measure(constraints)
+                    // Valid input and a y offset is provided - apply fade, scale and offset.
                     return layout(placeable.width, placeable.height) {
                         val progress: Float = (yPx / maxScrollOut.toPx()).coerceIn(0f, 1f)
                         val motionFraction: Float = lerp(minMotionOut, maxMotionOut, progress)
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index b10ef6e..2f3d8cc 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -25,8 +25,8 @@
 
 dependencies {
 
-    api("androidx.compose.ui:ui:1.3.0")
-    api("androidx.compose.runtime:runtime:1.3.0")
+    api(project(":compose:ui:ui"))
+    api(project(":compose:runtime:runtime"))
     api("androidx.navigation:navigation-runtime:2.4.0")
     api(project(":wear:compose:compose-material"))
 
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index fe1fd54..c12dbc7 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -139,6 +139,9 @@
                 ComposableDemo("SLC Cards offset>0") {
                     ScrollAwayScalingLazyColumnCardDemoMismatch()
                 },
+                ComposableDemo("Out of range") {
+                    ScrollAwayScalingLazyColumnCardDemoOutOfRange()
+                },
                 ComposableDemo("SLC Chips") {
                     ScrollAwayScalingLazyColumnChipDemo()
                 },
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
index 36c223f..9744cae 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/ScrollAwayDemos.kt
@@ -100,6 +100,15 @@
 }
 
 @Composable
+fun ScrollAwayScalingLazyColumnCardDemoOutOfRange() {
+    ScalingLazyColumnCardDemo(
+        itemIndex = 100,
+        offset = 75.dp,
+        initialCenterItemIndex = 1,
+    )
+}
+
+@Composable
 fun ScrollAwayScalingLazyColumnChipDemo() {
     ScalingLazyColumnChipDemo(
         itemIndex = 1,
diff --git a/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt b/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
index 7486f72..3a57183 100644
--- a/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
+++ b/wear/watchface/watchface-client-guava/src/androidTest/java/androidx/wear/watchface/client/guava/ListenableWatchFaceMetadataClientTest.kt
@@ -17,7 +17,6 @@
 package androidx.wear.watchface.client.guava
 
 import android.annotation.SuppressLint
-import android.app.Service
 import android.content.ComponentName
 import android.content.Context
 import android.content.Intent
@@ -44,8 +43,8 @@
  * Test shim to allow us to connect to WatchFaceControlService from
  * [ListenableWatchFaceMetadataClientTest] and to optionally override the reported API version.
  */
-public class WatchFaceControlTestService : Service() {
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+public class WatchFaceControlTestService : WatchFaceControlService() {
     private val realService = object : WatchFaceControlService() {
         @SuppressLint("NewApi")
         override fun createServiceStub(): IWatchFaceInstanceServiceStub =
@@ -58,6 +57,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
 public class ListenableWatchFaceMetadataClientTest {
     private val exampleWatchFaceComponentName = ComponentName(
         "androidx.wear.watchface.samples.test",
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 26ca147..99b7e4e 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -134,6 +134,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
 class WatchFaceControlClientTest {
     private val context = ApplicationProvider.getApplicationContext<Context>()
     private val service = runBlocking {
@@ -1561,7 +1562,6 @@
     }
 
     @Ignore // b/225230182
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     @Test
     fun interactiveAndHeadlessOpenGlWatchFaceInstances() {
         val surfaceTexture = SurfaceTexture(false)
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
index 4ac548d..41ce537 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlTestService.kt
@@ -16,7 +16,6 @@
 
 package androidx.wear.watchface.client.test
 
-import android.app.Service
 import android.content.Context
 import android.content.Intent
 import android.os.Build
@@ -31,7 +30,8 @@
  * Test shim to allow us to connect to WatchFaceControlService from
  * [WatchFaceControlClientTest] and to optionally override the reported API version.
  */
-public class WatchFaceControlTestService : Service() {
+@RequiresApi(Build.VERSION_CODES.O_MR1)
+public class WatchFaceControlTestService : WatchFaceControlService() {
     public companion object {
         /**
          * If non-null this overrides the API version reported by [IWatchFaceInstanceServiceStub].
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
index 7a59c11..7d54fd5 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceMetadataServiceTest.kt
@@ -22,7 +22,9 @@
 import android.content.pm.PackageManager
 import android.content.res.XmlResourceParser
 import android.graphics.RectF
+import android.os.Build
 import android.support.wearable.watchface.Constants
+import androidx.annotation.RequiresApi
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
@@ -47,6 +49,7 @@
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
+@RequiresApi(Build.VERSION_CODES.O_MR1)
 public class WatchFaceMetadataServiceTest {
     private val exampleWatchFaceComponentName = ComponentName(
         "androidx.wear.watchface.client.test",
diff --git a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
index 4c9237f..7c0031a 100644
--- a/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
+++ b/wear/watchface/watchface-guava/src/androidTest/java/androidx/wear/watchface/WatchFaceControlTestService.kt
@@ -16,7 +16,6 @@
 
 package androidx.wear.watchface
 
-import android.app.Service
 import android.content.Context
 import android.content.Intent
 import android.graphics.Bitmap
@@ -53,7 +52,7 @@
  * override the reported API version.
  */
 @RequiresApi(Build.VERSION_CODES.O_MR1)
-public class WatchFaceControlTestService : Service() {
+public class WatchFaceControlTestService : WatchFaceControlService() {
     public companion object {
         /**
          * If non-null this overrides the API version reported by [IWatchFaceInstanceServiceStub].
@@ -62,10 +61,8 @@
     }
 
     private val realService =
-        @RequiresApi(Build.VERSION_CODES.O_MR1)
         object : WatchFaceControlService() {
             override fun createServiceStub(): IWatchFaceInstanceServiceStub =
-                @RequiresApi(Build.VERSION_CODES.O_MR1)
                 object : IWatchFaceInstanceServiceStub(
                     this@WatchFaceControlTestService,
                     MainScope()
@@ -78,7 +75,6 @@
             }
         }
 
-    @RequiresApi(Build.VERSION_CODES.O_MR1)
     override fun onBind(intent: Intent?): IBinder? = realService.onBind(intent)
 }
 
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
index b41c16a..7c4b22e 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/control/WatchFaceControlService.kt
@@ -78,6 +78,18 @@
             }
         }
 
+    open fun createWatchFaceService(watchFaceName: ComponentName): WatchFaceService? {
+        return try {
+            val watchFaceServiceClass = Class.forName(watchFaceName.className) ?: return null
+            if (!WatchFaceService::class.java.isAssignableFrom(WatchFaceService::class.java)) {
+                return null
+            }
+            watchFaceServiceClass.getConstructor().newInstance() as WatchFaceService
+        } catch (e: ClassNotFoundException) {
+            null
+        }
+    }
+
     @VisibleForTesting
     public open fun createServiceStub(): IWatchFaceInstanceServiceStub =
         TraceEvent("WatchFaceControlService.createServiceStub").use {
@@ -109,7 +121,7 @@
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public open class IWatchFaceInstanceServiceStub(
     // We need to explicitly null this object in onDestroy to avoid a memory leak.
-    private var service: Service?,
+    private var service: WatchFaceControlService?,
     private val uiThreadCoroutineScope: CoroutineScope
 ) : IWatchFaceControlService.Stub() {
     override fun getApiVersion(): Int = IWatchFaceControlService.API_VERSION
@@ -164,42 +176,36 @@
     ) = TraceEvent("IWatchFaceInstanceServiceStub.createEngine").use {
         // Attempt to construct the class for the specified watchFaceName, failing if it either
         // doesn't exist or isn't a [WatchFaceService].
-        try {
-            val watchFaceServiceClass = Class.forName(watchFaceName.className) ?: return null
-            if (!WatchFaceService::class.java.isAssignableFrom(WatchFaceService::class.java)) {
-                null
-            } else {
-                val watchFaceService =
-                    watchFaceServiceClass.getConstructor().newInstance() as WatchFaceService
+        val watchFaceService = service?.createWatchFaceService(watchFaceName)
 
-                // Set the context and if possible the application for watchFaceService.
-                try {
-                    val method = Service::class.java.declaredMethods.find { it.name == "attach" }
-                    method!!.isAccessible = true
-                    method.invoke(
-                        watchFaceService,
-                        service!! as Context,
-                        null,
-                        watchFaceService::class.qualifiedName,
-                        null,
-                        service!!.application,
-                        null
-                    )
-                } catch (e: Exception) {
-                    Log.w(
-                        TAG,
-                        "createServiceAndHeadlessEngine can't call attach by reflection, " +
-                            "falling back to setContext",
-                        e
-                    )
-                    watchFaceService.setContext(watchFaceService)
-                }
-                watchFaceService.onCreate()
-                val engine =
-                    watchFaceService.createHeadlessEngine() as WatchFaceService.EngineWrapper
-                ServiceAndEngine(watchFaceService, engine)
+        if (watchFaceService != null) {
+            // Set the context and if possible the application for watchFaceService.
+            try {
+                val method = Service::class.java.declaredMethods.find { it.name == "attach" }
+                method!!.isAccessible = true
+                method.invoke(
+                    watchFaceService,
+                    service as Context,
+                    null,
+                    watchFaceService::class.qualifiedName,
+                    null,
+                    service!!.application,
+                    null
+                )
+            } catch (e: Exception) {
+                Log.w(
+                    TAG,
+                    "createServiceAndHeadlessEngine can't call attach by reflection, " +
+                        "falling back to setContext",
+                    e
+                )
+                watchFaceService.setContext(watchFaceService)
             }
-        } catch (e: ClassNotFoundException) {
+            watchFaceService.onCreate()
+            val engine =
+                watchFaceService.createHeadlessEngine() as WatchFaceService.EngineWrapper
+            ServiceAndEngine(watchFaceService, engine)
+        } else {
             null
         }
     }
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
index 1c4b8ce..1fdb830 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/ProcessGlobalConfigActivityTestAppTest.java
@@ -44,7 +44,8 @@
 
     @Before
     public void setUp() {
-        WebkitTestHelpers.assumeStartupFeature(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+        WebkitTestHelpers.assumeStartupFeature(
+                WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
                 ApplicationProvider.getApplicationContext());
     }
 
diff --git a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
index 7f5257d..1d64198 100644
--- a/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
+++ b/webkit/integration-tests/testapp/src/androidTest/java/com/example/androidx/webkit/WebkitTestHelpers.java
@@ -139,7 +139,7 @@
             @WebViewFeature.WebViewStartupFeature String featureName,
             Context context) {
         final String msg = "This device does not have the feature '" +  featureName + "'";
-        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(context, featureName);
         Assume.assumeTrue(msg, hasFeature);
     }
 
@@ -155,7 +155,7 @@
     public static void assumeStartupFeatureNotAvailable(
             @WebViewFeature.WebViewStartupFeature String featureName, Context context) {
         final String msg = "This device has the feature '" +  featureName + "'";
-        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(featureName, context);
+        final boolean hasFeature = WebViewFeature.isStartupFeatureSupported(context, featureName);
         Assume.assumeFalse(msg, hasFeature);
     }
 
diff --git a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
index c74f667..91ae917 100644
--- a/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
+++ b/webkit/integration-tests/testapp/src/main/java/com/example/androidx/webkit/ProcessGlobalConfigActivity.java
@@ -38,13 +38,13 @@
         setTitle(R.string.process_global_config_activity_title);
         WebkitHelpers.appendWebViewVersionToTitle(this);
 
-        if (!WebViewFeature.isStartupFeatureSupported(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
-                this)) {
+        if (!WebViewFeature.isStartupFeatureSupported(this,
+                WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX)) {
             WebkitHelpers.showMessageInActivity(this, R.string.webkit_api_not_available);
             return;
         }
         ProcessGlobalConfig.createInstance()
-                .setDataDirectorySuffix("per_process_webview_data_0", this)
+                .setDataDirectorySuffix(this, "per_process_webview_data_0")
                 .apply();
         setContentView(R.layout.activity_process_global_config);
         WebView wv = findViewById(R.id.process_global_config_webview);
diff --git a/webkit/webkit/api/current.txt b/webkit/webkit/api/current.txt
index 772b183..f354f29 100644
--- a/webkit/webkit/api/current.txt
+++ b/webkit/webkit/api/current.txt
@@ -9,6 +9,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
 
+  public class ProcessGlobalConfig {
+    method public void apply();
+    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
+  }
+
   public final class ProxyConfig {
     method public java.util.List<java.lang.String!> getBypassRules();
     method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
@@ -229,6 +235,7 @@
 
   public class WebViewFeature {
     method public static boolean isFeatureSupported(String);
+    method public static boolean isStartupFeatureSupported(android.content.Context, String);
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
@@ -262,6 +269,7 @@
     field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
     field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
     field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
+    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
     field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
     field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
     field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
diff --git a/webkit/webkit/api/public_plus_experimental_current.txt b/webkit/webkit/api/public_plus_experimental_current.txt
index 772b183..f354f29 100644
--- a/webkit/webkit/api/public_plus_experimental_current.txt
+++ b/webkit/webkit/api/public_plus_experimental_current.txt
@@ -9,6 +9,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
 
+  public class ProcessGlobalConfig {
+    method public void apply();
+    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
+  }
+
   public final class ProxyConfig {
     method public java.util.List<java.lang.String!> getBypassRules();
     method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
@@ -229,6 +235,7 @@
 
   public class WebViewFeature {
     method public static boolean isFeatureSupported(String);
+    method public static boolean isStartupFeatureSupported(android.content.Context, String);
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
@@ -262,6 +269,7 @@
     field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
     field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
     field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
+    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
     field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
     field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
     field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
diff --git a/webkit/webkit/api/restricted_current.txt b/webkit/webkit/api/restricted_current.txt
index 772b183..f354f29 100644
--- a/webkit/webkit/api/restricted_current.txt
+++ b/webkit/webkit/api/restricted_current.txt
@@ -9,6 +9,12 @@
     method @RequiresFeature(name=androidx.webkit.WebViewFeature.WEB_MESSAGE_LISTENER, enforcement="androidx.webkit.WebViewFeature#isFeatureSupported") public abstract void postMessage(String);
   }
 
+  public class ProcessGlobalConfig {
+    method public void apply();
+    method public static androidx.webkit.ProcessGlobalConfig createInstance();
+    method @RequiresFeature(name=androidx.webkit.WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX, enforcement="androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)") public androidx.webkit.ProcessGlobalConfig setDataDirectorySuffix(android.content.Context, String);
+  }
+
   public final class ProxyConfig {
     method public java.util.List<java.lang.String!> getBypassRules();
     method public java.util.List<androidx.webkit.ProxyConfig.ProxyRule!> getProxyRules();
@@ -229,6 +235,7 @@
 
   public class WebViewFeature {
     method public static boolean isFeatureSupported(String);
+    method public static boolean isStartupFeatureSupported(android.content.Context, String);
     field public static final String ALGORITHMIC_DARKENING = "ALGORITHMIC_DARKENING";
     field public static final String CREATE_WEB_MESSAGE_CHANNEL = "CREATE_WEB_MESSAGE_CHANNEL";
     field public static final String DISABLED_ACTION_MODE_MENU_ITEMS = "DISABLED_ACTION_MODE_MENU_ITEMS";
@@ -262,6 +269,7 @@
     field public static final String SERVICE_WORKER_FILE_ACCESS = "SERVICE_WORKER_FILE_ACCESS";
     field public static final String SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST = "SERVICE_WORKER_SHOULD_INTERCEPT_REQUEST";
     field public static final String SHOULD_OVERRIDE_WITH_REDIRECTS = "SHOULD_OVERRIDE_WITH_REDIRECTS";
+    field public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX = "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
     field public static final String START_SAFE_BROWSING = "START_SAFE_BROWSING";
     field public static final String TRACING_CONTROLLER_BASIC_USAGE = "TRACING_CONTROLLER_BASIC_USAGE";
     field public static final String VISUAL_STATE_CALLBACK = "VISUAL_STATE_CALLBACK";
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index f2a5a06..d16d15c 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -55,24 +55,28 @@
     /**
      * Test web content is always light regardless the algorithmic darkening is allowed or not.
      */
-    @SdkSuppress(maxSdkVersion = 32) // b/254572377
     @Test
     public void testSimplifiedDarkMode_rendersLight() throws Throwable {
         WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
         WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
         setWebViewSize();
+        float expected_luminance = 0.5f;
+        // Theme.AppCompat.Light has a darker background in api 33 than previous.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            expected_luminance = 0.4f;
+        }
         // Loading about:blank which doesn't support dark style result in a light background.
         getWebViewOnUiThread().loadUrlAndWaitForCompletion("about:blank");
         assertTrue("Bitmap colour should be light",
-                ColorUtils.calculateLuminance(getWebPageColor()) > 0.5f);
+                ColorUtils.calculateLuminance(getWebPageColor()) > expected_luminance);
         assertFalse("WebView should always prefers light color scheme in light theme app",
                 prefersDarkTheme());
-        // Allowing algorithmic darkening in dark theme app should result in a dark background.
+        // Allowing algorithmic darkening in light theme app should not take any effect.
         WebSettingsCompat.setAlgorithmicDarkeningAllowed(
                 getSettingsOnUiThread(), true);
         getWebViewOnUiThread().loadUrlAndWaitForCompletion("about:blank");
-        assertTrue("Bitmap colour should be dark",
-                ColorUtils.calculateLuminance(getWebPageColor()) > 0.5f);
+        assertTrue("Bitmap colour should be light",
+                ColorUtils.calculateLuminance(getWebPageColor()) > expected_luminance);
         assertFalse("WebView should always prefers light color scheme in light theme app",
                 prefersDarkTheme());
     }
@@ -80,17 +84,21 @@
     /**
      * Test web content is always light (if supported) on the light theme app.
      */
-    @SdkSuppress(maxSdkVersion = 32) // b/254572377
     @Test
     public void testSimplifiedDarkMode_pageSupportDarkTheme() {
         WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
         WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
         setWebViewSize();
 
+        float expected_luminance = 0.5f;
+        // Theme.AppCompat.Light has a darker background in api 33 than previous.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            expected_luminance = 0.4f;
+        }
         // Loading about:blank which doesn't support dark style result in a light background.
         getWebViewOnUiThread().loadUrlAndWaitForCompletion("about:blank");
         assertTrue("Bitmap colour should be light",
-                ColorUtils.calculateLuminance(getWebPageColor()) > 0.5f);
+                ColorUtils.calculateLuminance(getWebPageColor()) > expected_luminance);
         assertFalse("WebView should always prefers light color scheme in light theme app",
                 prefersDarkTheme());
 
@@ -99,7 +107,7 @@
         getWebViewOnUiThread().loadDataAndWaitForCompletion(mDarkThemeSupport, "text/html",
                 "base64");
         assertTrue("Bitmap colour should be light",
-                ColorUtils.calculateLuminance(getWebPageColor()) > 0.5f);
+                ColorUtils.calculateLuminance(getWebPageColor()) > expected_luminance);
         assertFalse("WebView should always prefers light color scheme in light theme app",
                 prefersDarkTheme());
     }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
index d6aab67..bdcfa0f 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/ProcessGlobalConfig.java
@@ -20,7 +20,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresFeature;
-import androidx.annotation.RestrictTo;
 import androidx.webkit.internal.StartupApiFeature;
 import androidx.webkit.internal.WebViewFeatureInternal;
 
@@ -58,10 +57,7 @@
  * <p>
  * The configuration should be set up as early as possible during application startup, to ensure
  * that it happens before any other thread can call a method that loads WebView.
- *
- * @hide
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
 public class ProcessGlobalConfig {
     private static final AtomicReference<HashMap<String, Object>> sProcessGlobalConfig =
             new AtomicReference<HashMap<String, Object>>();
@@ -84,8 +80,14 @@
     @NonNull
     public static ProcessGlobalConfig createInstance() {
         if (!sInstanceCreated.compareAndSet(false, true)) {
-            throw new IllegalStateException("ProcessGlobalConfig#createInstance() was already "
-                    + "called before");
+            throw new IllegalStateException("ProcessGlobalConfig#createInstance was "
+                    + "called more than once, which is an illegal operation. The configuration "
+                    + "settings provided by ProcessGlobalConfig take effect only once, when "
+                    + "WebView is first loaded into the current process. Every process should "
+                    + "only ever create a single instance of ProcessGlobalConfig and apply it "
+                    + "once, before any calls to android.webkit APIs, such as during early app "
+                    + "startup."
+            );
         }
         return new ProcessGlobalConfig();
     }
@@ -112,16 +114,25 @@
         //  ProcessGlobalConfig, revisit this.
         HashMap<String, Object> configMap = new HashMap<String, Object>();
         if (mApplyCalled) {
-            throw new IllegalStateException("ProcessGlobalConfig#apply() was already called "
-                    + "before");
+            throw new IllegalStateException("ProcessGlobalConfig#apply was "
+                    + "called more than once, which is an illegal operation. The configuration "
+                    + "settings provided by ProcessGlobalConfig take effect only once, when "
+                    + "WebView is first loaded into the current process. Every process should "
+                    + "only ever create a single instance of ProcessGlobalConfig and apply it "
+                    + "once, before any calls to android.webkit APIs, such as during early app "
+                    + "startup."
+            );
         }
         mApplyCalled = true;
         if (webViewCurrentlyLoaded()) {
-            throw new IllegalStateException(
-                    "WebView has already been initialized in the current process");
+            throw new IllegalStateException("WebView has already been loaded in the current "
+                    + "process, so any attempt to apply the settings in ProcessGlobalConfig will "
+                    + "have no effect. ProcessGlobalConfig#apply needs to be called before any "
+                    + "calls to android.webkit APIs, such as during early app startup.");
         }
 
-        final StartupApiFeature.P feature = WebViewFeatureInternal.SET_DATA_DIRECTORY_SUFFIX;
+        final StartupApiFeature.P feature =
+                WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
         if (feature.isSupportedByFramework()) {
             androidx.webkit.internal.ApiHelperForP.setDataDirectorySuffix(mDataDirectorySuffix);
         } else {
@@ -158,24 +169,26 @@
      * This is a compatibility method for
      * {@link android.webkit.WebView#setDataDirectorySuffix(String)}
      *
+     * @param context a Context to access application assets This value cannot be null.
      * @param suffix The directory name suffix to be used for the current
      *               process. Must not contain a path separator and should not be empty.
      * @throws IllegalStateException if WebView has already been initialized
      *                               in the current process or if this method was called before
      * @throws IllegalArgumentException if the suffix contains a path separator or is empty.
      */
-    @RequiresFeature(name = WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
+    @RequiresFeature(name = WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
             enforcement =
                     "androidx.webkit.WebViewFeature#isConfigFeatureSupported(String, Context)")
     @NonNull
-    public ProcessGlobalConfig setDataDirectorySuffix(@NonNull String suffix,
-            @NonNull Context context) {
+    public ProcessGlobalConfig setDataDirectorySuffix(@NonNull Context context,
+            @NonNull String suffix) {
         if (mDataDirectorySuffix != null) {
             throw new IllegalStateException(
                     "ProcessGlobalConfig#setDataDirectorySuffix(String) was already "
                             + "called");
         }
-        final StartupApiFeature.P feature = WebViewFeatureInternal.SET_DATA_DIRECTORY_SUFFIX;
+        final StartupApiFeature.P feature =
+                WebViewFeatureInternal.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX;
         if (!feature.isSupported(context)) {
             throw WebViewFeatureInternal.getUnsupportedOperationException();
         }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index b089191..e7eb361 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -113,7 +113,7 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY)
     @StringDef(value = {
-            SET_DATA_DIRECTORY_SUFFIX,
+            STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
     })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -524,13 +524,12 @@
     public static final String GET_COOKIE_INFO = "GET_COOKIE_INFO";
 
     /**
-     * Feature for {@link #isFeatureSupported(String)}.
+     * Feature for {@link #isStartupFeatureSupported(Context, String)}.
      * This feature covers
      * {@link androidx.webkit.ProcessGlobalConfig#setDataDirectorySuffix(String)}.
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    public static final String SET_DATA_DIRECTORY_SUFFIX = "SET_DATA_DIRECTORY_SUFFIX";
+    public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX =
+            "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
 
     /**
      * Feature for {@link #isFeatureSupported(String)}.
@@ -585,14 +584,12 @@
      * the methods requiring the desired feature. Furthermore, if this method returns {@code false}
      * for a particular feature, any callback guarded by that feature will not be invoked.
      *
+     * @param context a Context to access application assets This value cannot be null.
      * @param startupFeature the startup feature to be checked
      * @return whether the feature is supported given the current platform SDK and webview version
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
-    public static boolean isStartupFeatureSupported(
-            @NonNull @WebViewStartupFeature String startupFeature,
-            @NonNull Context context) {
+    public static boolean isStartupFeatureSupported(@NonNull Context context,
+            @NonNull @WebViewStartupFeature String startupFeature) {
         return WebViewFeatureInternal.isStartupFeatureSupported(startupFeature, context);
     }
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
index 6007a34..51e8e84 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/StartupFeatures.java
@@ -27,5 +27,6 @@
     }
 
     // ProcessGlobalConfig#setDataDirectorySuffix(String)
-    public static final String SET_DATA_DIRECTORY_SUFFIX =  "SET_DATA_DIRECTORY_SUFFIX:DEV";
+    public static final String STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX =
+            "STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX";
 }
diff --git a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index 78de512..080195d 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -383,9 +383,9 @@
      * This feature covers
      * {@link androidx.webkit.ProcessGlobalConfig#setDataDirectorySuffix(String)}.
      */
-    public static final StartupApiFeature.P SET_DATA_DIRECTORY_SUFFIX =
-            new StartupApiFeature.P(WebViewFeature.SET_DATA_DIRECTORY_SUFFIX,
-                    StartupFeatures.SET_DATA_DIRECTORY_SUFFIX);
+    public static final StartupApiFeature.P STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX =
+            new StartupApiFeature.P(WebViewFeature.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX,
+                    StartupFeatures.STARTUP_FEATURE_SET_DATA_DIRECTORY_SUFFIX);
 
     /**
      * This feature covers
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index 199e08d..624b2df 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -2,7 +2,6 @@
 package androidx.window.core.layout {
 
   public final class WindowHeightSizeClass {
-    method public static androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
     field public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
     field public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
     field public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
@@ -10,7 +9,6 @@
   }
 
   public static final class WindowHeightSizeClass.Companion {
-    method public androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
   }
 
   public final class WindowSizeClass {
diff --git a/window/window-core/api/public_plus_experimental_current.txt b/window/window-core/api/public_plus_experimental_current.txt
index 199e08d..624b2df 100644
--- a/window/window-core/api/public_plus_experimental_current.txt
+++ b/window/window-core/api/public_plus_experimental_current.txt
@@ -2,7 +2,6 @@
 package androidx.window.core.layout {
 
   public final class WindowHeightSizeClass {
-    method public static androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
     field public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
     field public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
     field public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
@@ -10,7 +9,6 @@
   }
 
   public static final class WindowHeightSizeClass.Companion {
-    method public androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
   }
 
   public final class WindowSizeClass {
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index 199e08d..624b2df 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -2,7 +2,6 @@
 package androidx.window.core.layout {
 
   public final class WindowHeightSizeClass {
-    method public static androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
     field public static final androidx.window.core.layout.WindowHeightSizeClass COMPACT;
     field public static final androidx.window.core.layout.WindowHeightSizeClass.Companion Companion;
     field public static final androidx.window.core.layout.WindowHeightSizeClass EXPANDED;
@@ -10,7 +9,6 @@
   }
 
   public static final class WindowHeightSizeClass.Companion {
-    method public androidx.window.core.layout.WindowHeightSizeClass compute(float dpHeight);
   }
 
   public final class WindowSizeClass {
diff --git a/window/window-core/src/main/java/androidx/window/core/layout/WindowHeightSizeClass.kt b/window/window-core/src/main/java/androidx/window/core/layout/WindowHeightSizeClass.kt
index bc94c99..849def5 100644
--- a/window/window-core/src/main/java/androidx/window/core/layout/WindowHeightSizeClass.kt
+++ b/window/window-core/src/main/java/androidx/window/core/layout/WindowHeightSizeClass.kt
@@ -21,7 +21,7 @@
 import androidx.window.core.layout.WindowHeightSizeClass.Companion.MEDIUM
 
 /**
- * A class to represent the width size buckets for a viewport. The possible values are [COMPACT],
+ * A class to represent the height size buckets for a viewport. The possible values are [COMPACT],
  * [MEDIUM], and [EXPANDED]. [WindowHeightSizeClass] should not be used as a proxy for the device
  * type. It is possible to have resizeable windows in different device types.
  * The viewport might change from a [COMPACT] all the way to an [EXPANDED] size class.
@@ -83,8 +83,8 @@
          * @throws IllegalArgumentException if the height is negative
          */
         @JvmStatic
-        fun compute(dpHeight: Float): WindowHeightSizeClass {
-            require(dpHeight > 0) { "Width must be positive, received $dpHeight" }
+        internal fun compute(dpHeight: Float): WindowHeightSizeClass {
+            require(dpHeight > 0) { "Height must be positive, received $dpHeight" }
             return when {
                 dpHeight < 480 -> COMPACT
                 dpHeight < 900 -> MEDIUM
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
index 05ee9a1..29f6ce3 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitActivityBase.java
@@ -198,12 +198,12 @@
     }
 
     /** Gets the placeholder rule for the given activity. */
-    SplitPlaceholderRule getPlaceholderRule(Class<? extends Activity> a) {
+    SplitPlaceholderRule getPlaceholderRule(@NonNull Class<? extends Activity> a) {
         Set<EmbeddingRule> currentRules = mSplitController.getSplitRules();
         for (EmbeddingRule rule : currentRules) {
             if (rule instanceof SplitPlaceholderRule) {
                 for (ActivityFilter filter : ((SplitPlaceholderRule) rule).getFilters()) {
-                    if (filter.matchesClassName(a)) {
+                    if (filter.getComponentName().getClassName().equals(a.getName())) {
                         return (SplitPlaceholderRule) rule;
                     }
                 }
@@ -244,12 +244,9 @@
     }
 
     /** Whether the given rule is for splitting the given activity with another. */
-    private boolean isRuleFor(
-            @Nullable Class<? extends Activity> a,
-            @NonNull ActivityRule config
-    ) {
+    private boolean isRuleFor(@NonNull Class<? extends Activity> a, @NonNull ActivityRule config) {
         for (ActivityFilter filter : config.getFilters()) {
-            if (filter.matchesClassNameOrWildCard(a)) {
+            if (filter.getComponentName().getClassName().equals(a.getName())) {
                 return true;
             }
         }
diff --git a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
index e499df3..6b7c99f 100644
--- a/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
+++ b/window/window-samples/src/main/java/androidx/window/sample/embedding/SplitPipActivityBase.kt
@@ -223,7 +223,7 @@
             return false
         }
         for (filter in rule.filters) {
-            if (filter.matchesClassName(SplitPipActivityB::class.java)) {
+            if (filter.componentName.className == SplitPipActivityB::class.java.name) {
                 return true
             }
         }
diff --git a/window/window/api/current.txt b/window/window/api/current.txt
index 45ead03..27cf3e7 100644
--- a/window/window/api/current.txt
+++ b/window/window/api/current.txt
@@ -12,10 +12,12 @@
 
   public final class ActivityFilter {
     ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
+    method public android.content.ComponentName getComponentName();
+    method public String? getIntentAction();
     method public boolean matchesActivity(android.app.Activity activity);
-    method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
-    method public <T extends android.app.Activity> boolean matchesClassNameOrWildCard(Class<T>? clazz);
     method public boolean matchesIntent(android.content.Intent intent);
+    property public final android.content.ComponentName componentName;
+    property public final String? intentAction;
   }
 
   public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
@@ -32,9 +34,10 @@
   }
 
   public final class ActivityStack {
-    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
+    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activitiesInProcess, optional boolean isEmpty);
     method public operator boolean contains(android.app.Activity activity);
     method public boolean isEmpty();
+    property public final boolean isEmpty;
   }
 
   public abstract class EmbeddingRule {
diff --git a/window/window/api/public_plus_experimental_current.txt b/window/window/api/public_plus_experimental_current.txt
index 00f20da..93e20f5 100644
--- a/window/window/api/public_plus_experimental_current.txt
+++ b/window/window/api/public_plus_experimental_current.txt
@@ -19,10 +19,12 @@
 
   public final class ActivityFilter {
     ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
+    method public android.content.ComponentName getComponentName();
+    method public String? getIntentAction();
     method public boolean matchesActivity(android.app.Activity activity);
-    method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
-    method public <T extends android.app.Activity> boolean matchesClassNameOrWildCard(Class<T>? clazz);
     method public boolean matchesIntent(android.content.Intent intent);
+    property public final android.content.ComponentName componentName;
+    property public final String? intentAction;
   }
 
   public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
@@ -39,9 +41,10 @@
   }
 
   public final class ActivityStack {
-    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
+    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activitiesInProcess, optional boolean isEmpty);
     method public operator boolean contains(android.app.Activity activity);
     method public boolean isEmpty();
+    property public final boolean isEmpty;
   }
 
   public abstract class EmbeddingRule {
diff --git a/window/window/api/restricted_current.txt b/window/window/api/restricted_current.txt
index 45ead03..27cf3e7 100644
--- a/window/window/api/restricted_current.txt
+++ b/window/window/api/restricted_current.txt
@@ -12,10 +12,12 @@
 
   public final class ActivityFilter {
     ctor public ActivityFilter(android.content.ComponentName componentName, String? intentAction);
+    method public android.content.ComponentName getComponentName();
+    method public String? getIntentAction();
     method public boolean matchesActivity(android.app.Activity activity);
-    method public <T extends android.app.Activity> boolean matchesClassName(Class<T> clazz);
-    method public <T extends android.app.Activity> boolean matchesClassNameOrWildCard(Class<T>? clazz);
     method public boolean matchesIntent(android.content.Intent intent);
+    property public final android.content.ComponentName componentName;
+    property public final String? intentAction;
   }
 
   public final class ActivityRule extends androidx.window.embedding.EmbeddingRule {
@@ -32,9 +34,10 @@
   }
 
   public final class ActivityStack {
-    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activities, optional boolean isEmpty);
+    ctor public ActivityStack(java.util.List<? extends android.app.Activity> activitiesInProcess, optional boolean isEmpty);
     method public operator boolean contains(android.app.Activity activity);
     method public boolean isEmpty();
+    property public final boolean isEmpty;
   }
 
   public abstract class EmbeddingRule {
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
index f2b811c..836a897 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityFilter.kt
@@ -44,10 +44,10 @@
     /**
      * Action used for activity launch intent.
      *
-     * To match with intents based only on the [Intent.getAction], use a wildcard (&#42/&#42) with
-     * [activityComponentInfo].
+     * If it is not `null`, the [ActivityFilter] will check the activity [Intent.getAction] besides
+     * the component name. If it is `null`, [Intent.getAction] will be ignored.
      */
-    internal val intentAction: String?
+    val intentAction: String?
 ) {
 
     /**
@@ -59,8 +59,9 @@
      *   - `package/*`
      *   - `package/suffix.*`
      *   - `*/*`
-     * @param intentAction Action used for activity launch intent. To match with intents based only
-     * on the [Intent.getAction], use a wildcard (&#42/&#42) with [componentName].
+     * @param intentAction Action used for activity launch intent. If it is not `null`, the
+     * [ActivityFilter] will check the activity [Intent.getAction] besides the component name. If it
+     * is `null`, [Intent.getAction] will be ignored.
      */
     constructor(componentName: ComponentName, intentAction: String?) : this(
         ActivityComponentInfo(componentName),
@@ -90,7 +91,13 @@
         ) { "Wildcard in class name is only allowed at the end." }
     }
 
-    /** Returns `true` if the [ActivityFilter] matches this [intent]. */
+    /**
+     * Returns `true` if the [ActivityFilter] matches this [Intent].
+     * If the [ActivityFilter] is created with an intent action, the filter will also compare it
+     * with [Intent.getAction].
+     *
+     * @param intent the [Intent] to test against.
+     */
     fun matchesIntent(intent: Intent): Boolean {
         val match = if (!isIntentMatching(intent, activityComponentInfo)) {
                 false
@@ -107,7 +114,13 @@
         return match
     }
 
-    /** Returns `true` if the [ActivityFilter] matches this [activity]. */
+    /**
+     * Returns `true` if the [ActivityFilter] matches this [Activity].
+     * If the [ActivityFilter] is created with an intent action, the filter will also compare it
+     * with [Intent.getAction] of [Activity.getIntent].
+     *
+     * @param activity the [Activity] to test against.
+     */
     fun matchesActivity(activity: Activity): Boolean {
         val match =
             isActivityOrIntentMatching(activity, activityComponentInfo) &&
@@ -122,17 +135,13 @@
         return match
     }
 
-    /** Returns `true` if the [ActivityFilter] matches the [Class.getName]. */
-    fun <T : Activity> matchesClassName(clazz: Class<T>): Boolean {
-        return activityComponentInfo.className == clazz.name
-    }
-
     /**
-     * Returns `true` if the [ActivityFilter] matches the [Class.getName] or includes wildcard (`*`)
+     * [ComponentName] that the [ActivityFilter] will use to match [Activity] and [Intent].
      */
-    fun <T : Activity> matchesClassNameOrWildCard(clazz: Class<T>?): Boolean {
-        return clazz?.let(::matchesClassName) ?: activityComponentInfo.className.contains("*")
-    }
+    val componentName: ComponentName
+        get() {
+            return ComponentName(activityComponentInfo.packageName, activityComponentInfo.className)
+        }
 
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
index 45c928e..d38b78d 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityRule.kt
@@ -22,7 +22,7 @@
  */
 class ActivityRule internal constructor(
     /**
-     * Filters used to choose when to apply this rule. The rule may be used if any one of the
+     * Filters used to choose when to apply this rule. The rule will be applied if any one of the
      * provided filters matches.
      */
     val filters: Set<ActivityFilter>,
@@ -36,6 +36,7 @@
 
     /**
      * Builder for [ActivityRule].
+     *
      * @param filters See [ActivityRule.filters].
      */
     class Builder(
diff --git a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
index 676e78d..bd90e21 100644
--- a/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
+++ b/window/window/src/main/java/androidx/window/embedding/ActivityStack.kt
@@ -20,32 +20,35 @@
 /**
  * A container that holds a stack of activities, overlapping and bound to the same rectangle on the
  * screen.
+ *
+ * @param activitiesInProcess the [Activity] list in this application's process that belongs to this
+ * [ActivityStack]. Activities running in other processes will not be contained in this list.
+ * @param isEmpty whether there is no [Activity] running in this [ActivityStack]. It can be
+ * non-empty when the [activitiesInProcess] is empty, because there can be [Activity] running in
+ * other processes, which will not be reported in [activitiesInProcess].
  */
 class ActivityStack(
     /**
-     * The [Activity] list in this application's process that belongs to this ActivityStack.
+     * The [Activity] list in this application's process that belongs to this [ActivityStack].
      *
-     * Note that Activities that are running in other processes do not contain in this [Activity]
+     * Note that Activities that are running in other processes will not be contained in this
      * list. They can be in any position in terms of ordering relative to the activities in the
      * list.
      */
-    internal val activities: List<Activity>,
-    private val isEmpty: Boolean = false
+    internal val activitiesInProcess: List<Activity>,
+    /**
+     * Whether there is no [Activity] running in this [ActivityStack].
+     *
+     * Note that [activitiesInProcess] only report [Activity] in the process used to create this
+     * ActivityStack. That said, if this ActivityStack only contains activities from other
+     * process(es), [activitiesInProcess] will return an empty list, but this method will return
+     * `false`.
+     */
+    val isEmpty: Boolean = false
 ) {
 
     operator fun contains(activity: Activity): Boolean {
-        return activities.contains(activity)
-    }
-
-    /**
-     * Returns `true` if there's no [Activity] running in this ActivityStack.
-     *
-     * Note that [activities] only report Activity in the process used to create this
-     * ActivityStack. That said, if this ActivityStack only contains activities from other
-     * process(es), [activities] will return empty list, and this method will return `false`.
-     */
-    fun isEmpty(): Boolean {
-        return isEmpty
+        return activitiesInProcess.contains(activity)
     }
 
     override fun equals(other: Any?): Boolean {
@@ -54,14 +57,14 @@
 
         other as ActivityStack
 
-        if (activities != other.activities) return false
+        if (activitiesInProcess != other.activitiesInProcess) return false
         if (isEmpty != other.isEmpty) return false
 
         return true
     }
 
     override fun hashCode(): Int {
-        var result = activities.hashCode()
+        var result = activitiesInProcess.hashCode()
         result = 31 * result + isEmpty.hashCode()
         return result
     }
@@ -69,7 +72,7 @@
     override fun toString(): String {
         return buildString {
             append("ActivityStack{")
-            append("activities=$activities")
+            append("activitiesInProcess=$activitiesInProcess")
             append("isEmpty=$isEmpty}")
         }
     }
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitController.kt b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
index 5d45231..2d88b02 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitController.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitController.kt
@@ -49,6 +49,12 @@
 
     /**
      * Registers a new runtime rule. Will be cleared automatically when the process is stopped.
+     *
+     * Note that updating the existing rule will **not** be applied to any existing split activity
+     * container, and will only be used for new split containers created with future activity
+     * launches.
+     *
+     * @param rule new [EmbeddingRule] to register.
      */
     fun registerRule(rule: EmbeddingRule) {
         embeddingBackend.registerRule(rule)
@@ -56,6 +62,8 @@
 
     /**
      * Unregisters a runtime rule that was previously registered via [SplitController.registerRule].
+     *
+     * @param rule the previously registered [EmbeddingRule] to unregister.
      */
     fun unregisterRule(rule: EmbeddingRule) {
         embeddingBackend.unregisterRule(rule)
@@ -78,6 +86,12 @@
      * activity to the side). The reported splits in the list are ordered from
      * bottom to top by their z-order, more recent splits appearing later.
      * Guaranteed to be called at least once to report the most recent state.
+     *
+     * @param activity only split that this [Activity] is part of will be reported.
+     * @param executor when there is an update to the active split state(s), the [consumer] will be
+     * invoked on this [Executor].
+     * @param consumer [Consumer] that will be invoked on the [executor] when there is an update to
+     * the active split state(s).
      */
     fun addSplitListener(
         activity: Activity,
@@ -88,7 +102,9 @@
     }
 
     /**
-     * Unregisters a listener for updates about the active split states.
+     * Unregisters a runtime rule that was previously registered via [addSplitListener].
+     *
+     * @param consumer the previously registered [Consumer] to unregister.
      */
     fun removeSplitListener(
         consumer: Consumer<List<SplitInfo>>
@@ -118,6 +134,8 @@
     /**
      * Checks if an activity is embedded and its presentation may be customized by its or any other
      * process.
+     *
+     * @param activity the [Activity] to check.
      */
     // TODO(b/204399167) Migrate to a Flow
     fun isActivityEmbedded(activity: Activity): Boolean {
@@ -149,8 +167,17 @@
         /**
          * Initializes the shared class instance with the split rules statically defined in an
          * app-provided XML. The rules will be kept for the lifetime of the application process.
-         * <p>It's recommended to set the static rules via an [androidx.startup.Initializer], so
-         * that they are applied early in the application startup before any activities appear.
+         * <p>It's recommended to set the static rules via an [androidx.startup.Initializer], or
+         * [android.app.Application.onCreate], so that they are applied early in the application
+         * startup before any activities appear.
+         * <p>Note that it is not necessary to call this function in order to use [SplitController].
+         * If the app doesn't have any static rule, it can use [registerRule] to register rules at
+         * any time.
+         *
+         * @param context the context to read the split resource from.
+         * @param staticRuleResourceId the resource containing the static split rules.
+         * @throws IllegalArgumentException if any of the rules in the XML is malformed or if
+         * there's a duplicated [tag][EmbeddingRule.tag].
          */
         @JvmStatic
         fun initialize(context: Context, staticRuleResourceId: Int) {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
index 7606d64..3cabc9e 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitInfo.kt
@@ -20,8 +20,17 @@
 
 /** Describes a split pair of two containers with activities. */
 class SplitInfo internal constructor(
+    /**
+     * The [ActivityStack] representing the primary split container.
+     */
     val primaryActivityStack: ActivityStack,
+    /**
+     * The [ActivityStack] representing the secondary split container.
+     */
     val secondaryActivityStack: ActivityStack,
+    /**
+     * Ratio of the Task width that is given to the primary split container.
+     */
     val splitRatio: Float
 ) {
     operator fun contains(activity: Activity): Boolean {
diff --git a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
index daf4a46..d09fa3b 100644
--- a/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
+++ b/window/window/src/main/java/androidx/window/embedding/SplitPairFilter.kt
@@ -30,6 +30,24 @@
  * It is used when a new activity is started from the primary activity.
  * If the filter matches the primary [Activity.getComponentName] and the new started activity
  * [Intent], it matches the [SplitPairRule] that holds this filter.
+ *
+ * @param primaryActivityName Component name of the primary activity in the split. Must be
+ * non-empty. Can contain a single wildcard at the end.
+ * Supported formats:
+ * - package/class
+ * - `package/*`
+ * - `package/suffix.*`
+ * - `*/*`
+ * @param secondaryActivityName Component name of the secondary activity in the split. Must be
+ * non-empty. Can contain a single wildcard at the end.
+ * Supported formats:
+ * - package/class
+ * - `package/*`
+ * - `package/suffix.*`
+ * - `*/*`
+ * @param secondaryActivityIntentAction action used for secondary activity launch Intent. If it is
+ * not `null`, the [SplitPairFilter] will check the activity [Intent.getAction] besides the
+ * component name. If it is `null`, [Intent.getAction] will be ignored.
  */
 class SplitPairFilter(
     /**
@@ -43,8 +61,8 @@
      */
     val primaryActivityName: ComponentName,
     /**
-     * Component name in the intent for the secondary activity in the split. Must be non-empty.
-     * Can contain a single wildcard at the end.
+     * Component name of the secondary activity in the split. Must be non-empty. Can contain a
+     * single wildcard at the end.
      * Supported formats:
      * - package/class
      * - `package/*`
@@ -53,7 +71,10 @@
      */
     val secondaryActivityName: ComponentName,
     /**
-     * Action used for secondary activity launch.
+     * Action used for secondary activity launch Intent.
+     *
+     * If it is not `null`, the [SplitPairFilter] will check the activity [Intent.getAction] besides
+     * the component name. If it is `null`, [Intent.getAction] will be ignored.
      */
     val secondaryActivityIntentAction: String?
 ) {
@@ -63,6 +84,11 @@
 
     /**
      * Returns `true` if this [SplitPairFilter] matches [primaryActivity] and [secondaryActivity].
+     * If the [SplitPairFilter] is created with [secondaryActivityIntentAction], the filter will
+     * also compare it with [Intent.getAction] of [Activity.getIntent] of [secondaryActivity].
+     *
+     * @param primaryActivity the [Activity] to test against with the [primaryActivityName]
+     * @param secondaryActivity the [Activity] to test against with the [secondaryActivityName]
      */
     fun matchesActivityPair(primaryActivity: Activity, secondaryActivity: Activity): Boolean {
         // Check if the activity component names match
@@ -87,6 +113,11 @@
     /**
      * Returns `true` if this [SplitPairFilter] matches the [primaryActivity] and the
      * [secondaryActivityIntent]
+     * If the [SplitPairFilter] is created with [secondaryActivityIntentAction], the filter will
+     * also compare it with [Intent.getAction] of the [secondaryActivityIntent].
+     *
+     * @param primaryActivity the [Activity] to test against with the [primaryActivityName]
+     * @param secondaryActivityIntent the [Intent] to test against with the [secondaryActivityName]
      */
     fun matchesActivityIntentPair(
         primaryActivity: Activity,
diff --git a/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt b/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
index 6d06d87..59acbb2 100644
--- a/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
+++ b/window/window/src/test/java/androidx/window/embedding/ActivityFilterTest.kt
@@ -19,8 +19,6 @@
 import android.app.Activity
 import androidx.window.core.ActivityComponentInfo
 import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
 import org.junit.Test
 
 /**
@@ -61,27 +59,6 @@
         assertEquals(first.hashCode(), second.hashCode())
     }
 
-    @Test
-    fun matchesClassName() {
-        val filter = ActivityFilter(
-            ActivityComponentInfo(FAKE_PACKAGE, Activity::class.java.name),
-            null
-        )
-        assertTrue(filter.matchesClassName(Activity::class.java))
-        assertFalse(filter.matchesClassName(FakeClass::class.java))
-    }
-
-    @Test
-    fun matchesWildCard() {
-        val filter = ActivityFilter(
-            ActivityComponentInfo(FAKE_PACKAGE, FAKE_CLASS_WILDCARD_VALID),
-            null
-        )
-
-        assertTrue(filter.matchesClassNameOrWildCard<Activity>(null))
-        assertFalse(filter.matchesClassNameOrWildCard(Activity::class.java))
-    }
-
     private class FakeClass : Activity()
 
     companion object {