Merge changes I1a3dc9fe,I5df63d3b into androidx-main

* changes:
  Fix playground build
  Fix playground build for biometric
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index d49a8da..80db919 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -232,6 +232,7 @@
     method public int getMaxSnippetSize();
     method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public java.util.List<java.lang.String!> getSchemaTypes();
@@ -240,9 +241,11 @@
     method public int getTermMatch();
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
+    field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -251,6 +254,8 @@
     ctor public SearchSpec.Builder();
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, 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 addSchemaByDataClass(java.util.Collection<java.lang.Class<?>!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index d49a8da..80db919 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -232,6 +232,7 @@
     method public int getMaxSnippetSize();
     method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public java.util.List<java.lang.String!> getSchemaTypes();
@@ -240,9 +241,11 @@
     method public int getTermMatch();
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
+    field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -251,6 +254,8 @@
     ctor public SearchSpec.Builder();
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, 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 addSchemaByDataClass(java.util.Collection<java.lang.Class<?>!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index d49a8da..80db919 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -232,6 +232,7 @@
     method public int getMaxSnippetSize();
     method public java.util.List<java.lang.String!> getNamespaces();
     method public int getOrder();
+    method public java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getProjections();
     method public int getRankingStrategy();
     method public int getResultCountPerPage();
     method public java.util.List<java.lang.String!> getSchemaTypes();
@@ -240,9 +241,11 @@
     method public int getTermMatch();
     field public static final int ORDER_ASCENDING = 1; // 0x1
     field public static final int ORDER_DESCENDING = 0; // 0x0
+    field public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
     field public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2; // 0x2
     field public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1; // 0x1
     field public static final int RANKING_STRATEGY_NONE = 0; // 0x0
+    field public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3; // 0x3
     field public static final int TERM_MATCH_EXACT_ONLY = 1; // 0x1
     field public static final int TERM_MATCH_PREFIX = 2; // 0x2
   }
@@ -251,6 +254,8 @@
     ctor public SearchSpec.Builder();
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.lang.String!...);
     method public androidx.appsearch.app.SearchSpec.Builder addNamespace(java.util.Collection<java.lang.String!>);
+    method public androidx.appsearch.app.SearchSpec.Builder addProjection(String, 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 addSchemaByDataClass(java.util.Collection<java.lang.Class<?>!>) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaByDataClass(Class<?>!...) throws androidx.appsearch.exceptions.AppSearchException;
     method public androidx.appsearch.app.SearchSpec.Builder addSchemaType(java.lang.String!...);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
index c1bb7af..60542d9 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SearchSpecTest.java
@@ -60,17 +60,26 @@
     public void testGetProjectionTypePropertyMasks() {
         SearchSpec searchSpec = new SearchSpec.Builder()
                 .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
-                .addProjectionTypePropertyPaths("TypeA", "field1", "field2.subfield2")
-                .addProjectionTypePropertyPaths("TypeB", "field7")
-                .addProjectionTypePropertyPaths("TypeC")
+                .addProjection("TypeA", "field1", "field2.subfield2")
+                .addProjection("TypeB", "field7")
+                .addProjection("TypeC")
                 .build();
 
-        Map<String, List<String>> typePropertyPathMap =
-                searchSpec.getProjectionTypePropertyPaths();
+        Map<String, List<String>> typePropertyPathMap = searchSpec.getProjections();
         assertThat(typePropertyPathMap.keySet())
                 .containsExactly("TypeA", "TypeB", "TypeC");
         assertThat(typePropertyPathMap.get("TypeA")).containsExactly("field1", "field2.subfield2");
         assertThat(typePropertyPathMap.get("TypeB")).containsExactly("field7");
         assertThat(typePropertyPathMap.get("TypeC")).isEmpty();
     }
+
+    @Test
+    public void testGetRankingStrategy() {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_PREFIX)
+                .setRankingStrategy(SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE)
+                .build();
+        assertThat(searchSpec.getRankingStrategy()).isEqualTo(
+                SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE);
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
index 10ee535..2369c2c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTestBase.java
@@ -54,6 +54,7 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -686,6 +687,174 @@
     }
 
     @Test
+    public void testQuery_projection() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1, email2).build()));
+
+        // Query with type property paths {"Email", ["subject", "to"]}
+        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
+    @Test
+    public void testQuery_projectionEmpty() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1, email2).build()));
+
+        // Query with type property paths {"Email", []}
+        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, Collections.emptyList())
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testQuery_projectionNonExistentType() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index two documents
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1, email2).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
+        SearchResults searchResults = mDb1.query("body", new SearchSpec.Builder()
+                .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                .addProjection("NonExistentType", Collections.emptyList())
+                .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+                .build());
+        List<GenericDocument> documents = convertSearchResultsToDocuments(searchResults);
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
     public void testQuery_twoInstances() throws Exception {
         // Schema registration
         mDb1.setSchema(new SetSchemaRequest.Builder()
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
index 0d71c4b..0120153 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/GlobalSearchSessionCtsTestBase.java
@@ -371,4 +371,198 @@
         assertAddedBetweenSnapshots(beforeBodyNamespace1Documents, afterBodyNamespace1Documents,
                 ImmutableList.of(document1));
     }
+
+    // TODO(b/175039682) Add test cases for wildcard projection once go/oag/1534646 is submitted.
+    @Test
+    public void testGlobalQuery_projectionTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email2).build()));
+
+        // Query with type property paths {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
+                                "subject", "to")
+                        .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionEmptyTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email2).build()));
+
+        // Query with type property paths {"Email", []}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection(AppSearchEmail.SCHEMA_TYPE,
+                                Collections.emptyList())
+                        .build());
+
+        // The two email documents should have been returned without any properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
+
+    @Test
+    public void testGlobalQuery_projectionNonExistentTypeTwoInstances() throws Exception {
+        // Schema registration
+        mDb1.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+        mDb2.setSchema(
+                new SetSchemaRequest.Builder()
+                        .addSchema(AppSearchEmail.SCHEMA)
+                        .build()).get();
+
+        // Index one document in each database.
+        AppSearchEmail email1 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb1.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email1).build()));
+
+        AppSearchEmail email2 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setFrom("from@example.com")
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .setBody("This is the body of the testPut email")
+                        .build();
+        checkIsBatchResultSuccess(mDb2.putDocuments(
+                new PutDocumentsRequest.Builder()
+                        .addGenericDocument(email2).build()));
+
+        // Query with type property paths {"NonExistentType", []}, {"Email", ["subject", "to"]}
+        List<GenericDocument> documents =
+                snapshotResults("body", new SearchSpec.Builder()
+                        .setTermMatch(SearchSpec.TERM_MATCH_EXACT_ONLY)
+                        .addProjection("NonExistentType", Collections.emptyList())
+                        .addProjection(AppSearchEmail.SCHEMA_TYPE, "subject", "to")
+                        .build());
+
+        // The two email documents should have been returned with only the "subject" and "to"
+        // properties.
+        AppSearchEmail expected1 =
+                new AppSearchEmail.Builder("uri2")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        AppSearchEmail expected2 =
+                new AppSearchEmail.Builder("uri1")
+                        .setNamespace("namespace")
+                        .setCreationTimestampMillis(1000)
+                        .setTo("to1@example.com", "to2@example.com")
+                        .setSubject("testPut example")
+                        .build();
+        assertThat(documents).containsExactly(expected1, expected2);
+    }
 }
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 1cf99e8..a1dd8fe 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SearchSpec.java
@@ -45,12 +45,10 @@
 // TODO(sidchhabra) : AddResultSpec fields for Snippets etc.
 public final class SearchSpec {
     /**
-     * Schema type to be used in {@link SearchSpec.Builder#addProjectionTypePropertyPath} to apply
+     * Schema type to be used in {@link SearchSpec.Builder#addProjection} to apply
      * property paths to all results, excepting any types that have had their own, specific
      * property paths set.
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     public static final String PROJECTION_SCHEMA_TYPE_WILDCARD = "*";
 
     static final String TERM_MATCH_TYPE_FIELD = "termMatchType";
@@ -108,7 +106,8 @@
     @IntDef(value = {
             RANKING_STRATEGY_NONE,
             RANKING_STRATEGY_DOCUMENT_SCORE,
-            RANKING_STRATEGY_CREATION_TIMESTAMP
+            RANKING_STRATEGY_CREATION_TIMESTAMP,
+            RANKING_STRATEGY_RELEVANCE_SCORE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface RankingStrategy {}
@@ -119,6 +118,8 @@
     public static final int RANKING_STRATEGY_DOCUMENT_SCORE = 1;
     /** Ranked by document creation timestamps. */
     public static final int RANKING_STRATEGY_CREATION_TIMESTAMP = 2;
+    /** Ranked by document relevance score. */
+    public static final int RANKING_STRATEGY_RELEVANCE_SCORE = 3;
 
     /**
      * Order for query result.
@@ -229,11 +230,9 @@
      *
      * <p>Calling this function repeatedly is inefficient. Prefer to retain the Map returned
      * by this function, rather than calling it multiple times.
-     * @hide
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY)
     @NonNull
-    public Map<String, List<String>> getProjectionTypePropertyPaths() {
+    public Map<String, List<String>> getProjections() {
         Bundle typePropertyPathsBundle =
                 mBundle.getBundle(PROJECTION_TYPE_PROPERTY_PATHS_FIELD);
         Set<String> schemaTypes = typePropertyPathsBundle.keySet();
@@ -389,7 +388,7 @@
         public Builder setRankingStrategy(@RankingStrategy int rankingStrategy) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkArgumentInRange(rankingStrategy, RANKING_STRATEGY_NONE,
-                    RANKING_STRATEGY_CREATION_TIMESTAMP, "Result ranking strategy");
+                    RANKING_STRATEGY_RELEVANCE_SCORE, "Result ranking strategy");
             mBundle.putInt(RANKING_STRATEGY_FIELD, rankingStrategy);
             return this;
         }
@@ -524,14 +523,12 @@
          *   subject: "IMPORTANT"
          * }
          * }</pre>
-         * @hide
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
         @NonNull
-        public SearchSpec.Builder addProjectionTypePropertyPaths(
+        public SearchSpec.Builder addProjection(
                 @NonNull String schemaType, @NonNull String... propertyPaths) {
             Preconditions.checkNotNull(propertyPaths);
-            return addProjectionTypePropertyPaths(schemaType, Arrays.asList(propertyPaths));
+            return addProjection(schemaType, Arrays.asList(propertyPaths));
         }
 
         /**
@@ -548,12 +545,10 @@
          * apply to all results, excepting any types that have their own, specific property paths
          * set.
          *
-         * {@see SearchSpec.Builder#addProjectionTypePropertyPath(String, String...)}
-         * @hide
+         * {@see SearchSpec.Builder#addProjection(String, String...)}
          */
-        @RestrictTo(RestrictTo.Scope.LIBRARY)
         @NonNull
-        public SearchSpec.Builder addProjectionTypePropertyPaths(
+        public SearchSpec.Builder addProjection(
                 @NonNull String schemaType, @NonNull Collection<String> propertyPaths) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Preconditions.checkNotNull(schemaType);
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index b4d076d..79b87f8 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -499,23 +499,29 @@
             @NonNull Set<String> prefixes, @NonNull String queryExpression,
             @NonNull SearchSpec searchSpec)
             throws AppSearchException {
-        SearchSpecProto searchSpecProto =
-                SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
-        SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
-                .setQuery(queryExpression);
-
-        ResultSpecProto resultSpec = SearchSpecToProtoConverter.toResultSpecProto(searchSpec);
-        ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
-        SearchResultProto searchResultProto;
-
+        SearchSpecProto.Builder searchSpecBuilder =
+                SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder().setQuery(
+                        queryExpression);
         // rewriteSearchSpecForPrefixesLocked will return false if none of the prefixes that the
         // client is trying to search on exist, so we can return an empty SearchResult and skip
         // sending request to Icing.
         if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes)) {
             return new SearchResultPage(Bundle.EMPTY);
         }
-        searchResultProto = mIcingSearchEngineLocked.search(
-                searchSpecBuilder.build(), scoringSpec, resultSpec);
+
+        ResultSpecProto.Builder resultSpecBuilder =
+                SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
+
+        // rewriteResultSpecForPrefixesLocked will return false if none of the prefixes that the
+        // client is trying to search on exist, so we can return an empty SearchResult and skip
+        // sending request to Icing.
+        if (!rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes)) {
+            return new SearchResultPage(Bundle.EMPTY);
+        }
+
+        ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
+        SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
+                searchSpecBuilder.build(), scoringSpec, resultSpecBuilder.build());
         checkSuccess(searchResultProto.getStatus());
 
         return rewriteSearchResultProto(searchResultProto);
@@ -908,6 +914,47 @@
         return true;
     }
 
+    /**
+     * Rewrites the typePropertyMasks that exist in {@code prefixes}.
+     *
+     * <p>This method should be only called in query methods and get the READ lock to keep thread
+     * safety.
+     *
+     * @return false if none of the requested prefixes exist.
+     */
+    @VisibleForTesting
+    @GuardedBy("mReadWriteLock")
+    boolean rewriteResultSpecForPrefixesLocked(
+            @NonNull ResultSpecProto.Builder resultSpecBuilder,
+            @NonNull Set<String> prefixes) {
+        // Create a copy since retainAll() modifies the original set.
+        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
+        existingPrefixes.retainAll(prefixes);
+
+        if (existingPrefixes.isEmpty()) {
+            // None of the prefixes exist, empty query.
+            return false;
+        }
+
+        List<ResultSpecProto.TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
+        // Rewrite filters to include a database prefix.
+        for (String prefix : existingPrefixes) {
+            Set<String> existingSchemaTypes = mSchemaMapLocked.get(prefix);
+            // Qualify the given schema types
+            for (ResultSpecProto.TypePropertyMask typePropertyMask :
+                    resultSpecBuilder.getTypePropertyMasksList()) {
+                String qualifiedType = prefix + typePropertyMask.getSchemaType();
+                if (existingSchemaTypes.contains(qualifiedType)) {
+                    prefixedTypePropertyMasks.add(
+                            typePropertyMask.toBuilder().setSchemaType(qualifiedType).build());
+                }
+            }
+        }
+        resultSpecBuilder.clearTypePropertyMasks().addAllTypePropertyMasks(
+                prefixedTypePropertyMasks);
+        return true;
+    }
+
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
     SchemaProto getSchemaProtoLocked() throws AppSearchException {
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index 762583d..c13d288 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -26,13 +26,18 @@
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.TermMatchType;
 
+import java.util.List;
+import java.util.Map;
+
 /**
  * Translates a {@link SearchSpec} into icing search protos.
+ *
  * @hide
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SearchSpecToProtoConverter {
-    private SearchSpecToProtoConverter() {}
+    private SearchSpecToProtoConverter() {
+    }
 
     /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */
     @NonNull
@@ -56,14 +61,20 @@
     @NonNull
     public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) {
         Preconditions.checkNotNull(spec);
-        return ResultSpecProto.newBuilder()
+        ResultSpecProto.Builder builder = ResultSpecProto.newBuilder()
                 .setNumPerPage(spec.getResultCountPerPage())
                 .setSnippetSpec(
                         ResultSpecProto.SnippetSpecProto.newBuilder()
                                 .setNumToSnippet(spec.getSnippetCount())
                                 .setNumMatchesPerProperty(spec.getSnippetCountPerProperty())
-                                .setMaxWindowBytes(spec.getMaxSnippetSize()))
-                .build();
+                                .setMaxWindowBytes(spec.getMaxSnippetSize()));
+        Map<String, List<String>> projectionTypePropertyPaths = spec.getProjections();
+        for (Map.Entry<String, List<String>> e : projectionTypePropertyPaths.entrySet()) {
+            builder.addTypePropertyMasks(
+                    ResultSpecProto.TypePropertyMask.newBuilder().setSchemaType(
+                            e.getKey()).addAllPaths(e.getValue()));
+        }
+        return builder.build();
     }
 
     /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
@@ -78,17 +89,27 @@
         if (orderCodeProto == null) {
             throw new IllegalArgumentException("Invalid result ranking order: " + orderCode);
         }
-        protoBuilder.setOrderBy(orderCodeProto);
-
-        @SearchSpec.RankingStrategy int rankingStrategyCode = spec.getRankingStrategy();
-        ScoringSpecProto.RankingStrategy.Code rankingStrategyCodeProto =
-                ScoringSpecProto.RankingStrategy.Code.forNumber(rankingStrategyCode);
-        if (rankingStrategyCodeProto == null) {
-            throw new IllegalArgumentException("Invalid result ranking strategy: "
-                    + rankingStrategyCode);
-        }
-        protoBuilder.setRankBy(rankingStrategyCodeProto);
+        protoBuilder.setOrderBy(orderCodeProto).setRankBy(
+                toProtoRankingStrategy(spec.getRankingStrategy()));
 
         return protoBuilder.build();
     }
+
+    private static ScoringSpecProto.RankingStrategy.Code toProtoRankingStrategy(
+            @SearchSpec.RankingStrategy int rankingStrategyCode) {
+        switch (rankingStrategyCode) {
+            case SearchSpec.RANKING_STRATEGY_NONE:
+                return ScoringSpecProto.RankingStrategy.Code.NONE;
+            case SearchSpec.RANKING_STRATEGY_DOCUMENT_SCORE:
+                return ScoringSpecProto.RankingStrategy.Code.DOCUMENT_SCORE;
+            case SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP:
+                return ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP;
+            case SearchSpec.RANKING_STRATEGY_RELEVANCE_SCORE:
+                return ScoringSpecProto.RankingStrategy
+                        .Code.RELEVANCE_SCORE_NONFUNCTIONAL_PLACEHOLDER;
+            default:
+                throw new IllegalArgumentException("Invalid result ranking strategy: "
+                        + rankingStrategyCode);
+        }
+    }
 }
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
index 199694d..4b67b94 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencyTracker/AffectedModuleDetector.kt
@@ -70,13 +70,27 @@
  * Since this needs to check project dependency graph to work, it cannot be accessed before
  * all projects are loaded. Doing so will throw an exception.
  */
-abstract class AffectedModuleDetector {
+abstract class AffectedModuleDetector(
+    protected val logger: Logger?
+) {
     /**
-     * Returns whether this project was affected by current changes..
+     * Returns whether this project was affected by current changes.
      */
     abstract fun shouldInclude(project: Project): Boolean
 
     /**
+     * Returns whether this task was affected by current changes.
+     */
+    fun shouldInclude(task: Task): Boolean {
+        val include = shouldInclude(task.project)
+        val inclusionVerb = if (include) "Including" else "Excluding"
+        logger?.info(
+            "$inclusionVerb task ${task.path}"
+        )
+        return include
+    }
+
+    /**
      * Returns the set that the project belongs to. The set is one of the ProjectSubset above.
      * This is used by the test config generator.
      */
@@ -169,7 +183,7 @@
         @JvmStatic
         internal fun configureTaskGuard(task: Task) {
             task.onlyIf {
-                getOrThrow(task.project).shouldInclude(task.project)
+                getOrThrow(task.project).shouldInclude(task)
             }
         }
 
@@ -193,8 +207,8 @@
  */
 private class AcceptAll(
     private val wrapped: AffectedModuleDetector? = null,
-    private val logger: Logger? = null
-) : AffectedModuleDetector() {
+    logger: Logger? = null
+) : AffectedModuleDetector(logger) {
     override fun shouldInclude(project: Project): Boolean {
         val wrappedResult = wrapped?.shouldInclude(project)
         logger?.info("[AcceptAll] wrapper returned $wrappedResult but I'll return true")
@@ -216,14 +230,14 @@
  */
 class AffectedModuleDetectorImpl constructor(
     private val rootProject: Project,
-    private val logger: Logger?,
+    logger: Logger?,
     // used for debugging purposes when we want to ignore non module files
     private val ignoreUnknownProjects: Boolean = false,
     private val projectSubset: ProjectSubset = ProjectSubset.ALL_AFFECTED_PROJECTS,
     private val cobuiltTestPaths: Set<Set<String>> = COBUILT_TEST_PATHS,
     private val injectedGitClient: GitClient? = null,
     private val baseCommitOverride: String? = null
-) : AffectedModuleDetector() {
+) : AffectedModuleDetector(logger) {
     private val git by lazy {
         injectedGitClient ?: GitClientImpl(rootProject.projectDir, logger)
     }
@@ -275,11 +289,7 @@
     private var isPresubmit: Boolean = false
 
     override fun shouldInclude(project: Project): Boolean {
-        return (project.isRoot || affectedProjects.contains(project)).also {
-            logger?.info(
-                "checking whether I should include ${project.path} and my answer is $it"
-            )
-        }
+        return (project.isRoot || affectedProjects.contains(project))
     }
 
     override fun getSubset(project: Project): ProjectSubset {
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
index 90f9b42..1520d65 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCameraState.kt
@@ -173,7 +173,7 @@
                     updateSignal = null
                     updating = false
                 }
-                it.setRepeating(request)
+                it.startRepeating(request)
             }
 
             // Complete the result after the session closes to allow other threads to acquire a
diff --git a/camera/camera-camera2-pipe-testing/build.gradle b/camera/camera-camera2-pipe-testing/build.gradle
index 7e8920f..f389f29 100644
--- a/camera/camera-camera2-pipe-testing/build.gradle
+++ b/camera/camera-camera2-pipe-testing/build.gradle
@@ -37,6 +37,7 @@
     implementation(KOTLIN_COROUTINES_GUAVA)
     implementation(project(":camera:camera-camera2-pipe"))
 
+    compileOnly(ANDROIDX_TEST_CORE)
     compileOnly(JUNIT)
     compileOnly(ROBOLECTRIC)
 
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
new file mode 100644
index 0000000..0ab26e4
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulator.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureResult
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.CameraTimestamp
+import androidx.camera.camera2.pipe.FrameMetadata
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamId
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.withTimeoutOrNull
+
+/** Simulator for observing and responding to interactions with the a [CameraGraph]. */
+class CameraGraphSimulator(
+    private val config: CameraGraph.Config,
+    metadata: CameraMetadata
+) {
+    init {
+        check(config.camera == metadata.camera)
+    }
+
+    private val requestProcessor = FakeRequestProcessor()
+    private val cameraPipe = CameraPipe.External()
+    public val cameraGraph = cameraPipe.create(
+        config,
+        FakeCameraDevices(listOf(metadata)),
+        requestProcessor
+    )
+
+    private var frameClockNanos = atomic(0L)
+    private var frameCounter = atomic(0L)
+    private val pendingFrameQueue = mutableListOf<FrameSimulator>()
+
+    suspend fun simulateNextFrame(
+        advanceClockByNanos: Long = 33_366_666 // (2_000_000_000 / (60  / 1.001))
+    ): FrameSimulator? = generateNextFrame().also {
+        val clockNanos = frameClockNanos.addAndGet(advanceClockByNanos)
+        it?.simulateStarted(clockNanos)
+    }
+
+    private suspend fun generateNextFrame(): FrameSimulator? {
+        // This checks the pending frame queue and polls for the next request. If no request is
+        // available it will suspend until the next interaction with the request processor.
+        if (pendingFrameQueue.isEmpty()) {
+            val requestSequence =
+                withTimeoutOrNull(timeMillis = 50) { requestProcessor.nextRequestSequence() }
+                    ?: return null
+
+            // Each sequence is processed as a group, and if a sequence contains multiple requests
+            // the list of requests is processed in order before polling the next sequence.
+            for (request in requestSequence.requests) {
+                pendingFrameQueue.add(FrameSimulator(request, requestSequence))
+            }
+        }
+        return pendingFrameQueue.removeFirstOrNull()
+    }
+
+    /**
+     * A [FrameSimulator] allows a test to synchronously invoke callbacks. A single request can
+     * generate multiple captures (eg, if used as a repeating request). A [FrameSimulator] allows
+     * a test to control exactly one of those captures. This means that a new simulator is
+     * created for each frame, and allows tests to simulate unusual ordering or delays that may
+     * appear under real conditions.
+     */
+    inner class FrameSimulator internal constructor(
+        val request: Request,
+        val requestSequence: FakeRequestProcessor.RequestSequence,
+    ) {
+        private val requestMetadata = requestSequence.requestMetadata[request]!!
+        private val requestListeners = requestSequence.requestListeners[request]!!
+
+        val frameNumber: FrameNumber = FrameNumber(frameCounter.incrementAndGet())
+        var timestampNanos: Long? = null
+
+        fun simulateStarted(timestampNanos: Long) {
+            this.timestampNanos = timestampNanos
+
+            for (listener in requestListeners) {
+                listener.onStarted(requestMetadata, frameNumber, CameraTimestamp(timestampNanos))
+            }
+        }
+
+        fun simulatePartialCaptureResult(
+            resultMetadata: Map<CaptureResult.Key<*>, Any?>,
+            extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
+            extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
+        ) {
+            val metadata = createFakeMetadataFor(
+                resultMetadata = resultMetadata,
+                extraResultMetadata = extraResultMetadata,
+                extraMetadata = extraMetadata
+            )
+
+            for (listener in requestListeners) {
+                listener.onPartialCaptureResult(requestMetadata, frameNumber, metadata)
+            }
+        }
+
+        fun simulateTotalCaptureResult(
+            resultMetadata: Map<CaptureResult.Key<*>, Any?>,
+            extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
+            extraMetadata: Map<*, Any?> = emptyMap<Any, Any>(),
+            physicalResultMetadata: Map<CameraId, Map<CaptureResult.Key<*>, Any?>> = emptyMap()
+        ) {
+            val metadata = createFakeMetadataFor(
+                resultMetadata = resultMetadata,
+                extraResultMetadata = extraResultMetadata,
+                extraMetadata = extraMetadata
+            )
+            val frameInfo = FakeFrameInfo(
+                metadata = metadata, requestMetadata,
+                createFakePhysicalMetadata(physicalResultMetadata)
+            )
+
+            for (listener in requestListeners) {
+                listener.onTotalCaptureResult(requestMetadata, frameNumber, frameInfo)
+            }
+        }
+
+        fun simulateComplete(
+            resultMetadata: Map<CaptureResult.Key<*>, Any?>,
+            extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
+            extraMetadata: Map<*, Any?> = emptyMap<Any, Any>(),
+            physicalResultMetadata: Map<CameraId, Map<CaptureResult.Key<*>, Any?>> = emptyMap()
+        ) {
+            val metadata = createFakeMetadataFor(
+                resultMetadata = resultMetadata,
+                extraResultMetadata = extraResultMetadata,
+                extraMetadata = extraMetadata
+            )
+            val frameInfo = FakeFrameInfo(
+                metadata = metadata, requestMetadata,
+                createFakePhysicalMetadata(physicalResultMetadata)
+            )
+
+            for (listener in requestListeners) {
+                listener.onComplete(requestMetadata, frameNumber, frameInfo)
+            }
+        }
+
+        fun simulateFailure(captureFailure: CaptureFailure) {
+            for (listener in requestListeners) {
+                listener.onFailed(requestMetadata, frameNumber, captureFailure)
+            }
+        }
+
+        fun simulateBufferLoss(streamId: StreamId) {
+            for (listener in requestListeners) {
+                listener.onBufferLost(requestMetadata, frameNumber, streamId)
+            }
+        }
+
+        fun simulateAbort() {
+            for (listener in requestListeners) {
+                listener.onAborted(request)
+            }
+        }
+
+        private fun createFakePhysicalMetadata(
+            physicalResultMetadata: Map<CameraId, Map<CaptureResult.Key<*>, Any?>>
+        ): Map<CameraId, FrameMetadata> {
+            val resultMap = mutableMapOf<CameraId, FrameMetadata>()
+            for ((k, v) in physicalResultMetadata) {
+                resultMap[k] = createFakeMetadataFor(v)
+            }
+            return resultMap
+        }
+
+        private fun createFakeMetadataFor(
+            resultMetadata: Map<CaptureResult.Key<*>, Any?>,
+            extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
+            extraMetadata: Map<*, Any?> = emptyMap<Any, Any>(),
+        ): FakeFrameMetadata = FakeFrameMetadata(
+            camera = config.camera,
+            frameNumber = frameNumber,
+            resultMetadata = resultMetadata.toMap(),
+            extraResultMetadata = extraResultMetadata.toMap(),
+            extraMetadata = extraMetadata.toMap()
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt
new file mode 100644
index 0000000..7ec59dc
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeCameraDevices.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+
+/**
+ * This provides a fake implementation of [CameraDevices] for tests with a fixed list of Cameras.
+ */
+class FakeCameraDevices(
+    private val cameras: List<CameraMetadata>
+) : CameraDevices {
+    override fun findAll(): List<CameraId> = cameras.map { it.camera }
+    override suspend fun getMetadata(camera: CameraId): CameraMetadata = awaitMetadata(camera)
+    override fun awaitMetadata(camera: CameraId): CameraMetadata = cameras.first {
+        it.camera == camera
+    }
+}
\ No newline at end of file
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 bb9e816..046dd5f 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
@@ -35,6 +35,17 @@
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.FrameMetadata
 import androidx.camera.camera2.pipe.StreamId
+import kotlinx.atomicfu.atomic
+
+private val fakeCameraIds = atomic(0)
+internal fun nextCameraId(): CameraId = CameraId("FakeCamera-${fakeCameraIds.incrementAndGet()}")
+
+private val fakeRequestNumbers = atomic(0L)
+internal fun nextRequestNumber(): RequestNumber =
+    RequestNumber(fakeRequestNumbers.incrementAndGet())
+
+private val fakeFrameNumbers = atomic(0L)
+internal fun nextFrameNumber(): FrameNumber = FrameNumber(fakeFrameNumbers.incrementAndGet())
 
 /**
  * Utility class for interacting with objects that require pre-populated Metadata.
@@ -43,6 +54,7 @@
     public companion object {
         @JvmField
         public val TEST_KEY: Metadata.Key<Int> = Metadata.Key.create("test.key")
+
         @JvmField
         public val TEST_KEY_ABSENT: Metadata.Key<Int> = Metadata.Key.create("test.key.absent")
     }
@@ -62,7 +74,7 @@
 public class FakeCameraMetadata(
     characteristics: Map<CameraCharacteristics.Key<*>, Any?> = emptyMap(),
     metadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
-    cameraId: CameraId = CameraId("Fake")
+    cameraId: CameraId = nextCameraId()
 ) : FakeMetadata(metadata), CameraMetadata {
 
     private val values = characteristics.toMap()
@@ -102,7 +114,7 @@
     override val streams: Map<StreamId, Surface> = mapOf(),
     override val repeating: Boolean = false,
     override val request: Request = Request(listOf()),
-    override val requestNumber: RequestNumber = RequestNumber(4321)
+    override val requestNumber: RequestNumber = nextRequestNumber()
 ) : FakeMetadata(request.extras.plus(metadata)), RequestMetadata {
 
     override fun <T> get(key: CaptureRequest.Key<T>): T? = requestParameters[key] as T?
@@ -122,8 +134,8 @@
 public class FakeFrameMetadata(
     private val resultMetadata: Map<CaptureResult.Key<*>, Any?> = emptyMap(),
     extraResultMetadata: Map<Metadata.Key<*>, Any?> = emptyMap(),
-    override val camera: CameraId = CameraId("Fake"),
-    override val frameNumber: FrameNumber = FrameNumber(21),
+    override val camera: CameraId = nextCameraId(),
+    override val frameNumber: FrameNumber = nextFrameNumber(),
     override val extraMetadata: Map<*, Any?> = emptyMap<Any, Any>()
 ) : FakeMetadata(extraResultMetadata), FrameMetadata {
 
@@ -143,7 +155,6 @@
 /**
  * Utility class for interacting with objects require specific [TotalCaptureResult] metadata
  */
-
 @Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
 public class FakeFrameInfo(
     override val metadata: FrameMetadata = FakeFrameMetadata(),
@@ -161,7 +172,7 @@
     /** @throws UnsupportedOperationException */
     override fun unwrap(): TotalCaptureResult? {
         throw UnsupportedOperationException(
-            "FakeFrameInfo does not wrap a real TotalCaptureResult object"
+            "FakeFrameInfo does not wrap a real TotalCaptureResult object!"
         )
     }
 }
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestListener.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestListener.kt
index 190fc3f..ea1e43b 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestListener.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestListener.kt
@@ -16,50 +16,187 @@
 
 package androidx.camera.camera2.pipe.testing
 
+import android.hardware.camera2.CaptureFailure
 import androidx.camera.camera2.pipe.CameraTimestamp
 import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameMetadata
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.StreamId
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asSharedFlow
 
 /**
  * Fake implementation of a [Request.Listener] for tests.
+ *
+ * Events are exposed as SharedFlows of events to allow a test to block and wait for events to
+ * to be sent.
  */
 @Suppress("ListenerInterface")
-public class FakeRequestListener : Request.Listener {
-    public var lastStartedRequestMetadata: RequestMetadata? = null
-    public var lastStartedFrameNumber: FrameNumber? = null
-    public var lastStartedTimestamp: CameraTimestamp? = null
+public class FakeRequestListener(private val replayBuffer: Int = 10) : Request.Listener {
 
-    public var lastCompleteRequestMetadata: RequestMetadata? = null
-    public var lastCompleteFrameNumber: FrameNumber? = null
-    public var lastCompleteResult: FrameInfo? = null
+    private val _onStartedFlow = MutableSharedFlow<OnStarted>(replay = replayBuffer)
+    val onStartedFlow = _onStartedFlow.asSharedFlow()
 
-    public var lastAbortedRequest: Request? = null
+    private val _onPartialCaptureResultFlow =
+        MutableSharedFlow<OnPartialCaptureResult>(replay = replayBuffer)
+    val onPartialCaptureResultFlow = _onPartialCaptureResultFlow.asSharedFlow()
+
+    private val _onTotalCaptureResultFlow =
+        MutableSharedFlow<OnTotalCaptureResult>(replay = replayBuffer)
+    val onTotalCaptureResultFlow = _onTotalCaptureResultFlow.asSharedFlow()
+
+    private val _onCompleteFlow =
+        MutableSharedFlow<OnComplete>(replay = replayBuffer)
+    val onCompleteFlow = _onCompleteFlow.asSharedFlow()
+
+    private val _onBufferLostFlow =
+        MutableSharedFlow<OnBufferLost>(replay = replayBuffer)
+    val onBufferLostFlow = _onBufferLostFlow.asSharedFlow()
+
+    private val _onAbortedFlow =
+        MutableSharedFlow<OnAborted>(replay = replayBuffer)
+    val onAbortedFlow = _onAbortedFlow.asSharedFlow()
+
+    private val _onFailedFlow =
+        MutableSharedFlow<OnFailed>(replay = replayBuffer)
+    val onFailedFlow = _onFailedFlow.asSharedFlow()
 
     override fun onStarted(
         requestMetadata: RequestMetadata,
         frameNumber: FrameNumber,
         timestamp: CameraTimestamp
+    ) = check(
+        _onStartedFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnStarted(requestMetadata, frameNumber, timestamp)
+        )
     ) {
-        lastStartedRequestMetadata = requestMetadata
-        lastStartedFrameNumber = frameNumber
-        lastStartedTimestamp = timestamp
+        "Failed to emit onStarted event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
+    }
+
+    override fun onPartialCaptureResult(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        captureResult: FrameMetadata
+    ) = check(
+        _onPartialCaptureResultFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnPartialCaptureResult(requestMetadata, frameNumber, captureResult)
+        )
+    ) {
+        "Failed to emit OnPartialCaptureResult event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
+    }
+
+    override fun onTotalCaptureResult(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        totalCaptureResult: FrameInfo
+    ) = check(
+        _onTotalCaptureResultFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnTotalCaptureResult(requestMetadata, frameNumber, totalCaptureResult)
+        )
+    ) {
+        "Failed to emit OnTotalCaptureResult event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
     }
 
     override fun onComplete(
         requestMetadata: RequestMetadata,
         frameNumber: FrameNumber,
         result: FrameInfo
+    ) = check(
+        _onCompleteFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnComplete(requestMetadata, frameNumber, result)
+        )
     ) {
-        lastCompleteRequestMetadata = requestMetadata
-        lastCompleteFrameNumber = frameNumber
-        lastCompleteResult = result
+        "Failed to emit onComplete event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
     }
 
     override fun onAborted(
         request: Request
+    ) = check(
+        _onAbortedFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnAborted(request)
+        )
     ) {
-        lastAbortedRequest = request
+        "Failed to emit OnAborted event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
     }
-}
\ No newline at end of file
+
+    override fun onBufferLost(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        stream: StreamId
+    ) = check(
+        _onBufferLostFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnBufferLost(requestMetadata, frameNumber, stream)
+        )
+    ) {
+        "Failed to emit OnBufferLost event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
+    }
+
+    override fun onFailed(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        captureFailure: CaptureFailure
+    ) = check(
+        _onFailedFlow.tryEmit(
+            @Suppress("SyntheticAccessor")
+            OnFailed(requestMetadata, frameNumber, captureFailure)
+        )
+    ) {
+        "Failed to emit OnFailed event! The size of the replay buffer" +
+            "($replayBuffer) may need to be increased."
+    }
+}
+
+sealed class RequestListenerEvent
+class OnStarted(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val timestamp: CameraTimestamp
+) : RequestListenerEvent()
+
+class OnPartialCaptureResult(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val frameMetadata: FrameMetadata
+) : RequestListenerEvent()
+
+class OnTotalCaptureResult(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val frameInfo: FrameInfo
+) : RequestListenerEvent()
+
+class OnComplete(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val frameInfo: FrameInfo
+) : RequestListenerEvent()
+
+class OnAborted(
+    val request: Request
+) : RequestListenerEvent()
+
+class OnBufferLost(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val streamId: StreamId
+) : RequestListenerEvent()
+
+class OnFailed(
+    val requestMetadata: RequestMetadata,
+    val frameNumber: FrameNumber,
+    val captureFailure: CaptureFailure
+) : RequestListenerEvent()
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
new file mode 100644
index 0000000..fccdfd88
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
@@ -0,0 +1,357 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import android.hardware.camera2.CaptureRequest
+import android.view.Surface
+import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.RequestProcessor
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Fake implementation of a [RequestProcessor] that passes events to a [Channel].
+ *
+ * This allows kotlin tests to check sequences of interactions that dispatch in the background
+ * without blocking between events.
+ */
+public class FakeRequestProcessor(
+    private val streamToSurfaceMap: Map<StreamId, Surface> = emptyMap(),
+    private val defaultTemplate: RequestTemplate = RequestTemplate(1)
+) : RequestProcessor {
+    private val lock = Any()
+    private val eventChannel = Channel<Event>(Channel.UNLIMITED)
+    private val requestCounter = atomic(0L)
+
+    @GuardedBy("lock")
+    private var pendingSequence: CompletableDeferred<RequestSequence>? = null
+
+    @GuardedBy("lock")
+    private val requestSequenceQueue: MutableList<RequestSequence> = mutableListOf()
+
+    @GuardedBy("lock")
+    private var repeatingRequestSequence: RequestSequence? = null
+
+    @GuardedBy("lock")
+    private var _rejectRequests = false
+
+    var rejectRequests: Boolean
+        get() = synchronized(lock) {
+            _rejectRequests
+        }
+        set(value) {
+            synchronized(lock) {
+                _rejectRequests = value
+            }
+        }
+
+    override fun submit(
+        request: Request,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean {
+        val requestSequence =
+            createRequestSequence(
+                repeating = false,
+                listOf(request),
+                defaultParameters,
+                requiredParameters,
+                defaultListeners
+            )
+
+        if (rejectRequests) {
+            check(eventChannel.offer(Event(requestSequence = requestSequence, rejected = true)))
+            return false
+        }
+
+        val signal = synchronized(lock) {
+            requestSequenceQueue.add(requestSequence)
+            pendingSequence?.also {
+                pendingSequence = null
+            }
+        }
+        requestSequence.invokeOnSequenceCreated()
+        requestSequence.invokeOnSequenceSubmitted()
+        signal?.complete(requestSequence)
+
+        check(eventChannel.offer(Event(requestSequence = requestSequence, submit = true)))
+
+        return true
+    }
+
+    override fun submit(
+        requests: List<Request>,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean {
+        val requestSequence =
+            createRequestSequence(
+                repeating = false,
+                requests,
+                defaultParameters,
+                requiredParameters,
+                defaultListeners
+            )
+        if (rejectRequests) {
+            check(eventChannel.offer(Event(requestSequence = requestSequence, rejected = true)))
+            return false
+        }
+
+        val signal = synchronized(lock) {
+            requestSequenceQueue.add(requestSequence)
+            pendingSequence?.also {
+                pendingSequence = null
+            }
+        }
+        requestSequence.invokeOnSequenceCreated()
+        requestSequence.invokeOnSequenceSubmitted()
+        signal?.complete(requestSequence)
+
+        check(eventChannel.offer(Event(requestSequence = requestSequence, submit = true)))
+
+        return true
+    }
+
+    override fun startRepeating(
+        request: Request,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean {
+        val requestSequence =
+            createRequestSequence(
+                repeating = true,
+                listOf(request),
+                defaultParameters,
+                requiredParameters,
+                defaultListeners
+            )
+        if (rejectRequests) {
+            check(eventChannel.offer(Event(requestSequence = requestSequence, rejected = true)))
+            return false
+        }
+
+        val signal = synchronized(lock) {
+            repeatingRequestSequence = requestSequence
+            pendingSequence?.also {
+                pendingSequence = null
+            }
+        }
+        requestSequence.invokeOnSequenceCreated()
+        requestSequence.invokeOnSequenceSubmitted()
+        signal?.complete(requestSequence)
+
+        check(eventChannel.offer(Event(requestSequence = requestSequence, startRepeating = true)))
+        return true
+    }
+
+    override fun abortCaptures() {
+        val requestSequencesToAbort: List<RequestSequence>
+        synchronized(lock) {
+            requestSequencesToAbort = requestSequenceQueue.toList()
+            requestSequenceQueue.clear()
+        }
+        for (sequence in requestSequencesToAbort) {
+            sequence.invokeOnSequenceAborted()
+        }
+        check(eventChannel.offer(Event(abort = true)))
+    }
+
+    override fun stopRepeating() {
+        val requestSequence = synchronized(lock) {
+            repeatingRequestSequence.also {
+                repeatingRequestSequence = null
+            }
+        }
+        requestSequence?.invokeOnSequenceAborted()
+        check(eventChannel.offer(Event(stop = true)))
+    }
+
+    override fun close() {
+        synchronized(lock) {
+            rejectRequests = true
+        }
+        check(eventChannel.offer(Event(close = true)))
+    }
+
+    /**
+     * Get the next event from queue with an option to specify a timeout for tests.
+     */
+    suspend fun nextEvent(timeMillis: Long = 500): Event = withTimeout(timeMillis) {
+        eventChannel.receive()
+    }
+
+    suspend fun nextRequestSequence(): RequestSequence {
+
+        while (true) {
+            val pending: Deferred<RequestSequence>
+            synchronized(lock) {
+                var sequence = requestSequenceQueue.removeFirstOrNull()
+                if (sequence == null) {
+                    sequence = repeatingRequestSequence
+                }
+                if (sequence != null) {
+                    return sequence
+                }
+
+                if (pendingSequence == null) {
+                    pendingSequence = CompletableDeferred()
+                }
+                pending = pendingSequence!!
+            }
+
+            pending.await()
+        }
+    }
+
+    private fun createRequestSequence(
+        repeating: Boolean,
+        requests: List<Request>,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>,
+    ): RequestSequence {
+        val requestInfoMap = mutableMapOf<Request, RequestMetadata>()
+        val requestListenerMap = mutableMapOf<Request, List<Request.Listener>>()
+        for (request in requests) {
+            val captureParameters = mutableMapOf<CaptureRequest.Key<*>, Any?>()
+            val metadataParameters = mutableMapOf<Metadata.Key<*>, Any?>()
+            for ((k, v) in defaultParameters) {
+                if (k != null) {
+                    if (k is CaptureRequest.Key<*>) {
+                        captureParameters[k] = v
+                    } else if (k is Metadata.Key<*>) {
+                        metadataParameters[k] = v
+                    }
+                }
+            }
+            for ((k, v) in request.parameters) {
+                captureParameters[k] = v
+            }
+            for ((k, v) in requiredParameters) {
+                if (k != null) {
+                    if (k is CaptureRequest.Key<*>) {
+                        captureParameters[k] = v
+                    } else if (k is Metadata.Key<*>) {
+                        metadataParameters[k] = v
+                    }
+                }
+            }
+            val listeners = mutableListOf<Request.Listener>()
+            listeners.addAll(defaultListeners)
+            listeners.addAll(request.listeners)
+
+            val requestNumber = RequestNumber(requestCounter.incrementAndGet())
+
+            val requestMetadata = FakeRequestMetadata(
+                request = request,
+                requestParameters = captureParameters,
+                metadata = metadataParameters,
+                template = request.template ?: defaultTemplate,
+                streams = streamToSurfaceMap,
+                repeating = repeating,
+                requestNumber = requestNumber
+            )
+            requestInfoMap[request] = requestMetadata
+            requestListenerMap[request] = listeners
+        }
+
+        // Copy maps / lists for tests.
+        return RequestSequence(
+            requests = requests.toList(),
+            defaultParameters = defaultParameters.toMap(),
+            requiredParameters = requiredParameters.toMap(),
+            defaultListeners = defaultListeners.toList(),
+            requestMetadata = requestInfoMap,
+            requestListeners = requestListenerMap
+        )
+    }
+
+    fun reset() {
+        synchronized(lock) {
+            requestSequenceQueue.clear()
+            repeatingRequestSequence = null
+            _rejectRequests = false
+        }
+    }
+
+    data class RequestSequence(
+        val requests: List<Request>,
+        val defaultParameters: Map<*, Any?>,
+        val requiredParameters: Map<*, Any?>,
+        val defaultListeners: List<Request.Listener>,
+        val requestMetadata: Map<Request, RequestMetadata>,
+        val requestListeners: Map<Request, List<Request.Listener>>
+    ) {
+        fun invokeOnSequenceCreated() {
+            for (request in requests) {
+                for (listener in requestListeners[request]!!) {
+                    listener.onRequestSequenceCreated(requestMetadata[request]!!)
+                }
+            }
+        }
+
+        fun invokeOnSequenceSubmitted() {
+            for (request in requests) {
+                for (listener in requestListeners[request]!!) {
+                    listener.onRequestSequenceSubmitted(requestMetadata[request]!!)
+                }
+            }
+        }
+
+        fun invokeOnSequenceAborted() {
+            for (request in requests) {
+                for (listener in requestListeners[request]!!) {
+                    listener.onRequestSequenceAborted(requestMetadata[request]!!)
+                }
+            }
+        }
+
+        fun invokeOnSequenceCompleted(frameNumber: FrameNumber) {
+            for (request in requests) {
+                for (listener in requestListeners[request]!!) {
+                    listener.onRequestSequenceCompleted(requestMetadata[request]!!, frameNumber)
+                }
+            }
+        }
+    }
+
+    /**
+     * TODO: It's probably better to model this as a sealed class.
+     */
+    data class Event(
+        val requestSequence: RequestSequence? = null,
+        val rejected: Boolean = false,
+        val abort: Boolean = false,
+        val close: Boolean = false,
+        val stop: Boolean = false,
+        val submit: Boolean = false,
+        val startRepeating: Boolean = false
+    )
+}
diff --git a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunner.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
similarity index 84%
rename from camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunner.kt
rename to camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
index fb9c97d..ddf221c 100644
--- a/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunner.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunner.kt
@@ -21,16 +21,17 @@
 import org.robolectric.internal.bytecode.InstrumentationConfiguration
 
 /**
- * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe.testing] unit tests.
+ * A [RobolectricTestRunner] for [androidx.camera.camera2.pipe] unit tests.
  *
- * It has instrumentation turned off for the [androidx.camera.camera2.pipe.testing] package.
+ * This test runner disables instrumentation for the [androidx.camera.camera2.pipe] and
+ * [androidx.camera.camera2.pipe.testing] packages.
  *
  * Robolectric tries to instrument Kotlin classes, and it throws errors when it encounters
  * companion objects, constructors with default values for parameters, and data classes with
  * inline classes. We don't need shadowing of our classes because we want to use the actual
  * objects in our tests.
  */
-public class CameraPipeRobolectricTestRunner(testClass: Class<*>) :
+public class RobolectricCameraPipeTestRunner(testClass: Class<*>) :
     RobolectricTestRunner(testClass) {
     override fun createClassLoaderConfig(method: FrameworkMethod?): InstrumentationConfiguration {
         val builder = InstrumentationConfiguration.Builder(super.createClassLoaderConfig(method))
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
similarity index 69%
rename from camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
rename to camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
index c11a475..679c18e 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameras.kt
+++ b/camera/camera-camera2-pipe-testing/src/main/java/androidx/camera/camera2/pipe/testing/RobolectricCameras.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
 package androidx.camera.camera2.pipe.testing
 
 import android.Manifest
+import android.annotation.SuppressLint
 import android.app.Application
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
@@ -26,34 +27,21 @@
 import android.hardware.camera2.CameraManager
 import android.os.Handler
 import android.os.Looper
-import android.util.Size
-import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.CameraMetadata
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.CameraStream.Config
-import androidx.camera.camera2.pipe.StreamFormat
-import androidx.camera.camera2.pipe.impl.CameraGraphModules
-import androidx.camera.camera2.pipe.impl.CameraMetadataImpl
-import androidx.camera.camera2.pipe.impl.CameraPipeModules
-import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
-import androidx.camera.camera2.pipe.wrapper.CameraDeviceWrapper
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraMetadata
 import androidx.test.core.app.ApplicationProvider
-import dagger.Module
-import dagger.Provides
 import kotlinx.atomicfu.atomic
 import org.robolectric.Shadows.shadowOf
 import org.robolectric.shadow.api.Shadow
 import org.robolectric.shadows.ShadowApplication
 import org.robolectric.shadows.ShadowCameraCharacteristics
 import org.robolectric.shadows.ShadowCameraManager
-import java.lang.UnsupportedOperationException
-import javax.inject.Singleton
 
 /**
- * Utility class for creating, configuring, and interacting with FakeCamera objects via Robolectric
+ * Utility class for creating, configuring, and interacting with Robolectric's [CameraManager].
  */
-internal object FakeCameras {
+public object RobolectricCameras {
     private val cameraIds = atomic(0)
 
     val application: Application
@@ -64,7 +52,7 @@
             return app
         }
 
-    private val cameraManager: CameraManager
+    val cameraManager: CameraManager
         get() = application.getSystemService(Context.CAMERA_SERVICE) as CameraManager
 
     private val initializedCameraIds = mutableSetOf<CameraId>()
@@ -74,6 +62,7 @@
      * CameraManager, which allows the camera characteristics to be queried for tests and for Fake
      * CameraDevice objects to be created for tests.
      */
+    @Suppress("MissingPermission")
     fun create(
         metadata: Map<CameraCharacteristics.Key<*>, Any> = emptyMap()
     ): CameraId {
@@ -90,8 +79,6 @@
         }
 
         val cameraNumber = cameraIds.incrementAndGet()
-        // Note: It would be better if this was something like "Fake-Camera-1", but robolectric
-        // will not let you open a CameraDevice unless the id is numeric.
         val cameraId = CameraId("FakeCamera-$cameraNumber")
 
         // Add the camera to the camera service
@@ -102,37 +89,41 @@
     }
 
     operator fun get(fakeCameraId: CameraId): CameraCharacteristics {
-        check(initializedCameraIds.contains(fakeCameraId))
+        check(initializedCameraIds.contains(fakeCameraId)) {
+            "CameraId ($fakeCameraId) MUST be created before being accessed!"
+        }
         return cameraManager.getCameraCharacteristics(fakeCameraId.value)
     }
 
+    @SuppressLint("MissingPermission")
     fun open(cameraId: CameraId): FakeCamera {
         check(initializedCameraIds.contains(cameraId))
         val characteristics = cameraManager.getCameraCharacteristics(cameraId.value)
-        val metadata = CameraMetadataImpl(cameraId, false, characteristics, emptyMap())
+        val metadata = AndroidCameraMetadata(cameraId, false, characteristics, emptyMap())
 
+        @Suppress("SyntheticAccessor")
         val callback = CameraStateCallback(cameraId)
         cameraManager.openCamera(
             cameraId.value,
             callback,
             Handler()
         )
+
+        // Wait until the camera is "opened" by robolectric.
         shadowOf(Looper.myLooper()).idle()
-
         val cameraDevice = callback.camera!!
-        val cameraDeviceWrapper = AndroidCameraDevice(metadata, cameraDevice, cameraId)
 
+        @Suppress("SyntheticAccessor")
         return FakeCamera(
             cameraId,
             characteristics,
             metadata,
-            cameraDevice,
-            cameraDeviceWrapper
+            cameraDevice
         )
     }
 
-    /** Remove all fake cameras */
-    fun removeAll() {
+    /** Remove all fake camera instances from Robolectric */
+    fun clear() {
         val shadowCameraManager = Shadow.extract<Any>(
             cameraManager
         ) as ShadowCameraManager
@@ -153,8 +144,7 @@
         val cameraId: CameraId,
         val characteristics: CameraCharacteristics,
         val metadata: CameraMetadata,
-        val cameraDevice: CameraDevice,
-        val cameraDeviceWrapper: CameraDeviceWrapper
+        val cameraDevice: CameraDevice
     )
 
     private class CameraStateCallback(private val cameraId: CameraId) :
@@ -178,33 +168,4 @@
             throw UnsupportedOperationException("onError is not expected for Robolectric Camera")
         }
     }
-
-    /**
-     * Utility module for testing the Dagger generated graph with a a reasonable default config.
-     */
-    @Module(includes = [CameraPipeModules::class])
-    class FakeCameraPipeModule(
-        private val context: Context,
-        private val fakeCamera: FakeCamera
-    ) {
-        @Provides
-        @Singleton
-        fun provideFakeCameraPipeConfig() = CameraPipe.Config(context)
-
-        @Provides
-        @Singleton
-        fun provideFakeGraphConfig(): CameraGraph.Config {
-            val stream = Config.create(
-                Size(640, 480),
-                StreamFormat.YUV_420_888
-            )
-            return CameraGraph.Config(
-                camera = fakeCamera.cameraId,
-                streams = listOf(stream),
-            )
-        }
-    }
-
-    @Module(includes = [CameraGraphModules::class])
-    class FakeCameraGraphModule
 }
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulatorTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulatorTest.kt
new file mode 100644
index 0000000..d4c8550
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraGraphSimulatorTest.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CaptureResult
+import android.os.Build
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.StreamFormat
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.take
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeout
+import kotlinx.coroutines.withTimeoutOrNull
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CameraGraphSimulatorTest {
+    private val metadata = FakeCameraMetadata(
+        mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+    )
+
+    private val streamConfig = CameraStream.Config.create(
+        Size(640, 480),
+        StreamFormat.YUV_420_888
+    )
+
+    private val graphConfig = CameraGraph.Config(
+        camera = metadata.camera,
+        streams = listOf(streamConfig)
+    )
+
+    private val simulator = CameraGraphSimulator(graphConfig, metadata)
+    private val stream = simulator.cameraGraph.streams[streamConfig]!!
+
+    @Test
+    fun simulatorCanSimulateRepeatingFrames() = runBlocking {
+        val listener = FakeRequestListener()
+        val request = Request(
+            streams = listOf(stream.id),
+            listeners = listOf(listener)
+        )
+        simulator.cameraGraph.acquireSession().use {
+            it.startRepeating(request)
+        }
+        simulator.cameraGraph.start()
+
+        val frame = simulator.simulateNextFrame()!!
+
+        assertThat(frame.request).isSameInstanceAs(request)
+        assertThat(frame.frameNumber.value).isGreaterThan(0)
+        assertThat(frame.timestampNanos).isGreaterThan(0)
+
+        val startEvent = listener.onStartedFlow.first()
+        assertThat(startEvent.frameNumber).isNotNull()
+        assertThat(startEvent.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(startEvent.timestamp).isNotNull()
+        assertThat(startEvent.timestamp.value).isGreaterThan(0)
+        assertThat(startEvent.requestMetadata.repeating).isTrue()
+        assertThat(startEvent.requestMetadata.request.streams).contains(stream.id)
+        assertThat(startEvent.requestMetadata.template)
+            .isEqualTo(graphConfig.defaultTemplate)
+
+        val totalCaptureResultEvent = withContext(Dispatchers.IO) {
+            withTimeoutOrNull(timeMillis = 50) {
+                listener.onTotalCaptureResultFlow.first()
+            }
+        }
+
+        assertThat(totalCaptureResultEvent).isNull()
+
+        // Launch the callbacks in a coroutine job to test the behavior of the simulator.
+        val simulateCallbacks = launch {
+            val resultMetadata = mutableMapOf<CaptureResult.Key<*>, Any>()
+            // Simulate two partial capture results, and one total capture result.
+            resultMetadata[CaptureResult.LENS_STATE] = CaptureResult.LENS_STATE_MOVING
+            frame.simulatePartialCaptureResult(resultMetadata)
+            delay(10)
+
+            resultMetadata[CaptureResult.LENS_APERTURE] = 2.0f
+            frame.simulatePartialCaptureResult(resultMetadata)
+            delay(10)
+
+            resultMetadata[CaptureResult.FLASH_STATE] = CaptureResult.FLASH_STATE_FIRED
+            frame.simulateTotalCaptureResult(resultMetadata)
+            delay(10)
+
+            frame.simulateComplete(
+                resultMetadata,
+                extraMetadata = mapOf(
+                    CaptureResult.LENS_APERTURE to 4.0f
+                )
+            )
+        }
+
+        val partialEvent1 = listener.onPartialCaptureResultFlow.first()
+        assertThat(partialEvent1.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(partialEvent1.frameMetadata.camera).isEqualTo(metadata.camera)
+        assertThat(partialEvent1.frameMetadata[CaptureResult.LENS_STATE]).isEqualTo(1)
+        assertThat(partialEvent1.frameMetadata[CaptureResult.LENS_APERTURE]).isNull()
+        assertThat(partialEvent1.frameMetadata[CaptureResult.FLASH_STATE]).isNull()
+
+        val partialEvent2 = listener.onPartialCaptureResultFlow.drop(1).first()
+        assertThat(partialEvent2.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(partialEvent2.frameMetadata.camera).isEqualTo(metadata.camera)
+        assertThat(partialEvent2.frameMetadata[CaptureResult.LENS_STATE]).isEqualTo(1)
+        assertThat(partialEvent2.frameMetadata[CaptureResult.LENS_APERTURE]).isEqualTo(2.0f)
+        assertThat(partialEvent2.frameMetadata[CaptureResult.FLASH_STATE]).isNull()
+
+        val totalEvent = listener.onTotalCaptureResultFlow.first()
+        assertThat(totalEvent.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(totalEvent.frameInfo.camera).isEqualTo(metadata.camera)
+        assertThat(totalEvent.frameInfo.metadata[CaptureResult.LENS_STATE]).isEqualTo(1)
+        assertThat(totalEvent.frameInfo.metadata[CaptureResult.LENS_APERTURE]).isEqualTo(2.0f)
+        assertThat(totalEvent.frameInfo.metadata[CaptureResult.FLASH_STATE]).isEqualTo(3)
+
+        val completedEvent = listener.onCompleteFlow.first()
+        assertThat(completedEvent.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(completedEvent.frameInfo.camera).isEqualTo(metadata.camera)
+        assertThat(completedEvent.frameInfo.metadata[CaptureResult.LENS_STATE]).isEqualTo(1)
+        assertThat(completedEvent.frameInfo.metadata[CaptureResult.LENS_APERTURE]).isEqualTo(
+            4.0f
+        )
+        assertThat(completedEvent.frameInfo.metadata[CaptureResult.FLASH_STATE]).isEqualTo(3)
+
+        simulateCallbacks.join()
+    }
+
+    @Test
+    fun simulatorAbortsRequests() = runBlocking {
+        val listener = FakeRequestListener()
+        val request = Request(
+            streams = listOf(stream.id),
+            listeners = listOf(listener)
+        )
+
+        simulator.cameraGraph.acquireSession().use {
+            it.submit(request = request)
+        }
+        simulator.cameraGraph.close()
+
+        val abortedEvent = listener.onAbortedFlow.first()
+        assertThat(abortedEvent.request).isSameInstanceAs(request)
+    }
+
+    @Test
+    fun simulatorCanIssueBufferLoss() = runBlocking {
+        val listener = FakeRequestListener()
+        val request = Request(
+            streams = listOf(stream.id),
+            listeners = listOf(listener)
+        )
+
+        simulator.cameraGraph.acquireSession().use {
+            it.submit(request = request)
+        }
+        simulator.cameraGraph.start()
+
+        val frame = simulator.simulateNextFrame()!!
+        assertThat(frame.request).isSameInstanceAs(request)
+
+        frame.simulateBufferLoss(stream.id)
+        val lossEvent = listener.onBufferLostFlow.first()
+        assertThat(lossEvent.frameNumber).isEqualTo(frame.frameNumber)
+        assertThat(lossEvent.requestMetadata.request).isSameInstanceAs(request)
+        assertThat(lossEvent.streamId).isEqualTo(stream.id)
+    }
+
+    @Test
+    fun simulatorCanIssueMultipleFrames() = runBlocking {
+        val listener = FakeRequestListener()
+        val request = Request(
+            streams = listOf(stream.id),
+            listeners = listOf(listener)
+        )
+
+        simulator.cameraGraph.acquireSession().use {
+            it.startRepeating(request = request)
+        }
+        simulator.cameraGraph.start()
+
+        val frame1 = simulator.simulateNextFrame()!!
+        val frame2 = simulator.simulateNextFrame()!!
+        val frame3 = simulator.simulateNextFrame()!!
+
+        assertThat(frame1).isNotEqualTo(frame2)
+        assertThat(frame2).isNotEqualTo(frame3)
+        assertThat(frame1.request).isSameInstanceAs(request)
+        assertThat(frame2.request).isSameInstanceAs(request)
+        assertThat(frame3.request).isSameInstanceAs(request)
+
+        val simulateCallbacks = launch {
+            val resultMetadata = mutableMapOf<CaptureResult.Key<*>, Any>()
+            resultMetadata[CaptureResult.LENS_STATE] = CaptureResult.LENS_STATE_MOVING
+            frame1.simulateTotalCaptureResult(resultMetadata)
+            frame1.simulateComplete(resultMetadata)
+
+            delay(15)
+            frame2.simulateTotalCaptureResult(resultMetadata)
+            frame2.simulateComplete(resultMetadata)
+
+            delay(15)
+            resultMetadata[CaptureResult.LENS_STATE] = CaptureResult.LENS_STATE_STATIONARY
+            frame3.simulateTotalCaptureResult(resultMetadata)
+            frame3.simulateComplete(resultMetadata)
+        }
+
+        val startEvents = withTimeout(timeMillis = 50) {
+            listener.onStartedFlow.take(3).toList()
+        }
+        assertThat(startEvents).hasSize(3)
+
+        val event1 = startEvents[0]
+        val event2 = startEvents[1]
+        val event3 = startEvents[2]
+
+        // Frame numbers are not equal
+        assertThat(event1.frameNumber).isNotEqualTo(event2.frameNumber)
+        assertThat(event2.frameNumber).isNotEqualTo(event3.frameNumber)
+
+        // Timestamps are in ascending order
+        assertThat(event3.timestamp.value).isGreaterThan(event2.timestamp.value)
+        assertThat(event2.timestamp.value).isGreaterThan(event1.timestamp.value)
+
+        // Metadata references the same request.
+        assertThat(event1.requestMetadata.repeating).isTrue()
+        assertThat(event2.requestMetadata.repeating).isTrue()
+        assertThat(event3.requestMetadata.repeating).isTrue()
+        assertThat(event1.requestMetadata.request).isSameInstanceAs(request)
+        assertThat(event2.requestMetadata.request).isSameInstanceAs(request)
+        assertThat(event3.requestMetadata.request).isSameInstanceAs(request)
+
+        val completeEvents = withTimeout(timeMillis = 50) {
+            listener.onCompleteFlow.take(3).toList()
+        }
+        assertThat(completeEvents).hasSize(3)
+
+        val completeEvent1 = completeEvents[0]
+        val completeEvent2 = completeEvents[1]
+        val completeEvent3 = completeEvents[2]
+
+        assertThat(completeEvent1.frameNumber).isEqualTo(event1.frameNumber)
+        assertThat(completeEvent2.frameNumber).isEqualTo(event2.frameNumber)
+        assertThat(completeEvent3.frameNumber).isEqualTo(event3.frameNumber)
+
+        assertThat(completeEvent1.frameInfo.metadata[CaptureResult.LENS_STATE])
+            .isEqualTo(CaptureResult.LENS_STATE_MOVING)
+        assertThat(completeEvent2.frameInfo.metadata[CaptureResult.LENS_STATE])
+            .isEqualTo(CaptureResult.LENS_STATE_MOVING)
+        assertThat(completeEvent3.frameInfo.metadata[CaptureResult.LENS_STATE])
+            .isEqualTo(CaptureResult.LENS_STATE_STATIONARY)
+
+        simulateCallbacks.join()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
new file mode 100644
index 0000000..4e3609e
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeCameraDevicesTest.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class FakeCameraDevicesTest {
+    @Test
+    fun cameraMetadataIsNotEqual() {
+        val metadata1 = FakeCameraMetadata(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT)
+        )
+        val metadata2 = FakeCameraMetadata(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+        )
+
+        val cameraDevices = FakeCameraDevices(listOf(metadata1, metadata2))
+
+        assertThat(cameraDevices.findAll()).containsExactlyElementsIn(
+            listOf(
+                metadata1.camera,
+                metadata2.camera
+            )
+        ).inOrder()
+
+        assertThat(cameraDevices.awaitMetadata(metadata1.camera)).isSameInstanceAs(metadata1)
+        assertThat(cameraDevices.awaitMetadata(metadata2.camera)).isSameInstanceAs(metadata2)
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
index 4333589..2e3ce9b 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/FakeMetadataTest.kt
@@ -47,9 +47,22 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class CameraMetadataTest {
+    @Test fun cameraMetadataIsNotEqual() {
+        val metadata1 = FakeCameraMetadata(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT),
+            mapOf(FakeMetadata.TEST_KEY to 42)
+        )
+        val metadata2 = FakeCameraMetadata(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_FRONT),
+            mapOf(FakeMetadata.TEST_KEY to 42)
+        )
+
+        assertThat(metadata1).isNotEqualTo(metadata2)
+        assertThat(metadata1.camera).isNotEqualTo(metadata2.camera)
+    }
 
     @Test
     public fun canRetrieveCameraCharacteristicsOrCameraMetadataViaInterface() {
@@ -66,7 +79,7 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class RequestMetadataTest {
 
@@ -93,10 +106,9 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class FrameMetadataTest {
-
     @Test
     public fun canRetrieveCaptureRequestOrCameraMetadataViaInterface() {
         val metadata = FakeFrameMetadata(
@@ -112,7 +124,7 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class MetadataTransformTest {
     private val metadata = FakeCameraMetadata(
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunnerTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
similarity index 97%
rename from camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunnerTest.kt
rename to camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
index a7229fa..b5f7555 100644
--- a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/CameraPipeRobolectricTestRunnerTest.kt
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCameraPipeTestRunnerTest.kt
@@ -56,7 +56,7 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 public class DataWithInlineClassRobolectricTest {
     @Test
diff --git a/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
new file mode 100644
index 0000000..87f16db
--- /dev/null
+++ b/camera/camera-camera2-pipe-testing/src/test/java/androidx/camera/camera2/pipe/testing/RobolectricCamerasTest.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.testing
+
+import android.content.Context
+import android.hardware.camera2.CameraCharacteristics
+import android.os.Build
+import android.os.Looper
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricCameraPipeTestRunner::class)
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+class RobolectricCamerasTest {
+    private val context = ApplicationProvider.getApplicationContext() as Context
+    private val mainLooper = shadowOf(Looper.getMainLooper())
+
+    @Test
+    fun fakeCamerasCanBeOpened() {
+        val fakeCameraId = RobolectricCameras.create(
+            mapOf(CameraCharacteristics.LENS_FACING to CameraCharacteristics.LENS_FACING_BACK)
+        )
+        val fakeCamera = RobolectricCameras.open(fakeCameraId)
+
+        assertThat(fakeCamera).isNotNull()
+        assertThat(fakeCamera.cameraId).isEqualTo(fakeCameraId)
+        assertThat(fakeCamera.cameraDevice).isNotNull()
+        assertThat(fakeCamera.characteristics).isNotNull()
+        assertThat(fakeCamera.characteristics[CameraCharacteristics.LENS_FACING]).isNotNull()
+        assertThat(fakeCamera.metadata).isNotNull()
+        assertThat(fakeCamera.metadata[CameraCharacteristics.LENS_FACING]).isNotNull()
+    }
+
+    @After
+    fun teardown() {
+        mainLooper.idle()
+        RobolectricCameras.clear()
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
index 865e8c0..6a1b5d5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraGraph.kt
@@ -17,29 +17,38 @@
 package androidx.camera.camera2.pipe
 
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession
+import android.hardware.camera2.params.SessionConfiguration
 import android.hardware.camera2.params.MeteringRectangle
 import android.view.Surface
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_MS
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
 import kotlinx.coroutines.Deferred
 import java.io.Closeable
 
 /**
- * A CameraGraph represents the combined configuration and state of a camera.
+ * A [CameraGraph] represents the combined configuration and state of a camera.
+ *
+ *
  */
 public interface CameraGraph : Closeable {
     public val streams: StreamGraph
 
     /**
-     * This will cause the CameraGraph to start opening the camera and configuring the Camera2
-     * CaptureSession. While the CameraGraph is started it will attempt to keep the camera alive,
-     * active, and in a configured running state.
+     * This will cause the [CameraGraph] to start opening the [CameraDevice] and configuring a
+     * [CameraCaptureSession]. While the CameraGraph is alive it will attempt to keep the camera
+     * open, active, and in a configured running state.
      */
     public fun start()
 
     /**
-     * This will cause the CameraGraph to stop executing requests and close the current Camera2
-     * CaptureSession (if one is active). The current repeating request is preserved, and any
-     * call to submit a request to a session will be enqueued. To prevent requests from being
-     * enqueued, close the CameraGraph.
+     * This will cause the [CameraGraph] to stop executing requests and close the current Camera2
+     * [CameraCaptureSession] (if one is active). The most recent repeating request will be
+     * preserved, and any calls to submit a request to a session will be enqueued. To stop
+     * requests from being enqueued, close the [CameraGraph].
      */
     public fun stop()
 
@@ -62,7 +71,25 @@
     public fun setSurface(stream: StreamId, surface: Surface?)
 
     /**
-     * This defines the configuration, flags, and pre-defined structure of a CameraGraph instance.
+     * This defines the configuration, flags, and pre-defined structure of a [CameraGraph] instance.
+     *
+     * @param camera The Camera2 [CameraId] that this [CameraGraph] represents.
+     * @param streams A list of [CameraStream]s to use when building the configuration.
+     * @param streamSharingGroups A list of [CameraStream]s to apply buffer sharing to.
+     * @param input An input configuration to support Camera2 Reprocessing.
+     * @param sessionTemplate The template id to use when creating the [CaptureRequest] to supply
+     *   the default parameters for a [SessionConfiguration] object.
+     * @param sessionParameters the extra parameters to apply to the [CaptureRequest] used to supply
+     *   the default parameters for a [SessionConfiguration] object. These parameters are *only*
+     *   used to create the [CaptureRequest] for session configuration. Use [defaultParameters] or
+     *   [requiredParameters] to enforce that the key is set for every request.
+     * @param sessionMode defines the [OperatingMode] of the session. May be used to configure a
+     *   [CameraConstrainedHighSpeedCaptureSession] for slow motion capture (If available)
+     * @param defaultTemplate The default template to be used if a [Request] does not specify one.
+     * @param defaultParameters The default parameters to be used for a [Request].
+     * @param defaultListeners A default set of listeners that will be added to every [Request].
+     * @param requiredParameters Are will override any other configured parameter, and can be used
+     *   to enforce that specific keys are always set to specific value for every [CaptureRequest].
      */
     public data class Config(
         val camera: CameraId,
@@ -75,9 +102,10 @@
         val defaultTemplate: RequestTemplate = RequestTemplate(1),
         val defaultParameters: Map<*, Any> = emptyMap<Any, Any>(),
         val defaultListeners: List<Request.Listener> = listOf(),
+        val requiredParameters: Map<*, Any> = emptyMap<Any, Any>(),
+
         val metadataTransform: MetadataTransform = MetadataTransform(),
         val flags: Flags = Flags()
-
         // TODO: Internal error handling. May be better at the CameraPipe level.
     )
 
@@ -97,7 +125,7 @@
         HIGH_SPEED,
     }
 
-    public companion object Constants3A {
+    public object Constants3A {
         // Constants related to controlling the time or frame budget a 3A operation should get.
         public const val DEFAULT_FRAME_LIMIT: Int = 60
         public const val DEFAULT_TIME_LIMIT_MS: Int = 3_000
@@ -121,14 +149,20 @@
     }
 
     /**
-     * A lock on CameraGraph. It facilitates an exclusive access to the managed camera device. Once
-     * this is acquired, a well ordered set of requests can be sent to the camera device without the
-     * possibility of being intermixed with any other request to the camera from non lock holders.
+     * A [Session] is an interactive lock for [CameraGraph] and allows state to be changed.
+     *
+     * Holding this object prevents other systems from acquiring a [Session] until the currently
+     * held session is released. Because of it's exclusive nature, [Session]s are intended for
+     * fast, short-lived state updates, or for interactive capture sequences that must not be
+     * altered. (Flash photo sequences, for example).
+     *
+     * While this object is thread-safe, it should not shared or held for long periods of time.
+     * Example: A [Session] should *not* be held during video recording.
      */
     public interface Session : Closeable {
         public fun submit(request: Request)
         public fun submit(requests: List<Request>)
-        public fun setRepeating(request: Request)
+        public fun startRepeating(request: Request)
 
         /**
          * Abort in-flight requests. This will abort *all* requests in the current
@@ -137,6 +171,11 @@
         public fun abort()
 
         /**
+         * Stop the current repeating request.
+         */
+        public fun stopRepeating()
+
+        /**
          * Applies the given 3A parameters to the camera device.
          *
          * @return earliest FrameNumber at which the parameters were successfully applied.
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
index 2668286..48d69f4 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraPipe.kt
@@ -22,6 +22,10 @@
 import androidx.camera.camera2.pipe.impl.CameraPipeComponent
 import androidx.camera.camera2.pipe.impl.CameraPipeConfigModule
 import androidx.camera.camera2.pipe.impl.DaggerCameraPipeComponent
+import androidx.camera.camera2.pipe.impl.DaggerExternalCameraPipeComponent
+import androidx.camera.camera2.pipe.impl.ExternalCameraGraphComponent
+import androidx.camera.camera2.pipe.impl.ExternalCameraGraphConfigModule
+import androidx.camera.camera2.pipe.impl.ExternalCameraPipeComponent
 import kotlinx.atomicfu.atomic
 
 internal val cameraPipeIds = atomic(0)
@@ -69,4 +73,37 @@
     )
 
     override fun toString(): String = "CameraPipe-$debugId"
+
+    /**
+     * External may be used if the underlying implementation needs to delegate to another library
+     * or system.
+     */
+    class External {
+        private val component: ExternalCameraPipeComponent = DaggerExternalCameraPipeComponent
+            .builder()
+            .build()
+
+        /**
+         * This creates a new [CameraGraph] instance that is configured to use an externally
+         * defined [RequestProcessor].
+         *
+         * TODO: Consider changing cameraDevices to be a single device + physical metadata.
+         */
+        public fun create(
+            config: CameraGraph.Config,
+            cameraDevices: CameraDevices,
+            requestProcessor: RequestProcessor
+        ): CameraGraph {
+            val componentBuilder = component.cameraGraphBuilder()
+            val component: ExternalCameraGraphComponent = componentBuilder
+                .externalCameraGraphConfigModule(
+                    ExternalCameraGraphConfigModule(
+                        config,
+                        cameraDevices,
+                        requestProcessor
+                    )
+                ).build()
+            return component.cameraGraph()
+        }
+    }
 }
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 5272007..9617d2b 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
@@ -234,7 +234,7 @@
 /**
  * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
  */
-public fun CaptureRequest.Builder.writeParameters(parameters: Map<*, Any>) {
+public fun CaptureRequest.Builder.writeParameters(parameters: Map<*, Any?>) {
     for ((key, value) in parameters) {
         writeParameter(key, value)
     }
@@ -243,7 +243,7 @@
 /**
  * Utility function to help deal with the unsafe nature of the typed Key/Value pairs.
  */
-public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any) {
+public fun CaptureRequest.Builder.writeParameter(key: Any?, value: Any?) {
     if (key != null && key is CaptureRequest.Key<*>) {
         @Suppress("UNCHECKED_CAST")
         this.set(key as CaptureRequest.Key<Any>, value)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
index 0e150b0..5a2c84b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/Request.kt
@@ -18,12 +18,25 @@
 
 import android.hardware.camera2.CaptureFailure
 import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
 
 /**
- * An immutable package of settings and outputs needed to capture a single image from the camera
- * device. The exact set of keys and surfaces used in the CaptureRequest builder may be different
- * from what is specified in a request depending on how the request was submitted and on the
- * state of the camera.
+ * A [Request] is an immutable package of outputs and parameters needed to issue a [CaptureRequest]
+ * to a Camera2 [CameraCaptureSession].
+ *
+ * [Request] objects are handled by camera2 via the [RequestProcessor] interface, and will
+ * translate each [Request] object into a corresponding [CaptureRequest] object using the active
+ * [CameraDevice], [CameraCaptureSession], and [CameraGraph.Config]. Requests may be queued up and
+ * submitted after a delay, or reused (in the case of repeating requests) if the
+ * [CameraCaptureSession] is reconfigured or recreated.
+ *
+ * Depending on the [CameraGraph.Config], it is possible that not all parameters that are set on
+ * the [Request] will be honored when a [Request] is sent to the camera. Specifically, Camera2
+ * parameters related to 3A State and any required parameters specified on the [CameraGraph.Config]
+ * will override parameters specified in a [Request]
+ *
+ * @param streams The list of streams to submit. Each request *must* have 1 or more valid streams.
  */
 public data class Request(
     val streams: List<StreamId>,
@@ -34,8 +47,11 @@
 ) {
 
     /**
-     * This listener is used to observe the state and progress of requests that are submitted to the
-     * [CameraGraph] and can be attached to individual requests.
+     * This listener is used to observe the state and progress of a [Request] that has been issued
+     * to the [CameraGraph]. Listeners will be invoked on background threads at high speed, and
+     * should avoid blocking work or accessing synchronized resources if possible. [Listener]s used
+     * in a repeating request may be issued multiple times within the same session, and should not
+     * rely on [onRequestSequenceSubmitted] from being invoked only once.
      */
     public interface Listener {
         /**
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt
new file mode 100644
index 0000000..fb4328f
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/RequestProcessor.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe
+
+/**
+ * An instance of a [RequestProcessor] exists for the duration of a CameraCaptureSession and must be
+ * created for each new CameraCaptureSession. It is responsible for low level interactions with the
+ * CameraCaptureSession and for shimming the interfaces and callbacks to make them easier to work
+ * with.
+ *
+ * There are some important design considerations:
+ * - Instances class is not thread safe, and must be synchronized outside of this class.
+ * - Implementations can and will assume that calls will not overlap across threads.
+ * - Implementations must take special care to reduce the number objects and wrappers that are
+ *   created, and to reduce the number of loops and overhead in wrapper objects. Its better to do
+ *   work during initialization than during a callback.
+ * - Implementations are expected to interact directly, and synchronously, with the Camera.
+ * - Callbacks are expected to be invoked at *very* high frequency.
+ * - One RequestProcessor instance per CameraCaptureSession
+ */
+interface RequestProcessor {
+
+    /**
+     * Set the repeating [Request] with an optional set of parameters and listeners. Parameters are
+     * applied, in order, to each request in the list:
+     *
+     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     *
+     * Parameters where values are set to null will physically remove a particular key from the
+     * final map of parameters.
+     *
+     * @param request the [Request] to submit to the camera.
+     * @param defaultParameters will not override parameters specified in the [Request].
+     * @param requiredParameters will override parameters specified in the [Request].
+     * @param defaultListeners are internal and/or global listeners that should be invoked in
+     * addition to listeners that are specified on each [Request]
+     * @return false if the repeating request was not successfully updated.
+     */
+    fun startRepeating(
+        request: Request,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean
+
+    /**
+     * Stops the current repeating request, but does *not* close the session. The current
+     * repeating request can be resumed by invoking [startRepeating] again.
+     */
+    fun stopRepeating()
+
+    /**
+     * Submit a single [Request] with optional sets of parameters and listeners. Parameters are
+     * applied, in order, to each request in the list:
+     *
+     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     *
+     * Parameters where values are set to null will physically remove a particular key from the
+     * final map of parameters.
+     *
+     * @param request the requests to submit to the camera.
+     * @param defaultParameters will not override parameters specified in the [Request].
+     * @param requiredParameters will override parameters specified in the [Request].
+     * @param defaultListeners are internal and/or global listeners that should be invoked in
+     * addition to listeners that are specified on each [Request]
+     * @return false if this request was not submitted to the camera for any reason.
+     */
+    fun submit(
+        request: Request,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean
+
+    /**
+     * Submit a list of [Request]s with optional sets of parameters and listeners. Parameters are
+     * applied, in order, to each request in the list:
+     *
+     *   [Request.template] -> defaultParameters -> [Request.parameters] -> requiredParameters
+     *
+     * Parameters where values are set to null will physically remove a particular key from the
+     * final map of parameters.
+     *
+     * @param requests the requests to submit to the camera.
+     * @param defaultParameters will not override parameters specified in the [Request].
+     * @param requiredParameters will override parameters specified in the [Request].
+     * @param defaultListeners are internal and/or global listeners that should be invoked in
+     * addition to listeners that are specified on each [Request]
+     * @return false if these requests were not submitted to the camera for any reason.
+     */
+    fun submit(
+        requests: List<Request>,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
+    ): Boolean
+
+    /**
+     * Abort requests that have been submitted but not completed.
+     */
+    fun abortCaptures()
+
+    /**
+     * Puts the RequestProcessor into a closed state where it should immediately reject all
+     * incoming requests. This should NOT call stopRepeating() or abortCaptures().
+     */
+    fun close()
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CameraController.kt
similarity index 90%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CameraController.kt
index c59b50f..123cfb5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CameraController.kt
@@ -22,12 +22,6 @@
 import kotlinx.coroutines.launch
 import javax.inject.Inject
 
-internal interface GraphState {
-    fun start()
-    fun stop()
-    fun reconfigure()
-}
-
 /**
  * This represents the core state loop for a Camera Graph instance.
  *
@@ -38,15 +32,15 @@
  * TODO: Reorganize these constructor parameters.
  */
 @CameraGraphScope
-internal class GraphStateImpl @Inject constructor(
+internal class Camera2CameraController @Inject constructor(
     @ForCameraGraph private val scope: CoroutineScope,
     private val config: CameraGraph.Config,
-    private val graphProcessor: GraphProcessor,
+    private val graphListener: GraphController.GraphListener,
     private val sessionFactory: SessionFactory,
-    private val requestProcessorFactory: RequestProcessorFactory,
+    private val requestProcessorFactory: Camera2RequestProcessorFactory,
     private val virtualCameraManager: VirtualCameraManager,
     private val streamGraph: StreamGraphImpl
-) : GraphState {
+) : GraphController {
     private var currentCamera: VirtualCamera? = null
     private var currentSession: VirtualSessionState? = null
 
@@ -61,7 +55,7 @@
 
             currentCamera = camera
             currentSession = VirtualSessionState(
-                graphProcessor,
+                graphListener,
                 sessionFactory,
                 requestProcessorFactory,
                 scope
@@ -87,7 +81,7 @@
         }
     }
 
-    override fun reconfigure() {
+    override fun restart() {
         val oldSession: VirtualSessionState?
         val newSession: VirtualSessionState?
 
@@ -96,7 +90,7 @@
 
             oldSession = currentSession
             newSession = VirtualSessionState(
-                graphProcessor,
+                graphListener,
                 sessionFactory,
                 requestProcessorFactory,
                 scope
@@ -130,4 +124,4 @@
             }
         }
     }
-}
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CaptureSequence.kt
similarity index 97%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CaptureSequence.kt
index 8dde9ab..89e0d03 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CaptureSequence.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2CaptureSequence.kt
@@ -32,15 +32,15 @@
 
 /**
  * This class responds to events from a set of one or more requests. It uses the tag field on
- * a CaptureRequest object to lookup and invoke per-request listeners so that a listener can be
+ * a [CaptureRequest] object to lookup and invoke per-request listeners so that a listener can be
  * defined on a specific request within a burst.
  */
-internal class CaptureSequence(
+internal class Camera2CaptureSequence(
     private val internalListeners: List<Request.Listener>,
     private val requests: Map<RequestNumber, RequestInfo>,
     private val captureRequests: List<CaptureRequest>,
     private val surfaceMap: Map<Surface, StreamId>,
-    private val inFlightRequests: MutableList<CaptureSequence>,
+    private val inFlightRequests: MutableList<Camera2CaptureSequence>,
     private val camera: CameraId
 ) : CameraCaptureSession.CaptureCallback() {
     private val debugId = requestSequenceDebugIds.incrementAndGet()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2RequestProcessor.kt
similarity index 86%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2RequestProcessor.kt
index 66cca5f..12fadb1 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/StandardRequestProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Camera2RequestProcessor.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.RequestMetadata
 import androidx.camera.camera2.pipe.RequestNumber
+import androidx.camera.camera2.pipe.RequestProcessor
 import androidx.camera.camera2.pipe.RequestTemplate
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.core.Log
@@ -37,30 +38,27 @@
 import java.util.Collections.singletonMap
 import javax.inject.Inject
 
-internal interface RequestProcessorFactory {
+internal interface Camera2RequestProcessorFactory {
     fun create(
         session: CameraCaptureSessionWrapper,
         surfaceMap: Map<StreamId, Surface>
     ): RequestProcessor
 }
 
-internal class StandardRequestProcessorFactory @Inject constructor(
+internal class StandardCamera2RequestProcessorFactory @Inject constructor(
     private val threads: Threads,
     private val graphConfig: CameraGraph.Config,
-    @ForCameraGraph private val graphListeners: ArrayList<Request.Listener>,
-    private val graphState3A: GraphState3A
-) : RequestProcessorFactory {
+) : Camera2RequestProcessorFactory {
     override fun create(
         session: CameraCaptureSessionWrapper,
         surfaceMap: Map<StreamId, Surface>
     ): RequestProcessor =
-        StandardRequestProcessor(
+        @Suppress("SyntheticAccessor")
+        Camera2RequestProcessor(
             session,
             threads,
-            graphConfig,
-            surfaceMap,
-            graphListeners,
-            graphState3A
+            graphConfig.defaultTemplate,
+            surfaceMap
         )
 }
 
@@ -70,60 +68,61 @@
 internal fun nextRequestTag(): RequestNumber = RequestNumber(requestTags.incrementAndGet())
 
 /**
- * This class is designed to synchronously handle interactions with the Camera CaptureSession.
+ * This class is designed to synchronously handle interactions with a [CameraCaptureSessionWrapper].
  */
-internal class StandardRequestProcessor(
+internal class Camera2RequestProcessor(
     private val session: CameraCaptureSessionWrapper,
     private val threads: Threads,
-    private val graphConfig: CameraGraph.Config,
-    private val surfaceMap: Map<StreamId, Surface>,
-    private val graphListeners: List<Request.Listener>,
-    private val graphState3A: GraphState3A
+    private val template: RequestTemplate,
+    private val surfaceMap: Map<StreamId, Surface>
 ) : RequestProcessor {
 
     @GuardedBy("inFlightRequests")
-    private val inFlightRequests = mutableListOf<CaptureSequence>()
+    private val inFlightRequests = mutableListOf<Camera2CaptureSequence>()
     private val debugId = requestProcessorDebugIds.incrementAndGet()
     private val closed = atomic(false)
 
     override fun submit(
         request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
     ): Boolean {
         return configureAndCapture(
             singletonList(request),
             defaultParameters,
             requiredParameters,
-            requireStreams = false,
+            defaultListeners,
             isRepeating = false
         )
     }
 
     override fun submit(
         requests: List<Request>,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
     ): Boolean {
         return configureAndCapture(
             requests,
             defaultParameters,
             requiredParameters,
-            requireStreams = false,
+            defaultListeners,
             isRepeating = false
         )
     }
 
-    override fun setRepeating(
+    override fun startRepeating(
         request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>
     ): Boolean {
         return configureAndCapture(
             singletonList(request),
             defaultParameters,
             requiredParameters,
-            requireStreams = false,
+            defaultListeners,
             isRepeating = true
         )
     }
@@ -150,9 +149,9 @@
 
     private fun configureAndCapture(
         requests: List<Request>,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>,
-        requireStreams: Boolean,
+        defaultParameters: Map<*, Any?>,
+        requiredParameters: Map<*, Any?>,
+        defaultListeners: List<Request.Listener>,
         isRepeating: Boolean
     ): Boolean {
         // Reject incoming requests if this instance has been stopped or closed.
@@ -167,7 +166,7 @@
         val streamToSurfaceMap = ArrayMap<StreamId, Surface>()
 
         for (request in requests) {
-            val requestTemplate = request.template ?: graphConfig.defaultTemplate
+            val requestTemplate = request.template ?: template
 
             Log.debug { "Building CaptureRequest for $request" }
 
@@ -188,7 +187,7 @@
                     surfaceToStreamMap[surface] = stream
                     streamToSurfaceMap[stream] = surface
                     hasSurface = true
-                } else if (requireStreams) {
+                } else {
                     Log.info { "  Failed to bind surface to $stream" }
                     // If requireStreams is set we are required to map every stream to a valid
                     // Surface object for this request. If this condition is violated, then we
@@ -229,14 +228,14 @@
             // surface per request.
             check(hasSurface)
 
-            // Apply default parameters to the request builder first.
+            // Apply default parameters to the builder first.
             requestBuilder.writeParameters(defaultParameters)
 
-            // Apply request parameters to the request builder.
+            // Apply request parameters to the builder.
             requestBuilder.writeParameters(request.parameters)
 
-            // Apply the 3A parameters. This gives the users of camerapipe the ability to
-            // still override the 3A parameters for complicated use cases.
+            // Finally, write required parameters to the request builder. This will override any
+            // value that has ben previously set.
             //
             // TODO(sushilnath@): Implement one of the two options. (1) Apply the 3A parameters
             // from internal 3A state machine at last and provide a flag in the Request object to
@@ -244,10 +243,6 @@
             // directly. Add code to handle the flag. (2) Let clients override the 3A parameters
             // freely and when that happens intercept those parameters from the request and keep the
             // internal 3A state machine in sync.
-            graphState3A.writeTo(requestBuilder)
-
-            // Finally, write required parameters to the request builder. This will override any
-            // value that has ben previously set.
             requestBuilder.writeParameters(requiredParameters)
 
             // The tag must be set for every request. We use it to lookup listeners for the
@@ -274,8 +269,8 @@
 
         // Create the captureSequence listener
         @Suppress("SyntheticAccessor")
-        val captureSequence = CaptureSequence(
-            graphListeners,
+        val captureSequence = Camera2CaptureSequence(
+            defaultListeners,
             if (requests.size == 1) {
                 singletonMap(requestMap.keyAt(0), requestMap.valueAt(0))
             } else {
@@ -316,7 +311,7 @@
 
     private fun capture(
         captureRequests: List<CaptureRequest>,
-        captureSequence: CaptureSequence,
+        captureSequence: Camera2CaptureSequence,
         isRepeating: Boolean
     ) {
         captureSequence.invokeOnRequestSequenceCreated()
@@ -367,8 +362,8 @@
 @Suppress("SyntheticAccessor") // Using an inline class generates a synthetic constructor
 internal class RequestInfo(
     private val captureRequest: CaptureRequest,
-    private val defaultParameters: Map<*, Any>,
-    private val requiredParameters: Map<*, Any>,
+    private val defaultParameters: Map<*, Any?>,
+    private val requiredParameters: Map<*, Any?>,
     override val streams: Map<StreamId, Surface>,
     override val template: RequestTemplate,
     override val repeating: Boolean,
@@ -380,8 +375,17 @@
         get(key) ?: default
 
     @Suppress("UNCHECKED_CAST")
-    override fun <T> get(key: Metadata.Key<T>): T? =
-        (requiredParameters[key] ?: request.extras[key] ?: defaultParameters[key]) as T?
+    override fun <T> get(key: Metadata.Key<T>): T? = when {
+        requiredParameters.containsKey(key) -> {
+            requiredParameters[key] as T?
+        }
+        request.extras.containsKey(key) -> {
+            request.extras[key] as T?
+        }
+        else -> {
+            defaultParameters[key] as T?
+        }
+    }
 
     override fun <T> getOrDefault(key: Metadata.Key<T>, default: T): T = get(key) ?: default
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
index a024cb3..fc4896a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphComponent.kt
@@ -16,9 +16,11 @@
 
 package androidx.camera.camera2.pipe.impl
 
+import androidx.camera.camera2.pipe.CameraDevices
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraMetadata
 import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.impl.GraphController.GraphListener
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
@@ -38,7 +40,8 @@
 @Subcomponent(
     modules = [
         CameraGraphModules::class,
-        CameraGraphConfigModule::class
+        CameraGraphConfigModule::class,
+        Camera2CameraGraphModules::class,
     ]
 )
 internal interface CameraGraphComponent {
@@ -57,11 +60,7 @@
     fun provideCameraGraphConfig(): CameraGraph.Config = config
 }
 
-@Module(
-    includes = [
-        SessionFactoryModule::class
-    ]
-)
+@Module
 internal abstract class CameraGraphModules {
     @Binds
     abstract fun bindCameraGraph(cameraGraph: CameraGraphImpl): CameraGraph
@@ -70,12 +69,7 @@
     abstract fun bindGraphProcessor(graphProcessor: GraphProcessorImpl): GraphProcessor
 
     @Binds
-    abstract fun bindRequestProcessorFactory(
-        factory: StandardRequestProcessorFactory
-    ): RequestProcessorFactory
-
-    @Binds
-    abstract fun bindGraphState(graphState: GraphStateImpl): GraphState
+    abstract fun bindGraphListener(graphProcessor: GraphProcessorImpl): GraphListener
 
     companion object {
         @CameraGraphScope
@@ -88,9 +82,9 @@
         @Provides
         fun provideCameraMetadata(
             graphConfig: CameraGraph.Config,
-            cache: CameraMetadataCache
+            cameraDevices: CameraDevices
         ): CameraMetadata {
-            return cache.awaitMetadata(graphConfig.camera)
+            return cameraDevices.awaitMetadata(graphConfig.camera)
         }
 
         @CameraGraphScope
@@ -112,3 +106,18 @@
         }
     }
 }
+
+@Module(
+    includes = [
+        SessionFactoryModule::class
+    ]
+)
+internal abstract class Camera2CameraGraphModules {
+    @Binds
+    abstract fun bindRequestProcessorFactory(
+        factoryStandard: StandardCamera2RequestProcessorFactory
+    ): Camera2RequestProcessorFactory
+
+    @Binds
+    abstract fun bindGraphState(camera2CameraState: Camera2CameraController): GraphController
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
index 4d7f06d..b01fbb5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphImpl.kt
@@ -34,7 +34,7 @@
     metadata: CameraMetadata,
     private val graphProcessor: GraphProcessor,
     private val streamGraph: StreamGraphImpl,
-    private val graphState: GraphState,
+    private val graphController: GraphController,
     private val graphState3A: GraphState3A,
     private val listener3A: Listener3A
 ) : CameraGraph {
@@ -47,7 +47,9 @@
 
     init {
         // Log out the configuration of the camera graph when it is created.
-        Debug.formatCameraGraphProperties(metadata, graphConfig, this)
+        Log.info {
+            Debug.formatCameraGraphProperties(metadata, graphConfig, this)
+        }
     }
 
     override val streams: StreamGraph
@@ -56,14 +58,14 @@
     override fun start() {
         Debug.traceStart { "$this#start" }
         Log.info { "Starting $this" }
-        graphState.start()
+        graphController.start()
         Debug.traceStop()
     }
 
     override fun stop() {
         Debug.traceStart { "$this#stop" }
         Log.info { "Stopping $this" }
-        graphState.stop()
+        graphController.stop()
         Debug.traceStop()
     }
 
@@ -94,7 +96,7 @@
         Log.info { "Closing $this" }
         sessionLock.close()
         graphProcessor.close()
-        graphState.stop()
+        graphController.stop()
         Debug.traceStop()
     }
 
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
index 0b632a3..63033c94 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraGraphSessionImpl.kt
@@ -46,14 +46,18 @@
         graphProcessor.submit(requests)
     }
 
-    override fun setRepeating(request: Request) {
-        graphProcessor.setRepeating(request)
+    override fun startRepeating(request: Request) {
+        graphProcessor.startRepeating(request)
     }
 
     override fun abort() {
         graphProcessor.abort()
     }
 
+    override fun stopRepeating() {
+        graphProcessor.stopRepeating()
+    }
+
     override fun close() {
         // Release the token so that a new instance of session can be created.
         token.release()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
index f1b51b7..aa563da 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataCache.kt
@@ -26,6 +26,7 @@
 import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.core.Timestamps
 import androidx.camera.camera2.pipe.core.Timestamps.formatMs
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraMetadata
 import kotlinx.coroutines.withContext
 import java.lang.IllegalStateException
 import javax.inject.Inject
@@ -76,7 +77,7 @@
         }
     }
 
-    private fun createCameraMetadata(cameraId: CameraId, redacted: Boolean): CameraMetadataImpl {
+    private fun createCameraMetadata(cameraId: CameraId, redacted: Boolean): AndroidCameraMetadata {
         val start = Timestamps.now()
 
         return Debug.trace("CameraCharacteristics_$cameraId") {
@@ -86,7 +87,7 @@
                 val characteristics =
                     cameraManager.getCameraCharacteristics(cameraId.value)
                 val cameraMetadata =
-                    CameraMetadataImpl(cameraId, redacted, characteristics, emptyMap())
+                    AndroidCameraMetadata(cameraId, redacted, characteristics, emptyMap())
 
                 Log.info {
                     val duration = Timestamps.now() - start
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
index e5f794d..a517ff4 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraPipeComponent.kt
@@ -21,8 +21,8 @@
 import android.os.Handler
 import android.os.HandlerThread
 import android.os.Process
-import androidx.camera.camera2.pipe.CameraPipe
 import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.CameraPipe
 import dagger.Binds
 import dagger.Component
 import dagger.Module
@@ -37,13 +37,14 @@
 import javax.inject.Singleton
 
 @Qualifier
-internal annotation class ForCameraPipe
+internal annotation class ForCameraThread
 
 @Singleton
 @Component(
     modules = [
         CameraPipeModules::class,
-        CameraPipeConfigModule::class
+        CameraPipeConfigModule::class,
+        Camera2CameraPipeModules::class,
     ]
 )
 internal interface CameraPipeComponent {
@@ -61,22 +62,10 @@
 
 @Module
 internal abstract class CameraPipeModules {
-    @Binds
-    abstract fun bindCameras(impl: CameraDevicesImpl): CameraDevices
-
     companion object {
-        @Provides
-        fun provideContext(config: CameraPipe.Config): Context = config.appContext
-
-        @Reusable
-        @Provides
-        fun provideCameraManager(context: Context): CameraManager =
-            context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-
         @Singleton
         @Provides
-        fun provideCameraPipeThreads(config: CameraPipe.Config): Threads {
-
+        fun provideCameraPipeThreads(@ForCameraThread cameraThread: HandlerThread?): Threads {
             val threadIds = atomic(0)
             val cameraThreadPriority =
                 Process.THREAD_PRIORITY_DISPLAY + Process.THREAD_PRIORITY_LESS_FAVORABLE
@@ -106,7 +95,7 @@
 
             val cameraHandlerFn =
                 {
-                    config.cameraThread?.let { Handler(it.looper) }
+                    cameraThread?.let { Handler(it.looper) }
                         ?: Handler(
                             HandlerThread("CXCP-Camera2-H").also {
                                 it.start()
@@ -129,10 +118,7 @@
             }
 
             val globalScope = CoroutineScope(
-                defaultDispatcher.plus(
-                    CoroutineName
-                    ("CXCP-Pipe")
-                )
+                defaultDispatcher.plus(CoroutineName("CXCP-Pipe"))
             )
 
             return Threads(
@@ -146,4 +132,24 @@
             )
         }
     }
-}
\ No newline at end of file
+}
+
+@Module
+internal abstract class Camera2CameraPipeModules {
+    @Binds
+    abstract fun bindCameras(impl: CameraDevicesImpl): CameraDevices
+
+    companion object {
+        @Provides
+        fun provideContext(config: CameraPipe.Config): Context = config.appContext
+
+        @Provides
+        @ForCameraThread
+        fun provideCameraThread(config: CameraPipe.Config): HandlerThread? = config.cameraThread
+
+        @Reusable
+        @Provides
+        fun provideCameraManager(context: Context): CameraManager =
+            context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
index 3133d88..bb220fe 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/Controller3A.kt
@@ -17,12 +17,12 @@
 package androidx.camera.camera2.pipe.impl
 
 import android.hardware.camera2.CameraMetadata.CONTROL_AE_PRECAPTURE_TRIGGER_START
-import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
 import android.hardware.camera2.CaptureRequest
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_LOCK
 import android.hardware.camera2.CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER
 import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+import android.hardware.camera2.CaptureRequest.CONTROL_AF_TRIGGER_START
 import android.hardware.camera2.CaptureResult
 import android.hardware.camera2.params.MeteringRectangle
 import android.os.Build
@@ -31,7 +31,8 @@
 import androidx.camera.camera2.pipe.AeMode
 import androidx.camera.camera2.pipe.AfMode
 import androidx.camera.camera2.pipe.AwbMode
-import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_FRAME_LIMIT
+import androidx.camera.camera2.pipe.CameraGraph.Constants3A.DEFAULT_TIME_LIMIT_NS
 import androidx.camera.camera2.pipe.CameraGraph.Constants3A.FRAME_NUMBER_INVALID
 import androidx.camera.camera2.pipe.FlashMode
 import androidx.camera.camera2.pipe.Lock3ABehavior
@@ -230,8 +231,8 @@
         aeLockBehavior: Lock3ABehavior? = null,
         afLockBehavior: Lock3ABehavior? = null,
         awbLockBehavior: Lock3ABehavior? = null,
-        frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
-        timeLimitNs: Long? = CameraGraph.DEFAULT_TIME_LIMIT_NS
+        frameLimit: Int = DEFAULT_FRAME_LIMIT,
+        timeLimitNs: Long? = DEFAULT_TIME_LIMIT_NS
     ): Deferred<Result3A> {
         // If we explicitly need to unlock af first before proceeding to lock it, we need to send
         // a single request with TRIGGER = TRIGGER_CANCEL so that af can start a fresh scan.
@@ -345,8 +346,8 @@
     }
 
     suspend fun lock3AForCapture(
-        frameLimit: Int = CameraGraph.DEFAULT_FRAME_LIMIT,
-        timeLimitNs: Long = CameraGraph.DEFAULT_TIME_LIMIT_NS
+        frameLimit: Int = DEFAULT_FRAME_LIMIT,
+        timeLimitNs: Long = DEFAULT_TIME_LIMIT_NS
     ): Deferred<Result3A> {
         val listener = Result3AStateListenerImpl(
             mapOf<CaptureResult.Key<*>, List<Any>>(
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraGraphComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraGraphComponent.kt
new file mode 100644
index 0000000..0aba351
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraGraphComponent.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import androidx.camera.camera2.pipe.CameraDevices
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.RequestProcessor
+import androidx.camera.camera2.pipe.impl.GraphController.GraphListener
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import kotlinx.atomicfu.atomic
+
+@CameraGraphScope
+@Subcomponent(
+    modules = [
+        CameraGraphModules::class,
+        ExternalCameraGraphConfigModule::class
+    ]
+)
+internal interface ExternalCameraGraphComponent {
+    fun cameraGraph(): CameraGraph
+
+    @Subcomponent.Builder
+    interface Builder {
+        fun externalCameraGraphConfigModule(config: ExternalCameraGraphConfigModule): Builder
+        fun build(): ExternalCameraGraphComponent
+    }
+}
+
+@Module
+internal class ExternalCameraGraphConfigModule(
+    private val config: CameraGraph.Config,
+    private val cameraDevices: CameraDevices,
+    private val requestProcessor: RequestProcessor
+) {
+    @Provides
+    fun provideCameraGraphConfig(): CameraGraph.Config = config
+
+    @Provides
+    fun provideCameraDevices(): CameraDevices = cameraDevices
+
+    @Provides
+    fun provideGraphController(graphListener: GraphListener): GraphController =
+        object : GraphController {
+            var started = atomic(false)
+            override fun start() {
+                if (started.compareAndSet(expect = false, update = true)) {
+                    graphListener.onGraphStarted(requestProcessor)
+                }
+            }
+
+            override fun stop() {
+                if (started.compareAndSet(expect = true, update = false)) {
+                    graphListener.onGraphStopped(requestProcessor)
+                }
+            }
+
+            override fun restart() {
+                stop()
+                start()
+            }
+        }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraPipeComponent.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraPipeComponent.kt
new file mode 100644
index 0000000..0cb6838
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/ExternalCameraPipeComponent.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import android.os.HandlerThread
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+@Singleton
+@Component(
+    modules = [
+        CameraPipeModules::class,
+        ExternalCameraPipeModule::class
+    ]
+)
+internal interface ExternalCameraPipeComponent {
+    fun cameraGraphBuilder(): ExternalCameraGraphComponent.Builder
+}
+
+@Module
+internal abstract class ExternalCameraPipeModule {
+    companion object {
+        @Provides
+        @ForCameraThread
+        fun provideExternalCameraThread(): HandlerThread? = null
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphController.kt
new file mode 100644
index 0000000..ab5b1af
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphController.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.pipe.impl
+
+import androidx.camera.camera2.pipe.RequestProcessor
+
+internal interface GraphController {
+    /**
+     * Tell the graph to start and initialize a [RequestProcessor] instances.
+     */
+    fun start()
+
+    /**
+     * Tell the [GraphController] to stop initialization and to tear down any existing
+     * [RequestProcessor] instance.
+     */
+    fun stop()
+
+    /**
+     * Signals the [GraphController] that a [RequestProcessor] may need to be recreated.
+     */
+    fun restart()
+
+    interface GraphListener {
+        /**
+         * Used to indicate that the graph has been initialized and is ready to actively process
+         * requests using the provided [RequestProcessor] interface.
+         */
+        fun onGraphStarted(requestProcessor: RequestProcessor)
+
+        /**
+         * Used to indicate that a previously initialized [RequestProcessor] is no longer available.
+         */
+        fun onGraphStopped(requestProcessor: RequestProcessor)
+
+        /**
+         * Used to indicate that the internal state of the [RequestProcessor] has changed. This is
+         * a signal that previously queued requests may now succeed if they previously failed.
+         */
+        fun onGraphModified(requestProcessor: RequestProcessor)
+    }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
index 856a94e..74a508b 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/GraphProcessor.kt
@@ -19,6 +19,7 @@
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestProcessor
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.core.Log.warn
@@ -33,12 +34,20 @@
  * [RequestProcessor] instance, and for maintaining state across one or more [RequestProcessor]
  * instances.
  */
-internal interface GraphProcessor {
-    fun setRepeating(request: Request)
+internal interface GraphProcessor : GraphController.GraphListener {
     fun submit(request: Request)
     fun submit(requests: List<Request>)
     suspend fun submit(parameters: Map<*, Any>): Boolean
 
+    fun startRepeating(request: Request)
+    fun stopRepeating()
+
+    /**
+     * Indicates that internal parameters may have changed, and that the repeating request should
+     * be updated as soon as possible.
+     */
+    fun invalidate()
+
     /**
      * Abort all submitted requests that have not yet been submitted to the [RequestProcessor] as
      * well as aborting requests on the [RequestProcessor] itself.
@@ -50,10 +59,6 @@
      * [GraphProcessor] is closed will be immediately aborted.
      */
     fun close()
-
-    fun attach(requestProcessor: RequestProcessor)
-    fun detach(requestProcessor: RequestProcessor)
-    fun invalidate()
 }
 
 /**
@@ -63,13 +68,14 @@
 internal class GraphProcessorImpl @Inject constructor(
     private val threads: Threads,
     private val cameraGraphConfig: CameraGraph.Config,
+    private val graphState3A: GraphState3A,
     @ForCameraGraph private val graphScope: CoroutineScope,
     @ForCameraGraph private val graphListeners: java.util.ArrayList<Request.Listener>
 ) : GraphProcessor {
     private val lock = Any()
 
     @GuardedBy("lock")
-    private val requestQueue: MutableList<List<Request>> = ArrayList()
+    private val submitQueue: MutableList<List<Request>> = ArrayList()
 
     @GuardedBy("lock")
     private var currentRepeatingRequest: Request? = null
@@ -89,7 +95,7 @@
     @GuardedBy("lock")
     private var closed = false
 
-    override fun attach(requestProcessor: RequestProcessor) {
+    override fun onGraphStarted(requestProcessor: RequestProcessor) {
         var oldRequestProcessor: RequestProcessor? = null
         synchronized(lock) {
             if (closed) {
@@ -113,7 +119,7 @@
         resubmit()
     }
 
-    override fun detach(requestProcessor: RequestProcessor) {
+    override fun onGraphStopped(requestProcessor: RequestProcessor) {
         var oldRequestProcessor: RequestProcessor? = null
         synchronized(lock) {
             if (closed) {
@@ -139,19 +145,48 @@
         }
     }
 
-    override fun invalidate() {
+    override fun onGraphModified(requestProcessor: RequestProcessor) {
+        synchronized(lock) {
+            if (closed) {
+                return
+            }
+            if (requestProcessor != _requestProcessor) {
+                return
+            }
+        }
         resubmit()
     }
 
-    override fun setRepeating(request: Request) {
+    override fun startRepeating(request: Request) {
         synchronized(lock) {
             if (closed) return
             nextRepeatingRequest = request
-            debug { "Set repeating request to ${request.formatForLogs()}" }
+            debug { "startRepeating with ${request.formatForLogs()}" }
         }
 
         graphScope.launch {
-            trySetRepeating()
+            tryStartRepeating()
+        }
+    }
+
+    override fun stopRepeating() {
+        val processor: RequestProcessor?
+
+        synchronized(lock) {
+            processor = _requestProcessor
+            nextRepeatingRequest = null
+            currentRepeatingRequest = null
+        }
+
+        graphScope.launch {
+            Debug.traceStart { "$this#stopRepeating" }
+            // Start with requests that have already been submitted
+            if (processor != null) {
+                synchronized(processor) {
+                    processor.stopRepeating()
+                }
+            }
+            Debug.traceStop()
         }
     }
 
@@ -167,7 +202,7 @@
                 }
                 return
             }
-            requestQueue.add(requests)
+            submitQueue.add(requests)
         }
 
         graphScope.launch {
@@ -182,11 +217,14 @@
         withContext(threads.ioDispatcher) {
             val processor: RequestProcessor?
             val request: Request?
+            val requiredParameters: Map<*, Any>
 
             synchronized(lock) {
                 if (closed) return@withContext false
                 processor = _requestProcessor
                 request = currentRepeatingRequest
+                requiredParameters =
+                    parameters.toMutableMap().also { it.putAll(graphState3A.readState()) }
             }
 
             return@withContext when {
@@ -194,19 +232,28 @@
                 else -> processor.submit(
                     request,
                     defaultParameters = cameraGraphConfig.defaultParameters,
-                    requiredParameters = parameters
+                    requiredParameters = requiredParameters,
+                    defaultListeners = graphListeners
                 )
             }
         }
 
+    override fun invalidate() {
+        // Invalidate is only used for updates to internal state (listeners, parameters, etc) and
+        // should not (currently) attempt to resubmit the normal request queue.
+        graphScope.launch {
+            tryStartRepeating()
+        }
+    }
+
     override fun abort() {
         val processor: RequestProcessor?
         val requests: List<List<Request>>
 
         synchronized(lock) {
             processor = _requestProcessor
-            requests = requestQueue.toList()
-            requestQueue.clear()
+            requests = submitQueue.toList()
+            submitQueue.clear()
         }
 
         graphScope.launch {
@@ -243,7 +290,7 @@
 
     private fun resubmit() {
         graphScope.launch {
-            trySetRepeating()
+            tryStartRepeating()
             submitLoop()
         }
     }
@@ -264,7 +311,7 @@
         }
     }
 
-    private fun trySetRepeating() {
+    private fun tryStartRepeating() {
         val processor: RequestProcessor?
         val request: Request?
 
@@ -273,16 +320,36 @@
 
             processor = _requestProcessor
             request = nextRepeatingRequest ?: currentRepeatingRequest
+
+            // TODO: It might be a good idea to turn the "nextRepeatingRequest" into a queue to
+            //  help with cases where we want to start the camera early. Example: If we have a
+            //  stream configuration where the viewfinder is deferred, but we have an ImageReader
+            //  that is _not_ deferred, it may be possible to submit the repeating request.
+            //  However, a request with *both* streams would be rejected because not all streams
+            //  are ready.
+            //  Example:
+            //   - Request(listOf(viewfinderStream, otherStream)) // Fails (no viewfinder surface)
+            //   - Request(listOf(otherStream)) // works
+            //  If (as an app developer) we wanted to make sure the camera starts before the
+            //  viewfinder is ready, we would likely want to do something like:
+            //   - startRepeating(listOf(otherStream))
+            //   - startRepeating(listOf(viewfinderStream, otherStream))
+            //  The way this is implemented at the moment, the "nextRepeatingRequest" would be set
+            //  to the second call to startRepeating, which would not work. Since the first call got
+            //  discarded, we would be unable to start the camera before the viewfinder was
+            //  available.
         }
 
         if (processor != null && request != null) {
 
-            Debug.traceStart { "$this#setRepeating" }
+            Debug.traceStart { "$this#startRepeating" }
             synchronized(processor) {
-                if (processor.setRepeating(
+
+                if (processor.startRepeating(
                         request,
-                        cameraGraphConfig.defaultParameters,
-                        emptyMap<Any, Any>()
+                        defaultParameters = cameraGraphConfig.defaultParameters,
+                        requiredParameters = graphState3A.readState(),
+                        defaultListeners = graphListeners
                     )
                 ) {
                     // ONLY update the current repeating request if the update succeeds
@@ -317,7 +384,7 @@
             }
 
             val nullableProcessor = _requestProcessor
-            val nullableBurst = requestQueue.firstOrNull()
+            val nullableBurst = submitQueue.firstOrNull()
             if (nullableProcessor == null || nullableBurst == null) {
                 return
             }
@@ -336,14 +403,16 @@
                     if (burst.size == 1) {
                         processor.submit(
                             burst[0],
-                            cameraGraphConfig.defaultParameters,
-                            emptyMap<Any, Any>()
+                            defaultParameters = cameraGraphConfig.defaultParameters,
+                            requiredParameters = graphState3A.readState(),
+                            defaultListeners = graphListeners
                         )
                     } else {
                         processor.submit(
                             burst,
-                            cameraGraphConfig.defaultParameters,
-                            emptyMap<Any, Any>()
+                            defaultParameters = cameraGraphConfig.defaultParameters,
+                            requiredParameters = graphState3A.readState(),
+                            defaultListeners = graphListeners
                         )
                     }
                 }
@@ -351,9 +420,9 @@
                 Debug.traceStop()
                 synchronized(lock) {
                     if (submitted) {
-                        check(requestQueue.removeAt(0) === burst)
+                        check(submitQueue.removeAt(0) === burst)
 
-                        val nullableBurst = requestQueue.firstOrNull()
+                        val nullableBurst = submitQueue.firstOrNull()
                         if (nullableBurst == null) {
                             dirty = false
                             submitting = false
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
deleted file mode 100644
index 69b9ac0..0000000
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/RequestProcessor.kt
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.impl
-
-import android.hardware.camera2.CaptureRequest
-import androidx.camera.camera2.pipe.Request
-
-/**
- * An instance of a RequestProcessor exists for the duration of a CameraCaptureSession and must be
- * created for each new CameraCaptureSession. It is responsible for low level interactions with the
- * CameraCaptureSession and for shimming the interfaces and callbacks to make them easier to work
- * with. Unlike the CameraCaptureSessionProxy interface the RequestProcessor has more liberty to
- * change the standard Camera2 API contract to make it easier to work with.
- *
- * There are some important design considerations:
- * - Instances class is not thread safe, although the companion object has some counters that are
- *   global and *are* thread safe.
- * - Special care is taken to reduce the number objects and wrappers that are created, and to reduce
- *   the number of loops and overhead in wrapper objects.
- * - Callbacks are expected to be invoked at *very* high frequency on the camera thread.
- * - One RequestProcessor instance per CameraCaptureSession
- */
-interface RequestProcessor {
-
-    /**
-     * Submit a single [Request] with an optional set of extra parameters.
-     *
-     * @param request the request to submit to the camera.
-     * @param defaultParameters will not override parameters specified in the request.
-     * @param requiredParameters will override parameters specified in the request.
-     * @return false if this request failed to be submitted. If this method returns false, none of
-     *   the callbacks on the Request(s) will be invoked.
-     */
-    fun submit(
-        request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean
-
-    /**
-     * Submit a list of [Request]s with an optional set of extra parameters.
-     *
-     * @param requests the requests to submit to the camera.
-     * @param defaultParameters will not override parameters specified in the request.
-     * @param requiredParameters will override parameters specified in the request.
-     * @return false if this request failed to be submitted. If this method returns false, none of
-     *   the callbacks on the Request(s) will be invoked.
-     */
-    fun submit(
-        requests: List<Request>,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean
-
-    /**
-     * Set the repeating [Request] with an optional set of extra parameters.
-     *
-     * The current repeating request may not be executed at all, or it may be executed multiple
-     * times. The repeating request is used as the base request for all 3A interactions which may
-     * cause the request to be used to generate multiple [CaptureRequest]s to the camera.
-     *
-     * @param request the requests to set as the repeating request.
-     * @param defaultParameters will not override parameters specified in the request.
-     * @param requiredParameters will override parameters specified in the request.
-     * @return false if this request failed to be submitted. If this method returns false, none of
-     *   the callbacks on the Request(s) will be invoked.
-     */
-    fun setRepeating(
-        request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean
-
-    /**
-     * Abort requests that have been submitted but not completed.
-     */
-    fun abortCaptures()
-
-    /**
-     * Stops the current repeating request.
-     */
-    fun stopRepeating()
-
-    /**
-     * Puts the RequestProcessor into a closed state where it will reject all incoming requests.
-     * This does NOT call stopRepeating() or abortCaptures().
-     */
-    fun close()
-}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
index 118d015..e916f6c 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/VirtualSessionState.kt
@@ -18,6 +18,7 @@
 
 import android.view.Surface
 import androidx.annotation.GuardedBy
+import androidx.camera.camera2.pipe.RequestProcessor
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.core.Debug
 import androidx.camera.camera2.pipe.core.Log
@@ -43,9 +44,9 @@
  * being created when shutdown / disconnect was called.
  */
 internal class VirtualSessionState(
-    private val graphProcessor: GraphProcessor,
+    private val graphListener: GraphController.GraphListener,
     private val sessionFactory: SessionFactory,
-    private val requestProcessorFactory: RequestProcessorFactory,
+    private val requestProcessorFactory: Camera2RequestProcessorFactory,
     private val scope: CoroutineScope
 ) : CameraCaptureSessionWrapper.StateCallback, StreamGraphImpl.SurfaceListener {
     private val debugId = virtualSessionDebugIds.incrementAndGet()
@@ -195,7 +196,7 @@
                     "Configured $this in ${duration.formatMs()}"
                 }
 
-                graphProcessor.attach(it.processor)
+                graphListener.onGraphStarted(it.processor)
             }
         }
     }
@@ -217,7 +218,7 @@
         }
 
         if (captureSession != null) {
-            graphProcessor.detach(captureSession.processor)
+            graphListener.onGraphStopped(captureSession.processor)
         }
 
         synchronized(this) {
@@ -245,8 +246,8 @@
         if (captureSession != null) {
             Debug.traceStart { "$this#shutdown" }
 
-            Debug.traceStart { "$graphProcessor#detach" }
-            graphProcessor.detach(captureSession.processor)
+            Debug.traceStart { "$graphListener#onGraphStopped" }
+            graphListener.onGraphStopped(captureSession.processor)
             Debug.traceStop()
 
             Debug.traceStart { "$this#stopRepeating" }
@@ -306,7 +307,7 @@
             }
 
             if (tryResubmit && retryAllowed) {
-                graphProcessor.invalidate()
+                graphListener.onGraphModified(captureSession.processor)
             }
             Debug.traceStop()
         }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/AndroidCameraMetadata.kt
similarity index 96%
rename from camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
rename to camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/AndroidCameraMetadata.kt
index dfdfe8e..2c4526a 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/impl/CameraMetadataImpl.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/wrapper/AndroidCameraMetadata.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.camera.camera2.pipe.impl
+package androidx.camera.camera2.pipe.wrapper
 
 import android.annotation.SuppressLint
 import android.hardware.camera2.CameraCharacteristics
@@ -29,14 +29,13 @@
 import androidx.camera.camera2.pipe.core.Log
 import androidx.camera.camera2.pipe.core.Timestamps
 import androidx.camera.camera2.pipe.core.Timestamps.formatMs
-import androidx.camera.camera2.pipe.wrapper.Api28Compat
 
 /**
  * This implementation provides access to CameraCharacteristics and lazy caching of properties
  * that are either expensive to create and access, or that only exist on newer versions of the
  * OS.
  */
-internal class CameraMetadataImpl constructor(
+public class AndroidCameraMetadata constructor(
     override val camera: CameraId,
     override val isRedacted: Boolean,
     private val characteristics: CameraCharacteristics,
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
index 246802dd..32674ba 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/CameraPipeTest.kt
@@ -18,15 +18,19 @@
 
 import android.content.Context
 import android.os.Build
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.FakeCameraDevices
+import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
+import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CameraPipeTest {
 
@@ -41,7 +45,7 @@
 
     @Test
     fun createCameraGraph() {
-        val fakeCameraId = FakeCameras.create()
+        val fakeCameraId = RobolectricCameras.create()
         val context = ApplicationProvider.getApplicationContext() as Context
         val cameraPipe = CameraPipe(CameraPipe.Config(context))
         val cameraGraph = cameraPipe.create(
@@ -56,7 +60,7 @@
 
     @Test
     fun iterateCameraIds() {
-        val fakeCameraId = FakeCameras.create()
+        val fakeCameraId = RobolectricCameras.create()
         val context = ApplicationProvider.getApplicationContext() as Context
         val cameraPipe = CameraPipe(CameraPipe.Config(context))
         val cameras = cameraPipe.cameras()
@@ -66,4 +70,56 @@
         assertThat(cameraList.size).isEqualTo(1)
         assertThat(cameraList).contains(fakeCameraId)
     }
+
+    @Test
+    fun createExternalCameraGraph() {
+        val fakeRequestProcessor = FakeRequestProcessor()
+        val fakeCameraMetadata = FakeCameraMetadata()
+        val fakeCameras = FakeCameraDevices(listOf(fakeCameraMetadata))
+
+        val config = CameraGraph.Config(
+            camera = fakeCameraMetadata.camera,
+            streams = listOf(),
+            defaultTemplate = RequestTemplate(0)
+        )
+
+        val cameraGraph = CameraPipe.External().create(config, fakeCameras, fakeRequestProcessor)
+        assertThat(cameraGraph).isNotNull()
+
+        val request = Request(streams = emptyList())
+        cameraGraph.start()
+
+        // Check that repeating request can be issued
+        runBlocking {
+            cameraGraph.acquireSession().use {
+                it.startRepeating(request)
+            }
+
+            val repeatingEvent = fakeRequestProcessor.nextEvent()
+            assertThat(repeatingEvent.startRepeating).isTrue()
+            assertThat(repeatingEvent.requestSequence!!.requests.first()).isSameInstanceAs(request)
+
+            cameraGraph.stop()
+
+            val closeEvent = fakeRequestProcessor.nextEvent()
+            assertThat(closeEvent.close).isTrue()
+        }
+
+        fakeRequestProcessor.reset()
+
+        // Check that repeating request is saved and reused.
+        runBlocking {
+            cameraGraph.start()
+
+            val repeatingEvent = fakeRequestProcessor.nextEvent()
+            if (!repeatingEvent.startRepeating) {
+                throw RuntimeException("$repeatingEvent")
+            }
+
+            assertThat(repeatingEvent.startRepeating).isTrue()
+            assertThat(repeatingEvent.requestSequence!!.requests.first()).isSameInstanceAs(request)
+
+            cameraGraph.stop()
+        }
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
index 5220c64..9f78329 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/ModeEnum3ATest.kt
@@ -17,13 +17,13 @@
 package androidx.camera.camera2.pipe
 
 import android.os.Build
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class ModeEnum3ATest {
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
index 5e02b30..9152342 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/RequestTest.kt
@@ -18,14 +18,14 @@
 
 import android.hardware.camera2.CaptureRequest
 import android.os.Build
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeMetadata
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class RequestTest {
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
index 5475134..8216861 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/StreamTest.kt
@@ -18,13 +18,13 @@
 
 import android.os.Build
 import android.util.Size
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class StreamTest {
     private val streamConfig1 = CameraStream.Config.create(
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
index c215ac1f..af0b2e0 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraGraphImplTest.kt
@@ -21,11 +21,11 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
-import androidx.camera.camera2.pipe.testing.FakeGraphState
+import androidx.camera.camera2.pipe.testing.FakeGraphController
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
 import org.junit.Before
@@ -34,17 +34,17 @@
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CameraGraphImplTest {
-    private val fakeCameraId = FakeCameras.create()
+    private val fakeCameraId = RobolectricCameras.create()
     private val fakeMetadata = FakeCameraMetadata(
         mapOf(INFO_SUPPORTED_HARDWARE_LEVEL to INFO_SUPPORTED_HARDWARE_LEVEL_FULL),
         cameraId = fakeCameraId
     )
     private val fakeGraphProcessor = FakeGraphProcessor()
-    private val fakeGraphState = FakeGraphState()
+    private val fakeGraphController = FakeGraphController()
     private lateinit var impl: CameraGraphImpl
 
     @Before
@@ -61,7 +61,7 @@
                 fakeMetadata,
                 config
             ),
-            fakeGraphState,
+            fakeGraphController,
             GraphState3A(),
             Listener3A()
         )
@@ -113,7 +113,7 @@
     fun sessionSetsRepeatingRequestOnGraphProcessor() {
         val session = checkNotNull(impl.acquireSessionOrNull())
         val request = Request(listOf())
-        session.setRepeating(request)
+        session.startRepeating(request)
 
         assertThat(fakeGraphProcessor.repeatingRequest).isSameInstanceAs(request)
     }
@@ -144,15 +144,15 @@
 
     @Test
     fun stoppingCameraGraphStopsGraphProcessor() {
-        assertThat(fakeGraphState.active).isFalse()
+        assertThat(fakeGraphController.active).isFalse()
         impl.start()
-        assertThat(fakeGraphState.active).isTrue()
+        assertThat(fakeGraphController.active).isTrue()
         impl.stop()
-        assertThat(fakeGraphState.active).isFalse()
+        assertThat(fakeGraphController.active).isFalse()
         impl.start()
-        assertThat(fakeGraphState.active).isTrue()
+        assertThat(fakeGraphController.active).isTrue()
         impl.close()
         assertThat(fakeGraphProcessor.closed).isTrue()
-        assertThat(fakeGraphState.active).isFalse()
+        assertThat(fakeGraphController.active).isFalse()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
index c62cf26..0f8c4b7 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraMetadataCacheTest.kt
@@ -18,8 +18,8 @@
 
 import android.hardware.camera2.CameraCharacteristics
 import android.os.Build
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
 import androidx.camera.camera2.pipe.testing.FakeThreads
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -27,13 +27,13 @@
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CameraMetadataCacheTest {
     @Test
     fun metadataIsCachedAndShimmed() {
-        val camera0 = FakeCameras.create(
+        val camera0 = RobolectricCameras.create(
             mapOf(
                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to CameraCharacteristics
                     .INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
@@ -43,7 +43,7 @@
             )
         )
 
-        val camera1 = FakeCameras.create(
+        val camera1 = RobolectricCameras.create(
             mapOf(
                 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL to CameraCharacteristics
                     .INFO_SUPPORTED_HARDWARE_LEVEL_3,
@@ -54,9 +54,9 @@
         )
 
         val cache = CameraMetadataCache(
-            FakeCameras.application,
+            RobolectricCameras.application,
             FakeThreads.forTests,
-            Permissions(FakeCameras.application)
+            Permissions(RobolectricCameras.application)
         )
 
         val metadata0 = cache.awaitMetadata(camera0)
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
index ea02fe7..1d98d0f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CameraPipeComponentTest.kt
@@ -20,18 +20,18 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.CameraGraph
 import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CameraPipeComponentTest {
-    private val fakeCameraId = FakeCameras.create()
+    private val fakeCameraId = RobolectricCameras.create()
 
     @Test
     fun createCameraPipeComponent() {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
index 61af5b8d..60ad8f5 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AForCaptureTest.kt
@@ -24,7 +24,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -38,12 +38,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class Controller3AForCaptureTest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -108,7 +108,7 @@
 
         // We now check if the correct sequence of requests were submitted by lock3AForCapture call.
         // There should be a request to trigger AF and AE precapture metering.
-        val request1 = requestProcessor.nextEvent().request
+        val request1 = requestProcessor.nextEvent().requestSequence
         assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
@@ -185,7 +185,7 @@
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and AE precapture metering.
-        val request1 = requestProcessor.nextEvent().request
+        val request1 = requestProcessor.nextEvent().requestSequence
         assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
@@ -222,7 +222,7 @@
 
         // We now check if the correct sequence of requests were submitted by unlock3APostCapture
         // call. There should be a request to cancel AF and lock ae.
-        val request1 = requestProcessor.nextEvent().request
+        val request1 = requestProcessor.nextEvent().requestSequence
         assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
@@ -230,14 +230,14 @@
             .isEqualTo(true)
 
         // Then another request to unlock ae.
-        val request2 = requestProcessor.nextEvent().request
+        val request2 = requestProcessor.nextEvent().requestSequence
         assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 
     companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
index 4374aee..f5b3b85 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ALock3ATest.kt
@@ -25,7 +25,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -40,12 +40,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Controller3ALock3ATest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -114,17 +114,17 @@
 
         // We not check if the correct sequence of requests were submitted by lock3A call. The
         // request should be a repeating request to lock AE.
-        val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // The second request should be a single request to lock AF.
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request2.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -171,10 +171,10 @@
 
         // Check the correctness of the requests submitted by lock3A.
         // One repeating request was sent to monitor the state of AE to get converged.
-        requestProcessor.nextEvent().request
+        requestProcessor.nextEvent().requestSequence
         // Once AE is converged, another repeatingrequest is sent to lock AE.
-        val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
@@ -203,8 +203,8 @@
         assertThat(result3A.status).isEqualTo(Status3A.OK)
 
         // A single request to lock AF must have been used as well.
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
     }
@@ -249,8 +249,8 @@
 
         // For a new AE scan we first send a request to unlock AE just in case it was
         // previously or internally locked.
-        val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             false
         )
 
@@ -279,17 +279,17 @@
         assertThat(result3A.status).isEqualTo(Status3A.OK)
 
         // There should be one more request to lock AE after new scan is done.
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
-        val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request3 = requestProcessor.nextEvent().requestSequence
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -359,17 +359,17 @@
         // There should be one request to monitor AF to finish it's scan.
         requestProcessor.nextEvent()
         // One request to lock AE
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
-        val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request3 = requestProcessor.nextEvent().requestSequence
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -437,25 +437,25 @@
         assertThat(result3A.status).isEqualTo(Status3A.OK)
 
         // One request to cancel AF to start a new scan.
-        val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
         // There should be one request to monitor AF to finish it's scan.
         requestProcessor.nextEvent()
 
         // There should be one request to monitor lock AE.
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
-        val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request3 = requestProcessor.nextEvent().requestSequence
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -523,19 +523,33 @@
         assertThat(result3A.status).isEqualTo(Status3A.OK)
 
         // There should be one request to monitor AF to finish it's scan.
-        requestProcessor.nextEvent()
+        val event = requestProcessor.nextEvent()
+        assertThat(event.startRepeating).isTrue()
+        assertThat(event.rejected).isFalse()
+        assertThat(event.abort).isFalse()
+        assertThat(event.close).isFalse()
+        assertThat(event.submit).isFalse()
+
         // One request to lock AE
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request2Event = requestProcessor.nextEvent()
+        assertThat(request2Event.startRepeating).isTrue()
+        assertThat(request2Event.submit).isFalse()
+        val request2 = request2Event.requestSequence!!
+        assertThat(request2).isNotNull()
+        assertThat(request2.requiredParameters).isNotEmpty()
+        assertThat(request2.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
-        val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request3Event = requestProcessor.nextEvent()
+        assertThat(request3Event.startRepeating).isFalse()
+        assertThat(request3Event.submit).isTrue()
+        val request3 = request3Event.requestSequence!!
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request3.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request3.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
@@ -603,35 +617,35 @@
         assertThat(result3A.status).isEqualTo(Status3A.OK)
 
         // One request to cancel AF to start a new scan.
-        val request1 = requestProcessor.nextEvent().request
-        assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request1 = requestProcessor.nextEvent().requestSequence
+        assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
         )
         // There should be one request to unlock AE and monitor the current AF scan to finish.
-        val request2 = requestProcessor.nextEvent().request
-        assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request2 = requestProcessor.nextEvent().requestSequence
+        assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             false
         )
 
         // There should be one request to monitor lock AE.
-        val request3 = requestProcessor.nextEvent().request
-        assertThat(request3!!.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        val request3 = requestProcessor.nextEvent().requestSequence
+        assertThat(request3!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
 
         // And one request to lock AF.
-        val request4 = requestProcessor.nextEvent().request
-        assertThat(request4!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
+        val request4 = requestProcessor.nextEvent().requestSequence
+        assertThat(request4!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER]).isEqualTo(
             CaptureRequest.CONTROL_AF_TRIGGER_START
         )
-        assertThat(request4.parameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
+        assertThat(request4.requiredParameters[CaptureRequest.CONTROL_AE_LOCK]).isEqualTo(
             true
         )
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 
     companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
index 18e7c2c..0d63d49 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASetTorchTest.kt
@@ -26,11 +26,11 @@
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
 import androidx.camera.camera2.pipe.TorchState
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
 import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
@@ -39,12 +39,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Controller3ASetTorchTest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -149,7 +149,7 @@
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
index 6309f96..77e4f03 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3ASubmit3ATest.kt
@@ -29,7 +29,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -42,12 +42,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Controller3ASubmit3ATest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -245,7 +245,7 @@
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
index ed9455b..0e02cb1 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUnlock3ATest.kt
@@ -24,7 +24,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -39,12 +39,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Controller3AUnlock3ATest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -84,8 +84,8 @@
         Truth.assertThat(result.isCompleted).isFalse()
 
         // There should be one request to lock AE.
-        val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
+        val request1 = requestProcessor.nextEvent().requestSequence
+        Truth.assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
@@ -145,8 +145,8 @@
         Truth.assertThat(result.isCompleted).isFalse()
 
         // There should be one request to unlock AF.
-        val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
+        val request1 = requestProcessor.nextEvent().requestSequence
+        Truth.assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
 
         GlobalScope.launch {
@@ -208,8 +208,8 @@
         Truth.assertThat(result.isCompleted).isFalse()
 
         // There should be one request to lock AWB.
-        val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AWB_LOCK])
+        val request1 = requestProcessor.nextEvent().requestSequence
+        Truth.assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AWB_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
@@ -270,12 +270,12 @@
         Truth.assertThat(result.isCompleted).isFalse()
 
         // There should be one request to unlock AF.
-        val request1 = requestProcessor.nextEvent().request
-        Truth.assertThat(request1!!.parameters[CaptureRequest.CONTROL_AF_TRIGGER])
+        val request1 = requestProcessor.nextEvent().requestSequence
+        Truth.assertThat(request1!!.requiredParameters[CaptureRequest.CONTROL_AF_TRIGGER])
             .isEqualTo(CaptureRequest.CONTROL_AF_TRIGGER_CANCEL)
         // Then request to unlock AE.
-        val request2 = requestProcessor.nextEvent().request
-        Truth.assertThat(request2!!.parameters[CaptureRequest.CONTROL_AE_LOCK])
+        val request2 = requestProcessor.nextEvent().requestSequence
+        Truth.assertThat(request2!!.requiredParameters[CaptureRequest.CONTROL_AE_LOCK])
             .isEqualTo(false)
 
         GlobalScope.launch {
@@ -303,8 +303,8 @@
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 
     companion object {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
index 365e6fb..68b59d4 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Controller3AUpdate3ATest.kt
@@ -28,7 +28,7 @@
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
@@ -43,12 +43,12 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Controller3AUpdate3ATest {
-    private val graphProcessor = FakeGraphProcessor()
     private val graphState3A = GraphState3A()
-    private val requestProcessor = FakeRequestProcessor(graphState3A)
+    private val graphProcessor = FakeGraphProcessor(graphState3A = graphState3A)
+    private val requestProcessor = FakeRequestProcessor()
     private val listener3A = Listener3A()
     private val controller3A = Controller3A(graphProcessor, graphState3A, listener3A)
 
@@ -245,7 +245,7 @@
     }
 
     private fun initGraphProcessor() {
-        graphProcessor.attach(requestProcessor)
-        graphProcessor.setRepeating(Request(streams = listOf(StreamId(1))))
+        graphProcessor.onGraphStarted(requestProcessor)
+        graphProcessor.startRepeating(Request(streams = listOf(StreamId(1))))
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CorrectedFrameMetadataTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CorrectedFrameMetadataTest.kt
index 087fb32..bc90bab 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CorrectedFrameMetadataTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/CorrectedFrameMetadataTest.kt
@@ -18,7 +18,7 @@
 
 import android.hardware.camera2.CaptureResult
 import android.os.Build
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeMetadata
 import com.google.common.truth.Truth.assertThat
@@ -26,7 +26,7 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class CorrectedFrameMetadataTest {
 
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
index a3e5555..0c08646 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/GraphProcessorTest.kt
@@ -21,25 +21,29 @@
 import androidx.camera.camera2.pipe.CameraId
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.Event
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeRequestListener
 import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
 import androidx.camera.camera2.pipe.testing.FakeThreads
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeoutOrNull
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class GraphProcessorTest {
     private val globalListener = FakeRequestListener()
 
-    private val fakeProcessor1 = FakeRequestProcessor(GraphState3A())
-    private val fakeProcessor2 = FakeRequestProcessor(GraphState3A())
+    private val graphState3A = GraphState3A()
+    private val fakeProcessor1 = FakeRequestProcessor()
+    private val fakeProcessor2 = FakeRequestProcessor()
 
     private val requestListener1 = FakeRequestListener()
     private val request1 = Request(listOf(StreamId(0)), listeners = listOf(requestListener1))
@@ -54,24 +58,25 @@
 
     @Test
     fun graphProcessorSubmitsRequests() {
-        // The graph processor uses 'launch' within the coroutine scope to invoke updates on the
-        // requestProcessor instance. runBlocking forces all jobs to complete before testing the
-        // state of results.
         runBlocking(Dispatchers.Default) {
+
+            // The graph processor uses 'launch' within the coroutine scope to invoke updates on the
+            // requestProcessor instance. runBlocking forces all jobs to complete before testing the
+            // state of results.
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
             graphProcessor.submit(request1)
-        }
 
-        // Make sure the requests get submitted to the request processor
-        assertThat(fakeProcessor1.requestQueue).hasSize(1)
-        assertThat(fakeProcessor1.requestQueue.first().burst).hasSize(1)
-        assertThat(fakeProcessor1.requestQueue.first().burst.first()).isSameInstanceAs(request1)
+            // Make sure the requests get submitted to the request processor
+            val event = fakeProcessor1.nextEvent()
+            assertThat(event.requestSequence!!.requests).containsExactly(request1)
+        }
     }
 
     @Test
@@ -83,22 +88,22 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
-            graphProcessor.attach(fakeProcessor1)
-            graphProcessor.attach(fakeProcessor2)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor2)
             graphProcessor.submit(request1)
+
+            val event1 = fakeProcessor1.nextEvent()
+            assertThat(event1.close).isTrue()
+
+            val event2 = fakeProcessor2.nextEvent()
+            assertThat(event2.submit).isTrue()
+            assertThat(event2.requestSequence!!.requests).containsExactly(request1)
         }
-
-        // requestProcessor1 does not receive requests
-        assertThat(fakeProcessor1.requestQueue).hasSize(0)
-
-        // requestProcessor2 receives requests
-        assertThat(fakeProcessor2.requestQueue).hasSize(1)
-        assertThat(fakeProcessor2.requestQueue.first().burst).hasSize(1)
-        assertThat(fakeProcessor2.requestQueue.first().burst.first()).isSameInstanceAs(request1)
     }
 
     @Test
@@ -110,6 +115,7 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
@@ -119,13 +125,16 @@
 
             // Request1 and 2 should be queued and will be submitted even when the request
             // processor is set after the requests are submitted.
-            graphProcessor.attach(fakeProcessor1)
-        }
+            graphProcessor.onGraphStarted(fakeProcessor1)
 
-        // Make sure the requests get submitted to the request processor
-        assertThat(fakeProcessor1.requestQueue).hasSize(2)
-        assertThat(fakeProcessor1.requestQueue[0].burst[0]).isSameInstanceAs(request1)
-        assertThat(fakeProcessor1.requestQueue[1].burst[0]).isSameInstanceAs(request2)
+            val event1 = awaitEvent(fakeProcessor1, request1) { it.submit }
+            assertThat(event1.requestSequence!!.requests).hasSize(1)
+            assertThat(event1.requestSequence!!.requests).contains(request1)
+
+            val event2 = fakeProcessor1.nextEvent()
+            assertThat(event2.requestSequence!!.requests).hasSize(1)
+            assertThat(event2.requestSequence!!.requests).contains(request2)
+        }
     }
 
     @Test
@@ -137,17 +146,18 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
             graphProcessor.submit(listOf(request1, request2))
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            val event = awaitEvent(fakeProcessor1, request1) { it.submit }
+            assertThat(event.requestSequence!!.requests).hasSize(2)
+            assertThat(event.requestSequence!!.requests).contains(request1)
+            assertThat(event.requestSequence!!.requests).contains(request2)
         }
-
-        assertThat(fakeProcessor1.requestQueue).hasSize(1)
-        assertThat(fakeProcessor1.requestQueue[0].burst[0]).isSameInstanceAs(request1)
-        assertThat(fakeProcessor1.requestQueue[0].burst[1]).isSameInstanceAs(request2)
     }
 
     @Test
@@ -156,33 +166,32 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
             fakeProcessor1.rejectRequests = true
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
 
             graphProcessor.submit(request1)
             val event1 = fakeProcessor1.nextEvent()
             assertThat(event1.rejected).isTrue()
-            assertThat(event1.request!!.burst[0]).isSameInstanceAs(request1)
+            assertThat(event1.requestSequence!!.requests[0]).isSameInstanceAs(request1)
 
             graphProcessor.submit(request2)
             val event2 = fakeProcessor1.nextEvent()
             assertThat(event2.rejected).isTrue()
-            assertThat(event2.request!!.burst[0]).isSameInstanceAs(request1)
+            assertThat(event2.requestSequence!!.requests[0]).isSameInstanceAs(request1)
 
-            graphProcessor.attach(fakeProcessor2)
-            assertThat(fakeProcessor2.nextEvent().request!!.burst[0]).isSameInstanceAs(request1)
-            assertThat(fakeProcessor2.nextEvent().request!!.burst[0]).isSameInstanceAs(request2)
-            assertThat(fakeProcessor2.requestQueue).hasSize(2)
+            graphProcessor.onGraphStarted(fakeProcessor2)
+            assertThat(fakeProcessor2.nextEvent().requestSequence!!.requests[0]).isSameInstanceAs(
+                request1
+            )
+            assertThat(fakeProcessor2.nextEvent().requestSequence!!.requests[0]).isSameInstanceAs(
+                request2
+            )
         }
-
-        assertThat(fakeProcessor1.requestQueue).hasSize(0)
-        assertThat(fakeProcessor2.requestQueue).hasSize(2)
-        assertThat(fakeProcessor2.requestQueue[0].burst[0]).isSameInstanceAs(request1)
-        assertThat(fakeProcessor2.requestQueue[1].burst[0]).isSameInstanceAs(request2)
     }
 
     @Test
@@ -191,6 +200,7 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
@@ -198,23 +208,17 @@
             // Note: setting the requestProcessor, and calling submit() can both trigger a call
             // to submit a request.
             fakeProcessor1.rejectRequests = true
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
             graphProcessor.submit(request1)
 
             // Check to make sure that submit is called at least once, and that request1 is rejected
             // from the request processor.
-            val event1 = fakeProcessor1.nextEvent()
-            assertThat(event1.request!!.burst).contains(request1)
-            assertThat(event1.rejected).isTrue()
+            awaitEvent(fakeProcessor1, request1) { it.rejected }
 
             // Stop rejecting requests
             fakeProcessor1.rejectRequests = false
-            assertThat(fakeProcessor1.rejectRequests).isFalse()
-            assertThat(fakeProcessor1.closeInvoked).isFalse()
-            assertThat(fakeProcessor1.stopInvoked).isFalse()
 
             graphProcessor.submit(request2)
-
             // Cycle events until we get a submitted event with request1
             val event2 = awaitEvent(fakeProcessor1, request1) { it.submit }
             assertThat(event2.rejected).isFalse()
@@ -222,7 +226,7 @@
             // Assert that immediately after we get a successfully submitted request, the
             //  next request is also submitted.
             val event3 = fakeProcessor1.nextEvent()
-            assertThat(event3.request!!.burst).contains(request2)
+            assertThat(event3.requestSequence!!.requests).contains(request2)
             assertThat(event3.submit).isTrue()
             assertThat(event3.rejected).isFalse()
         }
@@ -234,16 +238,16 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
-            graphProcessor.attach(fakeProcessor1)
-            graphProcessor.setRepeating(request1)
-            graphProcessor.setRepeating(request2)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            graphProcessor.startRepeating(request1)
+            graphProcessor.startRepeating(request2)
+            awaitEvent(fakeProcessor1, request2) { it.startRepeating }
         }
-
-        assertThat(fakeProcessor1.repeatingRequest?.burst).contains(request2)
     }
 
     @Test
@@ -252,20 +256,18 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
-            graphProcessor.attach(fakeProcessor1)
-            graphProcessor.setRepeating(request1)
-            awaitEvent(fakeProcessor1, request1) { it.setRepeating }
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            graphProcessor.startRepeating(request1)
+            awaitEvent(fakeProcessor1, request1) { it.startRepeating }
 
-            graphProcessor.attach(fakeProcessor2)
-            awaitEvent(fakeProcessor2, request1) { it.setRepeating }
+            graphProcessor.onGraphStarted(fakeProcessor2)
+            awaitEvent(fakeProcessor2, request1) { it.startRepeating }
         }
-
-        assertThat(fakeProcessor1.repeatingRequest?.burst).contains(request1)
-        assertThat(fakeProcessor2.repeatingRequest?.burst).contains(request1)
     }
 
     @Test
@@ -274,19 +276,19 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
             fakeProcessor1.rejectRequests = true
-            graphProcessor.attach(fakeProcessor1)
-            graphProcessor.setRepeating(request1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            graphProcessor.startRepeating(request1)
+            awaitEvent(fakeProcessor1, request1) { it.rejected }
 
-            graphProcessor.attach(fakeProcessor2)
-            awaitEvent(fakeProcessor2, request1) { it.setRepeating }
+            graphProcessor.onGraphStarted(fakeProcessor2)
+            awaitEvent(fakeProcessor2, request1) { it.startRepeating }
         }
-
-        assertThat(fakeProcessor2.repeatingRequest?.burst).contains(request1)
     }
 
     @Test
@@ -295,18 +297,29 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
-            graphProcessor.setRepeating(request1)
+            graphProcessor.startRepeating(request1)
             graphProcessor.submit(request2)
+            delay(50)
 
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+
+            var hasRequest1Event = false
+            var hasRequest2Event = false
+
+            // Loop until we see at least one repeating request, and one submit event.
+            while (!hasRequest1Event && !hasRequest2Event) {
+                val event = fakeProcessor1.nextEvent()
+                hasRequest1Event = hasRequest1Event ||
+                    event.requestSequence?.requests?.contains(request1) ?: false
+                hasRequest2Event = hasRequest2Event ||
+                    event.requestSequence?.requests?.contains(request2) ?: false
+            }
         }
-
-        assertThat(fakeProcessor1.repeatingRequest?.burst).contains(request1)
-        assertThat(fakeProcessor1.requestQueue[0].burst[0]).isSameInstanceAs(request2)
     }
 
     @Test
@@ -315,24 +328,32 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
 
-            graphProcessor.setRepeating(request1)
+            graphProcessor.startRepeating(request1)
             graphProcessor.submit(request2)
 
             // Abort queued and in-flight requests.
             graphProcessor.abort()
-            graphProcessor.attach(fakeProcessor1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+
+            val abortEvent1 = withTimeoutOrNull(timeMillis = 50L) {
+                requestListener1.onAbortedFlow.firstOrNull()
+            }
+            val abortEvent2 = requestListener2.onAbortedFlow.first()
+            val globalAbortEvent = globalListener.onAbortedFlow.first()
+
+            assertThat(abortEvent1).isNull()
+            assertThat(abortEvent2.request).isSameInstanceAs(request2)
+            assertThat(globalAbortEvent.request).isSameInstanceAs(request2)
+
+            val nextSequence = fakeProcessor1.nextRequestSequence()
+            assertThat(nextSequence.requests.first()).isSameInstanceAs(request1)
+            assertThat(nextSequence.requestMetadata[request1]!!.repeating).isTrue()
         }
-
-        assertThat(requestListener1.lastAbortedRequest).isNull()
-        assertThat(requestListener2.lastAbortedRequest).isSameInstanceAs(request2)
-        assertThat(globalListener.lastAbortedRequest).isSameInstanceAs(request2)
-
-        assertThat(fakeProcessor1.repeatingRequest?.burst).contains(request1)
-        assertThat(fakeProcessor1.requestQueue).isEmpty()
     }
 
     @Test
@@ -341,38 +362,40 @@
             val graphProcessor = GraphProcessorImpl(
                 FakeThreads.forTests,
                 graphConfig,
+                graphState3A,
                 this,
                 arrayListOf(globalListener)
             )
             graphProcessor.close()
 
             // Abort queued and in-flight requests.
-            graphProcessor.attach(fakeProcessor1)
-            graphProcessor.setRepeating(request1)
+            graphProcessor.onGraphStarted(fakeProcessor1)
+            graphProcessor.startRepeating(request1)
             graphProcessor.submit(request2)
+
+            val abortEvent1 = withTimeoutOrNull(timeMillis = 50L) {
+                requestListener1.onAbortedFlow.firstOrNull()
+            }
+            val abortEvent2 = requestListener2.onAbortedFlow.first()
+            assertThat(abortEvent1).isNull()
+            assertThat(abortEvent2.request).isSameInstanceAs(request2)
+
+            assertThat(fakeProcessor1.nextEvent().close).isTrue()
         }
-
-        // The repeating request is not aborted
-        assertThat(requestListener1.lastAbortedRequest).isNull()
-        assertThat(requestListener2.lastAbortedRequest).isSameInstanceAs(request2)
-
-        assertThat(fakeProcessor1.closeInvoked).isTrue()
-        assertThat(fakeProcessor1.repeatingRequest).isNull()
-        assertThat(fakeProcessor1.requestQueue).isEmpty()
     }
 
     private suspend fun awaitEvent(
         requestProcessor: FakeRequestProcessor,
         request: Request,
-        filter: (event: Event) -> Boolean
-    ): Event {
+        filter: (event: FakeRequestProcessor.Event) -> Boolean
+    ): FakeRequestProcessor.Event {
 
-        var event: Event
+        var event: FakeRequestProcessor.Event
         var loopCount = 0
         while (loopCount < 10) {
             loopCount++
             event = requestProcessor.nextEvent()
-            val contains = event.request?.burst?.contains(request) ?: false
+            val contains = event.requestSequence?.requests?.contains(request) ?: false
             if (filter(event) && contains) {
                 return event
             }
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Listener3ATest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Listener3ATest.kt
index 9749f1d..354106f 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Listener3ATest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Listener3ATest.kt
@@ -20,7 +20,7 @@
 import android.os.Build
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestNumber
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import androidx.camera.camera2.pipe.testing.FakeRequestMetadata
 import androidx.camera.camera2.pipe.testing.UpdateCounting3AStateListener
@@ -30,7 +30,7 @@
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class Listener3ATest {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerImplTest.kt
index 7849cdd..d9680e9 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/Result3AStateListenerImplTest.kt
@@ -21,7 +21,7 @@
 import androidx.camera.camera2.pipe.FrameNumber
 import androidx.camera.camera2.pipe.RequestNumber
 import androidx.camera.camera2.pipe.Status3A
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeFrameMetadata
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -29,7 +29,7 @@
 import org.junit.runner.RunWith
 import org.robolectric.annotation.Config
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @OptIn(ExperimentalCoroutinesApi::class)
 internal class Result3AStateListenerImplTest {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
index 40fa0a3..f41f0f2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/SessionFactoryTest.kt
@@ -20,15 +20,25 @@
 import android.graphics.SurfaceTexture
 import android.os.Build
 import android.os.Looper
+import android.util.Size
 import android.view.Surface
 import androidx.camera.camera2.pipe.CameraGraph
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.CameraStream
+import androidx.camera.camera2.pipe.RequestProcessor
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
 import androidx.camera.camera2.pipe.testing.FakeGraphProcessor
 import androidx.camera.camera2.pipe.testing.FakeRequestProcessor
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
+import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import dagger.Component
+import dagger.Module
+import dagger.Provides
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.After
@@ -38,41 +48,25 @@
 import org.robolectric.annotation.Config
 import javax.inject.Singleton
 
-@Singleton
-@CameraGraphScope
-@Component(
-    modules = [
-        FakeCameras.FakeCameraGraphModule::class,
-        FakeCameras.FakeCameraPipeModule::class
-    ]
-)
-internal interface CameraSessionTestComponent {
-    fun graphConfig(): CameraGraph.Config
-    fun sessionFactory(): SessionFactory
-    fun streamMap(): StreamGraphImpl
-}
-
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @OptIn(ExperimentalCoroutinesApi::class)
 internal class SessionFactoryTest {
     private val context = ApplicationProvider.getApplicationContext() as Context
     private val mainLooper = Shadows.shadowOf(Looper.getMainLooper())
-    private val cameraId = FakeCameras.create()
-    private val testCamera = FakeCameras.open(cameraId)
+    private val cameraId = RobolectricCameras.create()
+    private val testCamera = RobolectricCameras.open(cameraId)
 
     @After
     fun teardown() {
         mainLooper.idle()
-        FakeCameras.removeAll()
+        RobolectricCameras.clear()
     }
 
     @Test
     fun canCreateSessionFactoryTestComponent() = runBlockingTest {
         val component: CameraSessionTestComponent = DaggerCameraSessionTestComponent.builder()
-            .fakeCameraPipeModule(
-                FakeCameras.FakeCameraPipeModule(context, testCamera)
-            )
+            .fakeCameraPipeModule(FakeCameraPipeModule(context, testCamera))
             .build()
 
         val sessionFactory = component.sessionFactory()
@@ -82,9 +76,7 @@
     @Test
     fun createCameraCaptureSession() = runBlockingTest {
         val component: CameraSessionTestComponent = DaggerCameraSessionTestComponent.builder()
-            .fakeCameraPipeModule(
-                FakeCameras.FakeCameraPipeModule(context, testCamera)
-            )
+            .fakeCameraPipeModule(FakeCameraPipeModule(context, testCamera))
             .build()
 
         val sessionFactory = component.sessionFactory()
@@ -101,12 +93,21 @@
         val surface = Surface(surfaceTexture)
 
         val pendingOutputs = sessionFactory.create(
-            testCamera.cameraDeviceWrapper,
+            AndroidCameraDevice(
+                testCamera.metadata,
+                testCamera.cameraDevice,
+                testCamera.cameraId
+            ),
             mapOf(stream1.id to surface),
             virtualSessionState = VirtualSessionState(
                 FakeGraphProcessor(),
                 sessionFactory,
-                FakeRequestProcessor(GraphState3A()),
+                object : Camera2RequestProcessorFactory {
+                    override fun create(
+                        session: CameraCaptureSessionWrapper,
+                        surfaceMap: Map<StreamId, Surface>
+                    ): RequestProcessor = FakeRequestProcessor()
+                },
                 this
             )
         )
@@ -114,4 +115,50 @@
         assertThat(pendingOutputs).isNotNull()
         assertThat(pendingOutputs).isEmpty()
     }
+}
+
+@Singleton
+@CameraGraphScope
+@Component(
+    modules = [
+        FakeCameraGraphModule::class,
+        FakeCameraPipeModule::class
+    ]
+)
+internal interface CameraSessionTestComponent {
+    fun graphConfig(): CameraGraph.Config
+    fun sessionFactory(): SessionFactory
+    fun streamMap(): StreamGraphImpl
+}
+
+/**
+ * Utility module for testing the Dagger generated graph with a a reasonable default config.
+ */
+@Module(includes = [CameraPipeModules::class, Camera2CameraPipeModules::class])
+class FakeCameraPipeModule(
+    private val context: Context,
+    private val fakeCamera: RobolectricCameras.FakeCamera
+) {
+    @Provides
+    fun provideFakeCamera() = fakeCamera
+
+    @Provides
+    @Singleton
+    fun provideFakeCameraPipeConfig() = CameraPipe.Config(context)
+}
+
+@Module(includes = [CameraGraphModules::class, Camera2CameraGraphModules::class])
+class FakeCameraGraphModule {
+    @Provides
+    @CameraGraphScope
+    fun provideFakeGraphConfig(fakeCamera: RobolectricCameras.FakeCamera): CameraGraph.Config {
+        val stream = CameraStream.Config.create(
+            Size(640, 480),
+            StreamFormat.YUV_420_888
+        )
+        return CameraGraph.Config(
+            camera = fakeCamera.cameraId,
+            streams = listOf(stream),
+        )
+    }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
index 8391d35..84d53da 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/StreamGraphImplTest.kt
@@ -28,7 +28,7 @@
 import androidx.camera.camera2.pipe.OutputStream
 import androidx.camera.camera2.pipe.StreamFormat
 import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
 import androidx.camera.camera2.pipe.testing.FakeCameraMetadata
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
@@ -36,7 +36,7 @@
 import org.robolectric.annotation.Config
 import org.robolectric.annotation.internal.DoNotInstrument
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 internal class StreamGraphImplTest {
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
index 60f9c9b..f697d6cd 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/impl/VirtualCameraTest.kt
@@ -19,8 +19,9 @@
 import android.os.Build
 import android.os.Looper.getMainLooper
 import androidx.camera.camera2.pipe.core.Timestamps
-import androidx.camera.camera2.pipe.testing.CameraPipeRobolectricTestRunner
-import androidx.camera.camera2.pipe.testing.FakeCameras
+import androidx.camera.camera2.pipe.testing.RobolectricCameraPipeTestRunner
+import androidx.camera.camera2.pipe.testing.RobolectricCameras
+import androidx.camera.camera2.pipe.wrapper.AndroidCameraDevice
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -38,18 +39,18 @@
 import org.robolectric.annotation.Config
 import java.util.concurrent.TimeUnit
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @OptIn(ExperimentalCoroutinesApi::class)
 internal class VirtualCameraStateTest {
     private val mainLooper = shadowOf(getMainLooper())
-    private val cameraId = FakeCameras.create()
-    private val testCamera = FakeCameras.open(cameraId)
+    private val cameraId = RobolectricCameras.create()
+    private val testCamera = RobolectricCameras.open(cameraId)
 
     @After
     fun teardown() {
         mainLooper.idle()
-        FakeCameras.removeAll()
+        RobolectricCameras.clear()
     }
 
     @Test
@@ -81,7 +82,15 @@
         // changes that it receives those changes and can be subsequently disconnected, which stops
         // additional events from being passed to the virtual camera instance.
         val virtualCamera = VirtualCameraState(cameraId)
-        val cameraState = flowOf(CameraStateOpen(testCamera.cameraDeviceWrapper))
+        val cameraState = flowOf(
+            CameraStateOpen(
+                AndroidCameraDevice(
+                    testCamera.metadata,
+                    testCamera.cameraDevice,
+                    testCamera.cameraId
+                )
+            )
+        )
         virtualCamera.connect(
             cameraState,
             object : Token {
@@ -108,7 +117,13 @@
         // of the events, starting from CameraStateUnopened.
         val virtualCamera = VirtualCameraState(cameraId)
         val states = listOf(
-            CameraStateOpen(testCamera.cameraDeviceWrapper),
+            CameraStateOpen(
+                AndroidCameraDevice(
+                    testCamera.metadata,
+                    testCamera.cameraDevice,
+                    testCamera.cameraId
+                )
+            ),
             CameraStateClosing,
             CameraStateClosed(
                 cameraId,
@@ -142,18 +157,18 @@
     }
 }
 
-@RunWith(CameraPipeRobolectricTestRunner::class)
+@RunWith(RobolectricCameraPipeTestRunner::class)
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 @OptIn(ExperimentalCoroutinesApi::class)
 internal class AndroidCameraDeviceTest {
     private val mainLooper = shadowOf(getMainLooper())
-    private val cameraId = FakeCameras.create()
-    private val testCamera = FakeCameras.open(cameraId)
+    private val cameraId = RobolectricCameras.create()
+    private val testCamera = RobolectricCameras.open(cameraId)
     private val now = Timestamps.now()
 
     @After
     fun teardown() {
-        FakeCameras.removeAll()
+        RobolectricCameras.clear()
     }
 
     @Test
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphState.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphController.kt
similarity index 85%
rename from camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphState.kt
rename to camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphController.kt
index 08a8530..8c7e2f2 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphState.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphController.kt
@@ -16,9 +16,9 @@
 
 package androidx.camera.camera2.pipe.testing
 
-import androidx.camera.camera2.pipe.impl.GraphState
+import androidx.camera.camera2.pipe.impl.GraphController
 
-internal class FakeGraphState : GraphState {
+internal class FakeGraphController : GraphController {
     var active = false
     var reconfigured = false
 
@@ -30,7 +30,7 @@
         active = false
     }
 
-    override fun reconfigure() {
+    override fun restart() {
         reconfigured = true
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
index dde3ba50..a848572 100644
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
+++ b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeGraphProcessor.kt
@@ -18,12 +18,17 @@
 
 import androidx.camera.camera2.pipe.Request
 import androidx.camera.camera2.pipe.impl.GraphProcessor
-import androidx.camera.camera2.pipe.impl.RequestProcessor
+import androidx.camera.camera2.pipe.impl.GraphState3A
+import androidx.camera.camera2.pipe.RequestProcessor
 
 /**
  * Fake implementation of a [GraphProcessor] for tests.
  */
-internal class FakeGraphProcessor : GraphProcessor {
+internal class FakeGraphProcessor(
+    private val graphState3A: GraphState3A = GraphState3A(),
+    private val defaultParameters: Map<*, Any?> = emptyMap<Any, Any?>(),
+    private val defaultListeners: List<Request.Listener> = emptyList()
+) : GraphProcessor {
     var active = true
         private set
     var closed = false
@@ -35,12 +40,15 @@
 
     private val _requestQueue = mutableListOf<List<Request>>()
     private var processor: RequestProcessor? = null
-    private val defaultParameters = mapOf<Any, Any>()
 
-    override fun setRepeating(request: Request) {
+    override fun startRepeating(request: Request) {
         repeatingRequest = request
     }
 
+    override fun stopRepeating() {
+        repeatingRequest = null
+    }
+
     override fun submit(request: Request) {
         submit(listOf(request))
     }
@@ -55,18 +63,22 @@
         }
         val currProcessor = processor
         val currRepeatingRequest = repeatingRequest
+        val requiredParameters = parameters.toMutableMap()
+            .also { it.putAll(graphState3A.readState()) }
         return when {
             currProcessor == null || currRepeatingRequest == null -> false
             else -> currProcessor.submit(
                 currRepeatingRequest,
                 defaultParameters = defaultParameters,
-                requiredParameters = parameters
+                requiredParameters = requiredParameters,
+                defaultListeners = defaultListeners
             )
         }
     }
 
     override fun abort() {
         _requestQueue.clear()
+        // TODO: Invoke abort on the listeners in the queue.
     }
 
     override fun close() {
@@ -78,13 +90,13 @@
         _requestQueue.clear()
     }
 
-    override fun attach(requestProcessor: RequestProcessor) {
+    override fun onGraphStarted(requestProcessor: RequestProcessor) {
         val old = processor
         processor = requestProcessor
         old?.close()
     }
 
-    override fun detach(requestProcessor: RequestProcessor) {
+    override fun onGraphStopped(requestProcessor: RequestProcessor) {
         val old = processor
         if (requestProcessor === old) {
             processor = null
@@ -92,7 +104,28 @@
         }
     }
 
+    override fun onGraphModified(requestProcessor: RequestProcessor) {
+        invalidate()
+    }
+
     override fun invalidate() {
-        processor!!.setRepeating(repeatingRequest!!, defaultParameters, mapOf<Any, Any>())
+        if (closed) {
+            return
+        }
+
+        val currProcessor = processor
+        val currRepeatingRequest = repeatingRequest
+        val requiredParameters = graphState3A.readState()
+
+        if (currProcessor == null || currRepeatingRequest == null) {
+            return
+        }
+
+        currProcessor.startRepeating(
+            currRepeatingRequest,
+            defaultParameters = defaultParameters,
+            requiredParameters = requiredParameters,
+            defaultListeners = defaultListeners
+        )
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt b/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
deleted file mode 100644
index 006a544..0000000
--- a/camera/camera-camera2-pipe/src/test/java/androidx/camera/camera2/pipe/testing/FakeRequestProcessor.kt
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.pipe.testing
-
-import android.view.Surface
-import androidx.camera.camera2.pipe.Request
-import androidx.camera.camera2.pipe.StreamId
-import androidx.camera.camera2.pipe.impl.GraphState3A
-import androidx.camera.camera2.pipe.impl.RequestProcessorFactory
-import androidx.camera.camera2.pipe.impl.RequestProcessor
-import androidx.camera.camera2.pipe.impl.TokenLock
-import androidx.camera.camera2.pipe.impl.TokenLockImpl
-import androidx.camera.camera2.pipe.wrapper.CameraCaptureSessionWrapper
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.withTimeout
-
-/**
- * Fake implementation of a [RequestProcessor] for tests.
- */
-internal class FakeRequestProcessor(private val graphState3A: GraphState3A) :
-    RequestProcessor, RequestProcessorFactory {
-    private val eventChannel = Channel<Event>(Channel.UNLIMITED)
-
-    val requestQueue: MutableList<FakeRequest> = mutableListOf()
-    var repeatingRequest: FakeRequest? = null
-
-    var rejectRequests = false
-    var abortInvoked = false
-        private set
-    var closeInvoked = false
-        private set
-    var stopInvoked = false
-        private set
-
-    var captureSession: CameraCaptureSessionWrapper? = null
-    var surfaceMap: Map<StreamId, Surface>? = null
-
-    private val tokenLock = TokenLockImpl(1)
-    private var token: TokenLock.Token? = null
-
-    data class FakeRequest(
-        val burst: List<Request>,
-        val defaultParameters: Map<*, Any>,
-        val internalParameters: Map<*, Any>,
-        val requiredParameters: Map<*, Any>,
-        val parameters: Map<*, Any>,
-        val requireStreams: Boolean = false
-    )
-
-    override fun create(
-        session: CameraCaptureSessionWrapper,
-        surfaceMap: Map<StreamId, Surface>
-    ): RequestProcessor {
-        captureSession = session
-        this.surfaceMap = surfaceMap
-        return this
-    }
-
-    override fun submit(
-        request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean {
-        val fakeRequest =
-            createFakeRequest(listOf(request), defaultParameters, requiredParameters)
-
-        if (rejectRequests || closeInvoked) {
-            check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
-            return false
-        }
-
-        requestQueue.add(fakeRequest)
-        check(eventChannel.offer(Event(request = fakeRequest, submit = true)))
-
-        return true
-    }
-
-    override fun submit(
-        requests: List<Request>,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean {
-        val fakeRequest =
-            createFakeRequest(requests, defaultParameters, requiredParameters)
-        if (rejectRequests || closeInvoked) {
-            check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
-            return false
-        }
-
-        requestQueue.add(fakeRequest)
-        check(eventChannel.offer(Event(request = fakeRequest, submit = true)))
-
-        return true
-    }
-
-    override fun setRepeating(
-        request: Request,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): Boolean {
-        val fakeRequest =
-            createFakeRequest(listOf(request), defaultParameters, requiredParameters)
-        if (rejectRequests || closeInvoked) {
-            check(eventChannel.offer(Event(request = fakeRequest, rejected = true)))
-            return false
-        }
-
-        repeatingRequest = fakeRequest
-        check(eventChannel.offer(Event(request = fakeRequest, setRepeating = true)))
-        return true
-    }
-
-    override fun abortCaptures() {
-        abortInvoked = true
-        check(eventChannel.offer(Event(abort = true)))
-    }
-
-    override fun stopRepeating() {
-        stopInvoked = true
-        check(eventChannel.offer(Event(stop = true)))
-    }
-
-    override fun close() {
-        closeInvoked = true
-        check(eventChannel.offer(Event(close = true)))
-    }
-
-    /**
-     * Get the next event from queue with an option to specify a timeout for tests.
-     */
-    suspend fun nextEvent(timeMillis: Long = 500): Event = withTimeout(timeMillis) {
-        eventChannel.receive()
-    }
-
-    private fun createFakeRequest(
-        burst: List<Request>,
-        defaultParameters: Map<*, Any>,
-        requiredParameters: Map<*, Any>
-    ): FakeRequest {
-        val internalParameters = graphState3A.readState()
-        val parameterMap = mutableMapOf<Any, Any>()
-        for ((k, v) in defaultParameters) {
-            if (k != null) {
-                parameterMap[k] = v
-            }
-        }
-        parameterMap.putAll(internalParameters)
-        for ((k, v) in requiredParameters) {
-            if (k != null) {
-                parameterMap[k] = v
-            }
-        }
-        return FakeRequest(
-            burst,
-            defaultParameters,
-            internalParameters,
-            requiredParameters,
-            parameterMap
-        )
-    }
-}
-
-internal data class Event(
-    val request: FakeRequestProcessor.FakeRequest? = null,
-    val rejected: Boolean = false,
-    val abort: Boolean = false,
-    val close: Boolean = false,
-    val stop: Boolean = false,
-    val submit: Boolean = false,
-    val setRepeating: Boolean = false
-)
\ No newline at end of file
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
index c5637ee..5e97281 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/SamsungPreviewTargetAspectRatioQuirk.java
@@ -27,17 +27,21 @@
 import java.util.List;
 
 /**
- * Quirk that produces stretched preview on certain Samsung devices.
+ * Quirk that produces stretched or incorrect FOV preview on certain Samsung devices.
  *
  * <p> On certain Samsung devices, the HAL provides 16:9 preview even when the Surface size is
  * set to 4:3, which causes the preview to be stretched in PreviewView.
+ *
+ * <p> On certain Samsung devices, the HAL crops the preview images from the cropped 16:9 area but
+ * not the full active array area, which causes the preview FOV incorrect when using non-16:9 sizes.
  */
 public class SamsungPreviewTargetAspectRatioQuirk implements Quirk {
 
     // List of devices with the issue.
     private static final List<String> DEVICE_MODELS = Arrays.asList(
             "SM-J710MN", // b/170762209
-            "SM-T580" // b/169471824
+            "SM-T580", // b/169471824
+            "SM-J327U" // b/176474000
     );
 
     static boolean load() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
index 8049c3b..9d98353 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/TargetAspectRatioTest.java
@@ -84,6 +84,15 @@
         data.add(new Object[]{new Config("Samsung", "SM-T580", false,
                 INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
                 ALL_API_LEVELS)});
+        data.add(new Object[]{new Config("Samsung", "SM-J327U", true,
+                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_16_9, ALL_API_LEVELS)});
+        data.add(new Object[]{new Config("Samsung", "SM-J327U", true,
+                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_16_9, ALL_API_LEVELS)});
+        data.add(new Object[]{new Config("Samsung", "SM-J327U", false,
+                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_4_3, RATIO_ORIGINAL, ALL_API_LEVELS)});
+        data.add(new Object[]{new Config("Samsung", "SM-J327U", false,
+                INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED, RATIO_16_9, RATIO_ORIGINAL,
+                ALL_API_LEVELS)});
         data.add(new Object[]{new Config("Google", "Nexus 4", true,
                 INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY, RATIO_4_3, RATIO_MAX_JPEG,
                 new Range<>(21, 22))});
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
index 1af8754..f2e0369 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/PreviewTransformationDeviceTest.kt
@@ -30,6 +30,7 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.math.roundToInt
 
 /**
  * Instrument tests for [PreviewTransformation].
@@ -117,15 +118,15 @@
     @Test
     public fun correctTextureViewWith0Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_0)).isEqualTo(
-            floatArrayOf(
-                0f,
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                SURFACE_SIZE.height.toFloat()
+            intArrayOf(
+                0,
+                0,
+                SURFACE_SIZE.width,
+                0,
+                SURFACE_SIZE.width,
+                SURFACE_SIZE.height,
+                0,
+                SURFACE_SIZE.height
             )
         )
     }
@@ -133,15 +134,15 @@
     @Test
     public fun correctTextureViewWith90Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_90)).isEqualTo(
-            floatArrayOf(
-                0f,
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                SURFACE_SIZE.height.toFloat()
+            intArrayOf(
+                0,
+                SURFACE_SIZE.height,
+                0,
+                0,
+                SURFACE_SIZE.width,
+                0,
+                SURFACE_SIZE.width,
+                SURFACE_SIZE.height
             )
         )
     }
@@ -149,15 +150,15 @@
     @Test
     public fun correctTextureViewWith180Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_180)).isEqualTo(
-            floatArrayOf(
-                SURFACE_SIZE.width.toFloat(),
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                0f
+            intArrayOf(
+                SURFACE_SIZE.width,
+                SURFACE_SIZE.height,
+                0,
+                SURFACE_SIZE.height,
+                0,
+                0,
+                SURFACE_SIZE.width,
+                0
             )
         )
     }
@@ -165,15 +166,15 @@
     @Test
     public fun correctTextureViewWith270Rotation() {
         assertThat(getTextureViewCorrection(Surface.ROTATION_270)).isEqualTo(
-            floatArrayOf(
-                SURFACE_SIZE.width.toFloat(),
-                0f,
-                SURFACE_SIZE.width.toFloat(),
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                SURFACE_SIZE.height.toFloat(),
-                0f,
-                0f
+            intArrayOf(
+                SURFACE_SIZE.width,
+                0,
+                SURFACE_SIZE.width,
+                SURFACE_SIZE.height,
+                0,
+                SURFACE_SIZE.height,
+                0,
+                0
             )
         )
     }
@@ -181,7 +182,7 @@
     /**
      * Corrects TextureView based on target rotation and return the corrected vertices.
      */
-    private fun getTextureViewCorrection(@RotationValue rotation: Int): FloatArray {
+    private fun getTextureViewCorrection(@RotationValue rotation: Int): IntArray {
         // Arrange.
         mPreviewTransform.setTransformationInfo(
             SurfaceRequest.TransformationInfo.of(CROP_RECT, 90, rotation),
@@ -192,7 +193,19 @@
         // Act.
         val surfaceVertexes = PreviewTransformation.sizeToVertices(SURFACE_SIZE)
         mPreviewTransform.textureViewCorrectionMatrix.mapPoints(surfaceVertexes)
-        return surfaceVertexes
+        return convertToIntArray(surfaceVertexes)
+    }
+
+    private fun convertToIntArray(elements: FloatArray): IntArray {
+        var result = IntArray(elements.size)
+        var index = 0
+
+        for (element in elements) {
+            result.set(index, element.roundToInt())
+            index++
+        }
+
+        return result
     }
 
     @Test
diff --git a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
index c5bb347..aa66295 100644
--- a/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
+++ b/camera/integration-tests/camerapipetestapp/src/main/java/androidx/camera/integration/camera2/pipe/SimpleCamera.kt
@@ -123,7 +123,7 @@
             cameraGraph.setSurface(yuvStream.id, imageReader.surface)
 
             cameraGraph.acquireSessionOrNull()!!.use {
-                it.setRepeating(
+                it.startRepeating(
                     Request(
                         streams = listOf(viewfinderStream.id, yuvStream.id)
                     )
diff --git a/car/app/app/src/main/java/androidx/car/app/FailureResponse.java b/car/app/app/src/main/java/androidx/car/app/FailureResponse.java
index a4aede9..e066ce2 100644
--- a/car/app/app/src/main/java/androidx/car/app/FailureResponse.java
+++ b/car/app/app/src/main/java/androidx/car/app/FailureResponse.java
@@ -24,6 +24,7 @@
 import android.util.Log;
 
 import androidx.annotation.IntDef;
+import androidx.annotation.Keep;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
@@ -68,8 +69,10 @@
     public static final int RUNTIME_EXCEPTION = 5;
     public static final int REMOTE_EXCEPTION = 6;
 
+    @Keep
     @Nullable
     private final String mStackTrace;
+    @Keep
     @ErrorType
     private final int mErrorType;
 
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
index 7a9a0de..ffec340 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/example1/Main.kt
@@ -27,10 +27,12 @@
 import androidx.compose.foundation.VerticalScrollbar
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredHeight
@@ -83,6 +85,7 @@
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.fontFamily
 import androidx.compose.ui.text.platform.font
 import androidx.compose.ui.text.style.TextAlign
@@ -261,6 +264,35 @@
             overflow = TextOverflow.Ellipsis
         )
 
+        Row(
+            modifier = Modifier.fillMaxWidth(),
+            horizontalArrangement = Arrangement.SpaceEvenly
+        ) {
+            Text(
+                "Default",
+            )
+
+            Text(
+                "SansSerif",
+                fontFamily = FontFamily.SansSerif
+            )
+
+            Text(
+                "Serif",
+                fontFamily = FontFamily.Serif
+            )
+
+            Text(
+                "Monospace",
+                fontFamily = FontFamily.Monospace
+            )
+
+            Text(
+                "Cursive",
+                fontFamily = FontFamily.Cursive
+            )
+        }
+
         var overText by remember { mutableStateOf("Move mouse over text:") }
         Text(overText, style = TextStyle(letterSpacing = 10.sp))
 
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
index 207fd7c..08f91a3 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/AlignmentLineTest.kt
@@ -86,7 +86,7 @@
                     childDp, 0.dp, testLine, lineDp,
                     Modifier.onGloballyPositioned {
                         childSize.value = it.size
-                        childPosition.value = it.positionInRoot
+                        childPosition.value = it.positionInRoot()
                         layoutLatch.countDown()
                     }.paddingFrom(testLine, beforeDp, afterDp)
                 )
@@ -132,7 +132,7 @@
                     0.dp, childDp, testLine, lineDp,
                     Modifier.onGloballyPositioned {
                         childSize.value = it.size
-                        childPosition.value = it.positionInRoot
+                        childPosition.value = it.positionInRoot()
                         layoutLatch.countDown()
                     }.paddingFrom(testLine, beforeDp, afterDp)
                 )
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
index ca0995c..82c219c 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/ConstraintLayoutTest.kt
@@ -394,7 +394,7 @@
                         }
                         .preferredSize(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[0].value = it.positionInRoot
+                            position[0].value = it.positionInRoot()
                         }
                 )
                 val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
@@ -406,7 +406,7 @@
                         }
                         .preferredSize(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[1].value = it.positionInRoot
+                            position[1].value = it.positionInRoot()
                         }
                 )
                 Box(
@@ -417,7 +417,7 @@
                         }
                         .preferredSize(boxSize.toDp(), boxSize.toDp())
                         .onGloballyPositioned {
-                            position[2].value = it.positionInRoot
+                            position[2].value = it.positionInRoot()
                         }
                 )
             }
@@ -486,7 +486,7 @@
                     Box(
                         Modifier.layoutId("box$i").preferredSize(boxSize.toDp(), boxSize.toDp())
                             .onGloballyPositioned {
-                                position[i].value = it.positionInRoot
+                                position[i].value = it.positionInRoot()
                             }
                     )
                 }
@@ -539,7 +539,7 @@
                             }
                             .preferredSize(boxSize.toDp(), boxSize.toDp())
                             .onGloballyPositioned {
-                                position[0].value = it.positionInRoot
+                                position[0].value = it.positionInRoot()
                             }
                     )
                     val half = createGuidelineFromAbsoluteLeft(fraction = 0.5f)
@@ -551,7 +551,7 @@
                             }
                             .preferredSize(boxSize.toDp(), boxSize.toDp())
                             .onGloballyPositioned {
-                                position[1].value = it.positionInRoot
+                                position[1].value = it.positionInRoot()
                             }
                     )
                     Box(
@@ -562,7 +562,7 @@
                             }
                             .preferredSize(boxSize.toDp(), boxSize.toDp())
                             .onGloballyPositioned {
-                                position[2].value = it.positionInRoot
+                                position[2].value = it.positionInRoot()
                             }
                     )
                 }
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
index 4d70733..cbca1c0 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/OffsetTest.kt
@@ -81,8 +81,8 @@
                     .wrapContentSize(Alignment.TopStart)
                     .offset(offsetX, offsetY)
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        positionX = coordinates.positionInRoot.x.roundToInt()
-                        positionY = coordinates.positionInRoot.y.roundToInt()
+                        positionX = coordinates.positionInRoot().x.roundToInt()
+                        positionY = coordinates.positionInRoot().y.roundToInt()
                     }
             ) {
             }
@@ -112,8 +112,8 @@
                         .wrapContentSize(Alignment.TopStart)
                         .offset(offsetX, offsetY)
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x.roundToInt()
-                            positionY = coordinates.positionInRoot.y.roundToInt()
+                            positionX = coordinates.positionInRoot().x.roundToInt()
+                            positionY = coordinates.positionInRoot().y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
@@ -141,8 +141,8 @@
                     .wrapContentSize(Alignment.TopStart)
                     .absoluteOffset(offsetX, offsetY)
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        positionX = coordinates.positionInRoot.x.roundToInt()
-                        positionY = coordinates.positionInRoot.y.roundToInt()
+                        positionX = coordinates.positionInRoot().x.roundToInt()
+                        positionY = coordinates.positionInRoot().y.roundToInt()
                     }
             ) {
             }
@@ -172,8 +172,8 @@
                         .wrapContentSize(Alignment.TopStart)
                         .absoluteOffset(offsetX, offsetY)
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x.roundToInt()
-                            positionY = coordinates.positionInRoot.y.roundToInt()
+                            positionX = coordinates.positionInRoot().x.roundToInt()
+                            positionY = coordinates.positionInRoot().y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
@@ -201,8 +201,8 @@
                     .wrapContentSize(Alignment.TopStart)
                     .offset { IntOffset(offsetX.roundToInt(), offsetY.roundToInt()) }
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        positionX = coordinates.positionInRoot.x
-                        positionY = coordinates.positionInRoot.y
+                        positionX = coordinates.positionInRoot().x
+                        positionY = coordinates.positionInRoot().y
                     }
             ) {
             }
@@ -232,8 +232,8 @@
                         .wrapContentSize(Alignment.TopStart)
                         .offset { IntOffset(offsetX, offsetY) }
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x.roundToInt()
-                            positionY = coordinates.positionInRoot.y.roundToInt()
+                            positionX = coordinates.positionInRoot().x.roundToInt()
+                            positionY = coordinates.positionInRoot().y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
@@ -264,8 +264,8 @@
                     .wrapContentSize(Alignment.TopStart)
                     .absoluteOffset { IntOffset(offsetX, offsetY) }
                     .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                        positionX = coordinates.positionInRoot.x.roundToInt()
-                        positionY = coordinates.positionInRoot.y.roundToInt()
+                        positionX = coordinates.positionInRoot().x.roundToInt()
+                        positionY = coordinates.positionInRoot().y.roundToInt()
                     }
             ) {
             }
@@ -295,8 +295,8 @@
                         .wrapContentSize(Alignment.TopStart)
                         .absoluteOffset { IntOffset(offsetX, offsetY) }
                         .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                            positionX = coordinates.positionInRoot.x.roundToInt()
-                            positionY = coordinates.positionInRoot.y.roundToInt()
+                            positionX = coordinates.positionInRoot().x.roundToInt()
+                            positionY = coordinates.positionInRoot().y.roundToInt()
                         }
                 ) {
                     // TODO(soboleva): this box should not be needed after b/154758475 is fixed.
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
index 4f66539..7df407d 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/PaddingTest.kt
@@ -267,7 +267,7 @@
                             .preferredSize(sizeDp, sizeDp)
                             .onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize[0] = coordinates.size
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -278,7 +278,7 @@
                             .preferredSize(sizeDp, sizeDp)
                             .onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize[1] = coordinates.size
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -289,7 +289,7 @@
                             .preferredSize(sizeDp, sizeDp)
                             .onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize[2] = coordinates.size
-                                childPosition[2] = coordinates.positionInRoot
+                                childPosition[2] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -342,7 +342,7 @@
                         Modifier.absolutePadding(left = padding1Dp, right = padding2Dp)
                             .preferredSize(sizeDp, sizeDp)
                             .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -351,7 +351,7 @@
                         Modifier.absolutePadding(right = padding3Dp)
                             .preferredSize(sizeDp, sizeDp)
                             .onGloballyPositioned { coordinates: LayoutCoordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -439,7 +439,7 @@
                         Container(
                             Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize = coordinates.size
-                                childPosition = coordinates.positionInRoot
+                                childPosition = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                         ) {
@@ -487,7 +487,7 @@
                         Container(
                             Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize = coordinates.size
-                                childPosition = coordinates.positionInRoot
+                                childPosition = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                         ) {
@@ -542,7 +542,7 @@
                         Container(
                             Modifier.onGloballyPositioned { coordinates: LayoutCoordinates ->
                                 childSize = coordinates.size
-                                childPosition = coordinates.positionInRoot
+                                childPosition = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                         ) {
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
index b9a80bf..04eb1a3 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/RowColumnTest.kt
@@ -88,7 +88,7 @@
                         height = sizeDp,
                         modifier = Modifier.onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -99,7 +99,7 @@
                         height = (sizeDp * 2),
                         modifier = Modifier.onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -137,7 +137,7 @@
                         Modifier.weight(1f)
                             .onGloballyPositioned { coordinates ->
                                 childSize[0] = coordinates.size
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -149,7 +149,7 @@
                         Modifier.weight(2f)
                             .onGloballyPositioned { coordinates ->
                                 childSize[1] = coordinates.size
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -194,7 +194,7 @@
                         Modifier.weight(1f, fill = false)
                             .onGloballyPositioned { coordinates ->
                                 childSize[0] = coordinates.size
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -206,7 +206,7 @@
                         Modifier.weight(2f, fill = false)
                             .onGloballyPositioned { coordinates ->
                                 childSize[1] = coordinates.size
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -243,7 +243,7 @@
                         height = sizeDp,
                         modifier = Modifier.onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -253,7 +253,7 @@
                         height = (sizeDp * 2),
                         modifier = Modifier.onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -291,7 +291,7 @@
                         Modifier.weight(1f)
                             .onGloballyPositioned { coordinates ->
                                 childSize[0] = coordinates.size
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -303,7 +303,7 @@
                         Modifier.weight(2f)
                             .onGloballyPositioned { coordinates ->
                                 childSize[1] = coordinates.size
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -346,7 +346,7 @@
                         Modifier.weight(1f, fill = false)
                             .onGloballyPositioned { coordinates ->
                                 childSize[0] = coordinates.size
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -357,7 +357,7 @@
                         Modifier.weight(2f, fill = false)
                             .onGloballyPositioned { coordinates ->
                                 childSize[1] = coordinates.size
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             },
                         width = width,
@@ -401,7 +401,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             width[0] = coordinates.size.width.toFloat()
-                            x[0] = coordinates.positionInRoot.x
+                            x[0] = coordinates.positionInRoot().x
                             latch.countDown()
                         }
                 ) {
@@ -410,7 +410,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             width[1] = coordinates.size.width.toFloat()
-                            x[1] = coordinates.positionInRoot.x
+                            x[1] = coordinates.positionInRoot().x
                             latch.countDown()
                         }
                 ) {
@@ -447,7 +447,7 @@
                     Modifier.weight(2f)
                         .onGloballyPositioned { coordinates ->
                             width[0] = coordinates.size.width.toFloat()
-                            x[0] = coordinates.positionInRoot.x
+                            x[0] = coordinates.positionInRoot().x
                             latch.countDown()
                         }
                 ) {
@@ -456,7 +456,7 @@
                     Modifier.weight(2f)
                         .onGloballyPositioned { coordinates ->
                             width[1] = coordinates.size.width.toFloat()
-                            x[1] = coordinates.positionInRoot.x
+                            x[1] = coordinates.positionInRoot().x
                             latch.countDown()
                         }
                 ) {
@@ -465,7 +465,7 @@
                     Modifier.weight(3f)
                         .onGloballyPositioned { coordinates ->
                             width[2] = coordinates.size.width.toFloat()
-                            x[2] = coordinates.positionInRoot.x
+                            x[2] = coordinates.positionInRoot().x
                             latch.countDown()
                         }
                 ) {
@@ -503,7 +503,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             height[0] = coordinates.size.height.toFloat()
-                            y[0] = coordinates.positionInRoot.y
+                            y[0] = coordinates.positionInRoot().y
                             latch.countDown()
                         }
                 ) {
@@ -512,7 +512,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             height[1] = coordinates.size.height.toFloat()
-                            y[1] = coordinates.positionInRoot.y
+                            y[1] = coordinates.positionInRoot().y
                             latch.countDown()
                         }
                 ) {
@@ -521,7 +521,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             height[2] = coordinates.size.height.toFloat()
-                            y[2] = coordinates.positionInRoot.y
+                            y[2] = coordinates.positionInRoot().y
                             latch.countDown()
                         }
                 ) {
@@ -559,7 +559,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             height[0] = coordinates.size.height.toFloat()
-                            y[0] = coordinates.positionInRoot.y
+                            y[0] = coordinates.positionInRoot().y
                             latch.countDown()
                         }
                 ) {
@@ -568,7 +568,7 @@
                     Modifier.weight(1f)
                         .onGloballyPositioned { coordinates ->
                             height[1] = coordinates.size.height.toFloat()
-                            y[1] = coordinates.positionInRoot.y
+                            y[1] = coordinates.positionInRoot().y
                             latch.countDown()
                         }
                 ) {
@@ -603,7 +603,7 @@
                     modifier = Modifier.fillMaxHeight()
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -615,7 +615,7 @@
                     modifier = Modifier.fillMaxHeight()
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -652,7 +652,7 @@
                     modifier = Modifier.align(Alignment.Top)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -662,7 +662,7 @@
                     height = sizeDp,
                     modifier = Modifier.onGloballyPositioned { coordinates ->
                         childSize[1] = coordinates.size
-                        childPosition[1] = coordinates.positionInRoot
+                        childPosition[1] = coordinates.positionInRoot()
                         drawLatch.countDown()
                     }
                 ) {
@@ -673,7 +673,7 @@
                     modifier = Modifier.align(Alignment.Bottom)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -724,7 +724,7 @@
                     modifier = Modifier.align(Alignment.Top)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -735,7 +735,7 @@
                     modifier = Modifier.align(Alignment.CenterVertically)
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -746,7 +746,7 @@
                     modifier = Modifier.align(Alignment.Bottom)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -801,7 +801,7 @@
                     modifier = Modifier.alignBy(TestHorizontalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -812,7 +812,7 @@
                     modifier = Modifier.alignBy { it.height / 2 }
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -824,7 +824,7 @@
                     modifier = Modifier.alignBy(TestHorizontalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -835,7 +835,7 @@
                     modifier = Modifier.alignBy { it.height * 3 / 4 }
                         .onGloballyPositioned { coordinates ->
                             childSize[3] = coordinates.size
-                            childPosition[3] = coordinates.positionInRoot
+                            childPosition[3] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -848,7 +848,7 @@
                     modifier = Modifier.alignByBaseline()
                         .onGloballyPositioned { coordinates ->
                             childSize[4] = coordinates.size
-                            childPosition[4] = coordinates.positionInRoot
+                            childPosition[4] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -907,7 +907,7 @@
                     modifier = Modifier.alignBy(TestHorizontalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -918,7 +918,7 @@
                         .weight(1f)
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -955,7 +955,7 @@
                     modifier = Modifier.fillMaxWidth()
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -967,7 +967,7 @@
                     modifier = Modifier.fillMaxWidth()
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1004,7 +1004,7 @@
                     modifier = Modifier.align(Alignment.Start)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1015,7 +1015,7 @@
                     modifier = Modifier.align(Alignment.CenterHorizontally)
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1026,7 +1026,7 @@
                     modifier = Modifier.align(Alignment.End)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1074,7 +1074,7 @@
                     modifier = Modifier.align(Alignment.Start)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1084,7 +1084,7 @@
                     height = sizeDp,
                     modifier = Modifier.onGloballyPositioned { coordinates ->
                         childSize[1] = coordinates.size
-                        childPosition[1] = coordinates.positionInRoot
+                        childPosition[1] = coordinates.positionInRoot()
                         drawLatch.countDown()
                     }
                 ) {
@@ -1095,7 +1095,7 @@
                     modifier = Modifier.align(Alignment.End)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1145,7 +1145,7 @@
                     modifier = Modifier.alignBy { it.width }
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1156,7 +1156,7 @@
                     modifier = Modifier.alignBy { 0 }
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1168,7 +1168,7 @@
                     modifier = Modifier.alignBy(TestVerticalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[2] = coordinates.size
-                            childPosition[2] = coordinates.positionInRoot
+                            childPosition[2] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1180,7 +1180,7 @@
                     modifier = Modifier.alignBy(TestVerticalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[3] = coordinates.size
-                            childPosition[3] = coordinates.positionInRoot
+                            childPosition[3] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1233,7 +1233,7 @@
                     modifier = Modifier.alignBy(TestVerticalLine)
                         .onGloballyPositioned { coordinates ->
                             childSize[0] = coordinates.size
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -1244,7 +1244,7 @@
                         .weight(1f)
                         .onGloballyPositioned { coordinates ->
                             childSize[1] = coordinates.size
-                            childPosition[1] = coordinates.positionInRoot
+                            childPosition[1] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                 ) {
@@ -2878,7 +2878,7 @@
     ) {
         for (i in childPosition.indices) {
             childPosition[i] = parentLayoutCoordinates!!
-                .childToLocal(childLayoutCoordinates[i]!!, Offset(0f, 0f))
+                .localPositionOf(childLayoutCoordinates[i]!!, Offset(0f, 0f))
         }
     }
 
@@ -3998,7 +3998,7 @@
                         .alignBy { it.height / 2 }
                         .onGloballyPositioned { coordinates ->
                             containerSize.value = coordinates.size
-                            containerPosition.value = coordinates.positionInRoot
+                            containerPosition.value = coordinates.positionInRoot()
                             positionedLatch.countDown()
                         },
                     width = size,
@@ -4028,7 +4028,7 @@
                 Row(Modifier.fillMaxWidth()) {
                     Container(
                         Modifier.preferredSize(sizeDp).onGloballyPositioned { coordinates ->
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -4037,7 +4037,7 @@
                     Container(
                         Modifier.preferredSize(sizeDp * 2)
                             .onGloballyPositioned { coordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4289,7 +4289,7 @@
                 ) {
                     Container(
                         Modifier.preferredSize(sizeDp).onGloballyPositioned { coordinates ->
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -4298,7 +4298,7 @@
                     Container(
                         Modifier.preferredSize(sizeDp * 2)
                             .onGloballyPositioned { coordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4369,7 +4369,7 @@
                 Column(Modifier.fillMaxWidth()) {
                     Container(
                         Modifier.preferredSize(sizeDp).onGloballyPositioned { coordinates ->
-                            childPosition[0] = coordinates.positionInRoot
+                            childPosition[0] = coordinates.positionInRoot()
                             drawLatch.countDown()
                         }
                     ) {
@@ -4378,7 +4378,7 @@
                     Container(
                         Modifier.preferredSize(sizeDp * 2)
                             .onGloballyPositioned { coordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4416,7 +4416,7 @@
                         Modifier.preferredSize(sizeDp)
                             .align(Alignment.End)
                             .onGloballyPositioned { coordinates ->
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4426,7 +4426,7 @@
                         Modifier.preferredSize(sizeDp * 2)
                             .align(Alignment.End)
                             .onGloballyPositioned { coordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4455,7 +4455,7 @@
                         Modifier.preferredSize(sizeDp)
                             .alignBy { it.width }
                             .onGloballyPositioned { coordinates ->
-                                childPosition[0] = coordinates.positionInRoot
+                                childPosition[0] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
@@ -4465,7 +4465,7 @@
                         Modifier.preferredSize(sizeDp)
                             .alignBy { it.width / 2 }
                             .onGloballyPositioned { coordinates ->
-                                childPosition[1] = coordinates.positionInRoot
+                                childPosition[1] = coordinates.positionInRoot()
                                 drawLatch.countDown()
                             }
                     ) {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
index fe446d5..0384749 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/FocusableTest.kt
@@ -21,6 +21,7 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
@@ -94,6 +95,7 @@
             .assert(isNotFocusable())
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun focusableTest_focusAcquire() {
         val (focusRequester, otherFocusRequester) = FocusRequester.createRefs()
@@ -133,6 +135,7 @@
             .assertIsNotFocused()
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun focusableTest_interactionState() {
         val interactionState = InteractionState()
@@ -231,4 +234,4 @@
                 )
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
index 97cc0ed..fc787fe 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.testutils.MockAnimationClock
 import androidx.compose.testutils.assertPixels
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -40,7 +41,8 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.captureToImage
-import androidx.compose.ui.test.click
+import androidx.compose.ui.test.center
+import androidx.compose.ui.test.down
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -646,9 +648,8 @@
             assertThat(scrollState.isAnimationRunning).isEqualTo(true)
         }
 
-        // TODO (matvei/jelle): this should be down, and not click to be 100% fair
         rule.onNodeWithTag(scrollerTag)
-            .performGesture { click() }
+            .performGesture { down(center) }
 
         rule.runOnIdle {
             assertThat(scrollState.isAnimationRunning).isEqualTo(false)
@@ -927,7 +928,7 @@
         val state = ScrollState(
             initial = 0f,
             flingConfig = FlingConfig(FloatExponentialDecaySpec()),
-            animationClock = ManualAnimationClock(0)
+            animationClock = MockAnimationClock()
         )
         rule.setContent {
             val modifier = Modifier.verticalScroll(state) as InspectableValue
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
index ffb3e30..0e77bcd 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ScrollableTest.kt
@@ -17,9 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.animation.core.FloatExponentialDecaySpec
-import androidx.compose.animation.core.ManualAnimationClock
 import androidx.compose.animation.core.ManualFrameClock
-import androidx.compose.animation.core.advanceClockMillis
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.animation.smoothScrollBy
 import androidx.compose.foundation.gestures.Scrollable
@@ -31,6 +29,9 @@
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.testutils.MockAnimationClock
+import androidx.compose.testutils.advanceClockOnMainThreadMillis
+import androidx.compose.testutils.monotonicFrameAnimationClockOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
@@ -46,7 +47,6 @@
 import androidx.compose.ui.test.center
 import androidx.compose.ui.test.down
 import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.monotonicFrameAnimationClockOf
 import androidx.compose.ui.test.moveBy
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performGesture
@@ -99,7 +99,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -155,7 +155,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -213,7 +213,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -258,7 +258,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -295,7 +295,7 @@
 
     @Test
     @OptIn(ExperimentalTestApi::class)
-    fun scrollable_velocityProxy() = runBlockingWithManualClock { clock ->
+    fun scrollable_velocityProxy() = runBlockingWithManualClock {
         var velocityTriggered = 0f
         var total = 0f
         val controller = ScrollableController(
@@ -304,7 +304,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -343,7 +343,7 @@
 
     @Test
     @OptIn(ExperimentalTestApi::class)
-    fun scrollable_startWithoutSlop_ifFlinging() = runBlockingWithManualClock { clock ->
+    fun scrollable_startWithoutSlop_ifFlinging() = runBlockingWithManualClock {
         var total = 0f
         val controller = ScrollableController(
             consumeScrollDelta = {
@@ -351,7 +351,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             Modifier.scrollable(
@@ -461,7 +461,7 @@
                 it
             },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         )
         setScrollableContent {
             if (!disposed.value) {
@@ -495,7 +495,7 @@
     fun scrollable_nestedDrag() = runBlockingWithManualClock { clock ->
         var innerDrag = 0f
         var outerDrag = 0f
-        val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+        val animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         val outerState = ScrollableController(
             consumeScrollDelta = {
                 outerDrag += it
@@ -564,7 +564,7 @@
     fun scrollable_nestedFling() = runBlockingWithManualClock { clock ->
         var innerDrag = 0f
         var outerDrag = 0f
-        val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+        val animationClock = monotonicFrameAnimationClockOf(coroutineContext)
         val outerState = ScrollableController(
             consumeScrollDelta = {
                 outerDrag += it
@@ -637,7 +637,7 @@
             var value = 0f
             var lastReceivedPreScrollAvailable = 0f
             val preConsumeFraction = 0.7f
-            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext)
             val controller = ScrollableController(
                 consumeScrollDelta = {
                     val expected = lastReceivedPreScrollAvailable * (1 - preConsumeFraction)
@@ -705,7 +705,7 @@
             var value = 0f
             var expectedLeft = 0f
             val velocityFlung = 5000f
-            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext)
             val controller = ScrollableController(
                 consumeScrollDelta = {
                     val toConsume = it * 0.345f
@@ -781,7 +781,7 @@
         runBlockingWithManualClock { clock ->
             var value = 0f
             var expectedConsumed = 0f
-            val animationClock = monotonicFrameAnimationClockOf(coroutineContext, clock)
+            val animationClock = monotonicFrameAnimationClockOf(coroutineContext)
             val controller = ScrollableController(
                 consumeScrollDelta = {
                     expectedConsumed = it * 0.3f
@@ -954,7 +954,7 @@
         val controller = ScrollableController(
             consumeScrollDelta = { it },
             flingConfig = FlingConfig(decayAnimation = FloatExponentialDecaySpec()),
-            animationClock = ManualAnimationClock(0)
+            animationClock = MockAnimationClock()
         )
         rule.setContent {
             val modifier = Modifier.scrollable(Orientation.Vertical, controller) as InspectableValue
@@ -990,9 +990,7 @@
         rule.awaitIdle()
         yield()
         while (clock.hasAwaiters) {
-            clock.advanceClockMillis(5000L)
-            // Give awaiters the chance to await again
-            yield()
+            clock.advanceClockOnMainThreadMillis(5000L)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
index 1c78a3f..506aa92 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnTest.kt
@@ -54,7 +54,7 @@
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.center
-import androidx.compose.ui.test.click
+import androidx.compose.ui.test.down
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -920,9 +920,8 @@
             assertThat(state.isAnimationRunning).isEqualTo(true)
         }
 
-        // TODO (jelle): this should be down, and not click to be 100% fair
         rule.onNodeWithTag(LazyListTag)
-            .performGesture { click() }
+            .performGesture { down(center) }
 
         rule.runOnIdle {
             assertThat(state.isAnimationRunning).isEqualTo(false)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
index 157e622..f02d251 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldSoftKeyboardTest.kt
@@ -22,6 +22,7 @@
 import android.view.WindowInsetsAnimation
 import androidx.annotation.RequiresApi
 import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.focus.FocusRequester
@@ -163,6 +164,7 @@
         assertThat(isSoftKeyboardVisible).isTrue()
     }
 
+    @OptIn(ExperimentalComposeUiApi::class)
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R)
     @Test
     fun keyboardStaysVisibleWhenMovingFromOneTextFieldToAnother() {
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 ed09be3..6cf2792 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
@@ -41,8 +41,8 @@
 import androidx.compose.ui.layout.Layout
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.MeasureBlock
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientFontLoader
 import androidx.compose.ui.selection.AmbientSelectionRegistrar
@@ -228,7 +228,7 @@
         state.layoutCoordinates = it
         selectionRegistrar?.let { selectionRegistrar ->
             if (state.selectionRange != null) {
-                val newGlobalPosition = it.globalPosition
+                val newGlobalPosition = it.positionInWindow()
                 if (newGlobalPosition != state.previousGlobalPosition) {
                     selectionRegistrar.notifyPositionChange()
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
index f908eb0..48e2f1d 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/MultiWidgetSelectionDelegate.kt
@@ -44,7 +44,7 @@
         val layoutCoordinates = getLayoutCoordinates() ?: return null
         val textLayoutResult = layoutResultCallback() ?: return null
 
-        val relativePosition = containerLayoutCoordinates.childToLocal(
+        val relativePosition = containerLayoutCoordinates.localPositionOf(
             layoutCoordinates, Offset.Zero
         )
         val startPx = startPosition - relativePosition
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
index eaa270b..884eb28 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextFieldDelegateTest.kt
@@ -395,15 +395,25 @@
         override val isAttached: Boolean
             get() = true
         override fun globalToLocal(global: Offset): Offset = localOffset
+        override fun windowToLocal(relativeToWindow: Offset): Offset = localOffset
 
         override fun localToGlobal(local: Offset): Offset = globalOffset
+        override fun localToWindow(relativeToLocal: Offset): Offset = globalOffset
 
-        override fun localToRoot(local: Offset): Offset = rootOffset
+        override fun localToRoot(relativeToLocal: Offset): Offset = rootOffset
+        override fun localPositionOf(
+            sourceCoordinates: LayoutCoordinates,
+            relativeToSource: Offset
+        ): Offset = Offset.Zero
 
         override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset =
             Offset.Zero
 
         override fun childBoundingBox(child: LayoutCoordinates): Rect = Rect.Zero
+        override fun localBoundingBoxOf(
+            sourceCoordinates: LayoutCoordinates,
+            clipBounds: Boolean
+        ): Rect = Rect.Zero
 
         override fun get(line: AlignmentLine): Int = 0
     }
diff --git a/compose/material/material-ripple/api/current.txt b/compose/material/material-ripple/api/current.txt
index fac79c8..a56a57c 100644
--- a/compose/material/material-ripple/api/current.txt
+++ b/compose/material/material-ripple/api/current.txt
@@ -13,7 +13,6 @@
 
   public final class RippleKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
   }
 
   @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
diff --git a/compose/material/material-ripple/api/public_plus_experimental_current.txt b/compose/material/material-ripple/api/public_plus_experimental_current.txt
index fac79c8..a56a57c 100644
--- a/compose/material/material-ripple/api/public_plus_experimental_current.txt
+++ b/compose/material/material-ripple/api/public_plus_experimental_current.txt
@@ -13,7 +13,6 @@
 
   public final class RippleKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
   }
 
   @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
diff --git a/compose/material/material-ripple/api/restricted_current.txt b/compose/material/material-ripple/api/restricted_current.txt
index fac79c8..a56a57c 100644
--- a/compose/material/material-ripple/api/restricted_current.txt
+++ b/compose/material/material-ripple/api/restricted_current.txt
@@ -13,7 +13,6 @@
 
   public final class RippleKt {
     method @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRipple-aOO63xs(optional boolean bounded, optional float radius, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.foundation.Indication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
   }
 
   @androidx.compose.material.ripple.ExperimentalRippleApi public interface RippleTheme {
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index 5b0c252..b730fc3 100644
--- a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -66,60 +66,6 @@
  * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
  * ripples always animate from the target layout center, bounded ripples animate from the touch
  * position.
- * @param radius the radius for the ripple. If `null` is provided then the size will be calculated
- * based on the target layout size.
- * @param color the color of the ripple. This color is usually the same color used by the text or
- * iconography in the component. This color will then have [RippleTheme.rippleAlpha] applied to
- * calculate the final color used to draw the ripple. If [Color.Unspecified] is provided the color
- * used will be [RippleTheme.defaultColor] instead.
- */
-@Deprecated(
-    "Replaced with rememberRipple",
-    ReplaceWith(
-        "rememberRipple(bounded, radius, color)",
-        "androidx.compose.material.ripple.rememberRipple"
-    )
-)
-@Composable
-@OptIn(ExperimentalRippleApi::class)
-public fun rememberRippleIndication(
-    bounded: Boolean = true,
-    radius: Dp? = null,
-    color: Color = Color.Unspecified
-): Indication {
-    val theme = AmbientRippleTheme.current
-    val clock = AmbientAnimationClock.current.asDisposableClock()
-    val resolvedColor = color.takeOrElse { theme.defaultColor() }
-    val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
-    colorState.value = resolvedColor
-    val rippleAlpha = theme.rippleAlpha()
-    return remember(bounded, radius, theme, clock) {
-        Ripple(bounded, radius ?: Dp.Unspecified, colorState, rippleAlpha, clock)
-    }
-}
-
-/**
- * Creates and [remember]s a Ripple using values provided by [RippleTheme].
- *
- * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
- * by drawing ripple animations and state layers.
- *
- * A Ripple responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other [Interaction]s by showing a fixed [StateLayer] with varying alpha values
- * depending on the [Interaction].
- *
- * If you are using MaterialTheme in your hierarchy, a Ripple will be used as the default
- * [Indication] inside components such as [androidx.compose.foundation.clickable] and
- * [androidx.compose.foundation.indication]. You can also manually provide Ripples through
- * [androidx.compose.foundation.AmbientIndication] for the same effect if you are not using
- * MaterialTheme.
- *
- * You can also explicitly create a Ripple and provide it to components in order to change the
- * parameters from the default, such as to create an unbounded ripple with a fixed size.
- *
- * @param bounded If true, ripples are clipped by the bounds of the target layout. Unbounded
- * ripples always animate from the target layout center, bounded ripples animate from the touch
- * position.
  * @param radius the radius for the ripple. If [Dp.Unspecified] is provided then the size will be
  * calculated based on the target layout size.
  * @param color the color of the ripple. This color is usually the same color used by the text or
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index db130d2..43ef1f1 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -12,20 +12,6 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @Deprecated public final class BackdropScaffoldConstants {
-    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
-    property public final float DefaultFrontLayerElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
-    property public final float DefaultHeaderHeight;
-    property public final float DefaultPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
-  }
-
   public final class BackdropScaffoldDefaults {
     method public float getFrontLayerElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
@@ -96,14 +82,6 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  @Deprecated public final class BottomSheetScaffoldConstants {
-    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
-    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
-    property public final float DefaultSheetElevation;
-    property public final float DefaultSheetPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
-  }
-
   public final class BottomSheetScaffoldDefaults {
     method public float getSheetElevation-D9Ej5fM();
     method public float getSheetPeekHeight-D9Ej5fM();
@@ -153,31 +131,6 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  @Deprecated public final class ButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
-    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
-    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
-    property public final float DefaultIconSize;
-    property public final float DefaultIconSpacing;
-    property public final float DefaultMinHeight;
-    property public final float DefaultMinWidth;
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
-    property public final float OutlinedBorderSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
-    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
-  }
-
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
@@ -223,11 +176,6 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  @Deprecated public final class CheckboxConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
-  }
-
   public final class CheckboxDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
     field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
@@ -322,15 +270,6 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  @Deprecated public final class DrawerConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
-  }
-
   public final class DrawerDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -367,12 +306,6 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  @Deprecated public final class ElevationConstants {
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
-  }
-
   public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
@@ -392,24 +325,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ElevationOverlay> getAmbientElevationOverlay();
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public interface Emphasis {
-    method @Deprecated public long applyEmphasis-8_81llA(long color);
-  }
-
-  public final class EmphasisKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void ProvideEmphasis(androidx.compose.material.Emphasis emphasis, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @Deprecated public static androidx.compose.runtime.Ambient<androidx.compose.material.EmphasisLevels> getAmbientEmphasisLevels();
-  }
-
-  @Deprecated public interface EmphasisLevels {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
-  }
-
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
@@ -423,11 +338,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  @Deprecated public final class FloatingActionButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
-  }
-
   public final class FloatingActionButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
@@ -485,14 +395,6 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @Deprecated public final class ModalBottomSheetConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
-  }
-
   public final class ModalBottomSheetDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -530,15 +432,6 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-TM4hwe4(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 isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  @Deprecated public final class ProgressIndicatorConstants {
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
-    property public final float DefaultStrokeWidth;
-    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
-  }
-
   public final class ProgressIndicatorDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
     method public float getStrokeWidth-D9Ej5fM();
@@ -559,11 +452,6 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  @Deprecated public final class RadioButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
-  }
-
   public final class RadioButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
     field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
@@ -619,12 +507,6 @@
   public final class ShapesKt {
   }
 
-  @Deprecated public final class SliderConstants {
-    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
-    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
-    field @Deprecated public static final float TickColorAlpha = 0.54f;
-  }
-
   public final class SliderDefaults {
     field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
@@ -635,14 +517,6 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  @Deprecated public final class SnackbarConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
-    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
-    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
-    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
-  }
-
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
     method public void dismiss();
     method public String? getActionLabel();
@@ -713,17 +587,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  @Deprecated public final class SwipeableConstants {
-    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
-    property public final float DefaultVelocityThreshold;
-    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
-    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
-    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
-  }
-
   public final class SwipeableDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
     method public float getVelocityThreshold-D9Ej5fM();
@@ -772,11 +635,6 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  @Deprecated public final class SwitchConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
-  }
-
   public final class SwitchDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
     field public static final androidx.compose.material.SwitchDefaults INSTANCE;
@@ -786,20 +644,6 @@
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  @Deprecated public final class TabConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
-    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
-    property public final float DefaultDividerThickness;
-    property public final float DefaultIndicatorHeight;
-    property public final float DefaultScrollableTabRowPadding;
-    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
-    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
-  }
-
   public final class TabDefaults {
     method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index db130d2..43ef1f1 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -12,20 +12,6 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @Deprecated public final class BackdropScaffoldConstants {
-    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
-    property public final float DefaultFrontLayerElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
-    property public final float DefaultHeaderHeight;
-    property public final float DefaultPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
-  }
-
   public final class BackdropScaffoldDefaults {
     method public float getFrontLayerElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
@@ -96,14 +82,6 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  @Deprecated public final class BottomSheetScaffoldConstants {
-    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
-    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
-    property public final float DefaultSheetElevation;
-    property public final float DefaultSheetPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
-  }
-
   public final class BottomSheetScaffoldDefaults {
     method public float getSheetElevation-D9Ej5fM();
     method public float getSheetPeekHeight-D9Ej5fM();
@@ -153,31 +131,6 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  @Deprecated public final class ButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
-    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
-    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
-    property public final float DefaultIconSize;
-    property public final float DefaultIconSpacing;
-    property public final float DefaultMinHeight;
-    property public final float DefaultMinWidth;
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
-    property public final float OutlinedBorderSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
-    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
-  }
-
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
@@ -223,11 +176,6 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  @Deprecated public final class CheckboxConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
-  }
-
   public final class CheckboxDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
     field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
@@ -322,15 +270,6 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  @Deprecated public final class DrawerConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
-  }
-
   public final class DrawerDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -367,12 +306,6 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  @Deprecated public final class ElevationConstants {
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
-  }
-
   public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
@@ -392,24 +325,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ElevationOverlay> getAmbientElevationOverlay();
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public interface Emphasis {
-    method @Deprecated public long applyEmphasis-8_81llA(long color);
-  }
-
-  public final class EmphasisKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void ProvideEmphasis(androidx.compose.material.Emphasis emphasis, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @Deprecated public static androidx.compose.runtime.Ambient<androidx.compose.material.EmphasisLevels> getAmbientEmphasisLevels();
-  }
-
-  @Deprecated public interface EmphasisLevels {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
-  }
-
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
@@ -423,11 +338,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  @Deprecated public final class FloatingActionButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
-  }
-
   public final class FloatingActionButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
@@ -485,14 +395,6 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @Deprecated public final class ModalBottomSheetConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
-  }
-
   public final class ModalBottomSheetDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -530,15 +432,6 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-TM4hwe4(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 isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  @Deprecated public final class ProgressIndicatorConstants {
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
-    property public final float DefaultStrokeWidth;
-    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
-  }
-
   public final class ProgressIndicatorDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
     method public float getStrokeWidth-D9Ej5fM();
@@ -559,11 +452,6 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  @Deprecated public final class RadioButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
-  }
-
   public final class RadioButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
     field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
@@ -619,12 +507,6 @@
   public final class ShapesKt {
   }
 
-  @Deprecated public final class SliderConstants {
-    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
-    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
-    field @Deprecated public static final float TickColorAlpha = 0.54f;
-  }
-
   public final class SliderDefaults {
     field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
@@ -635,14 +517,6 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  @Deprecated public final class SnackbarConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
-    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
-    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
-    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
-  }
-
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
     method public void dismiss();
     method public String? getActionLabel();
@@ -713,17 +587,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  @Deprecated public final class SwipeableConstants {
-    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
-    property public final float DefaultVelocityThreshold;
-    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
-    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
-    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
-  }
-
   public final class SwipeableDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
     method public float getVelocityThreshold-D9Ej5fM();
@@ -772,11 +635,6 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  @Deprecated public final class SwitchConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
-  }
-
   public final class SwitchDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
     field public static final androidx.compose.material.SwitchDefaults INSTANCE;
@@ -786,20 +644,6 @@
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  @Deprecated public final class TabConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
-    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
-    property public final float DefaultDividerThickness;
-    property public final float DefaultIndicatorHeight;
-    property public final float DefaultScrollableTabRowPadding;
-    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
-    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
-  }
-
   public final class TabDefaults {
     method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index db130d2..43ef1f1 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -12,20 +12,6 @@
     method @androidx.compose.runtime.Composable public static void TopAppBar-ye6PvEY(optional androidx.compose.ui.Modifier modifier, optional long backgroundColor, optional long contentColor, optional float elevation, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.RowScope,kotlin.Unit> content);
   }
 
-  @Deprecated public final class BackdropScaffoldConstants {
-    method @Deprecated public float getDefaultFrontLayerElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultFrontLayerScrimColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.ui.graphics.Shape getDefaultFrontLayerShape();
-    method @Deprecated public float getDefaultHeaderHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultPeekHeight-D9Ej5fM();
-    property public final float DefaultFrontLayerElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultFrontLayerScrimColor;
-    property @androidx.compose.runtime.Composable public final androidx.compose.ui.graphics.Shape DefaultFrontLayerShape;
-    property public final float DefaultHeaderHeight;
-    property public final float DefaultPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BackdropScaffoldConstants INSTANCE;
-  }
-
   public final class BackdropScaffoldDefaults {
     method public float getFrontLayerElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getFrontLayerScrimColor-0d7_KjU();
@@ -96,14 +82,6 @@
     method @androidx.compose.runtime.Composable public static void BottomNavigationItem-S1l6qvI(kotlin.jvm.functions.Function0<kotlin.Unit> icon, boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional kotlin.jvm.functions.Function0<kotlin.Unit> label, optional boolean alwaysShowLabels, optional androidx.compose.foundation.InteractionState interactionState, optional long selectedContentColor, optional long unselectedContentColor);
   }
 
-  @Deprecated public final class BottomSheetScaffoldConstants {
-    method @Deprecated public float getDefaultSheetElevation-D9Ej5fM();
-    method @Deprecated public float getDefaultSheetPeekHeight-D9Ej5fM();
-    property public final float DefaultSheetElevation;
-    property public final float DefaultSheetPeekHeight;
-    field @Deprecated public static final androidx.compose.material.BottomSheetScaffoldConstants INSTANCE;
-  }
-
   public final class BottomSheetScaffoldDefaults {
     method public float getSheetElevation-D9Ej5fM();
     method public float getSheetPeekHeight-D9Ej5fM();
@@ -153,31 +131,6 @@
     method public long contentColor-0d7_KjU(boolean enabled);
   }
 
-  @Deprecated public final class ButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultButtonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation defaultElevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultOutlinedButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors defaultTextButtonColors-xS_xkl8(optional long backgroundColor, optional long contentColor, optional long disabledContentColor);
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultContentPadding();
-    method @Deprecated public float getDefaultIconSize-D9Ej5fM();
-    method @Deprecated public float getDefaultIconSpacing-D9Ej5fM();
-    method @Deprecated public float getDefaultMinHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultMinWidth-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.foundation.BorderStroke getDefaultOutlinedBorder();
-    method @Deprecated public androidx.compose.foundation.layout.PaddingValues getDefaultTextContentPadding();
-    method @Deprecated public float getOutlinedBorderSize-D9Ej5fM();
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultContentPadding;
-    property public final float DefaultIconSize;
-    property public final float DefaultIconSpacing;
-    property public final float DefaultMinHeight;
-    property public final float DefaultMinWidth;
-    property public final androidx.compose.foundation.layout.PaddingValues DefaultTextContentPadding;
-    property public final float OutlinedBorderSize;
-    property @androidx.compose.runtime.Composable public final androidx.compose.foundation.BorderStroke defaultOutlinedBorder;
-    field @Deprecated public static final androidx.compose.material.ButtonConstants INSTANCE;
-    field @Deprecated public static final float OutlinedBorderOpacity = 0.12f;
-  }
-
   public final class ButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonColors buttonColors-nlx5xbs(optional long backgroundColor, optional long disabledBackgroundColor, optional long contentColor, optional long disabledContentColor);
     method @androidx.compose.runtime.Composable public androidx.compose.material.ButtonElevation elevation-qYQSm_w(optional float defaultElevation, optional float pressedElevation, optional float disabledElevation);
@@ -223,11 +176,6 @@
     method public long checkmarkColor-0d7_KjU(androidx.compose.ui.state.ToggleableState state);
   }
 
-  @Deprecated public final class CheckboxConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors defaultColors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
-    field @Deprecated public static final androidx.compose.material.CheckboxConstants INSTANCE;
-  }
-
   public final class CheckboxDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.CheckboxColors colors-QGkLkJU(optional long checkedColor, optional long uncheckedColor, optional long checkmarkColor, optional long disabledColor, optional long disabledIndeterminateColor);
     field public static final androidx.compose.material.CheckboxDefaults INSTANCE;
@@ -322,15 +270,6 @@
     method @androidx.compose.runtime.Composable public static void Divider-JRSVyrs(optional androidx.compose.ui.Modifier modifier, optional long color, optional float thickness, optional float startIndent);
   }
 
-  @Deprecated public final class DrawerConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long defaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.DrawerConstants INSTANCE;
-    field @Deprecated public static final float ScrimDefaultOpacity = 0.32f;
-  }
-
   public final class DrawerDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -367,12 +306,6 @@
     enum_constant public static final androidx.compose.material.DrawerValue Open;
   }
 
-  @Deprecated public final class ElevationConstants {
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    method @Deprecated public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
-    field @Deprecated public static final androidx.compose.material.ElevationConstants INSTANCE;
-  }
-
   public final class ElevationDefaults {
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? incomingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
     method public androidx.compose.animation.core.AnimationSpec<androidx.compose.ui.unit.Dp>? outgoingAnimationSpecForInteraction(androidx.compose.foundation.Interaction interaction);
@@ -392,24 +325,6 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ElevationOverlay> getAmbientElevationOverlay();
   }
 
-  @Deprecated @androidx.compose.runtime.Immutable public interface Emphasis {
-    method @Deprecated public long applyEmphasis-8_81llA(long color);
-  }
-
-  public final class EmphasisKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static void ProvideEmphasis(androidx.compose.material.Emphasis emphasis, kotlin.jvm.functions.Function0<kotlin.Unit> content);
-    method @Deprecated public static androidx.compose.runtime.Ambient<androidx.compose.material.EmphasisLevels> getAmbientEmphasisLevels();
-  }
-
-  @Deprecated public interface EmphasisLevels {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getDisabled();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getHigh();
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.Emphasis getMedium();
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis disabled;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis high;
-    property @androidx.compose.runtime.Composable public abstract androidx.compose.material.Emphasis medium;
-  }
-
   @kotlin.RequiresOptIn(message="This material API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalMaterialApi {
   }
 
@@ -423,11 +338,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Immutable public androidx.compose.material.FixedThreshold copy-0680j_4(float offset);
   }
 
-  @Deprecated public final class FloatingActionButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation defaultElevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
-    field @Deprecated public static final androidx.compose.material.FloatingActionButtonConstants INSTANCE;
-  }
-
   public final class FloatingActionButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.FloatingActionButtonElevation elevation-ioHfwGI(optional float defaultElevation, optional float pressedElevation);
     field public static final androidx.compose.material.FloatingActionButtonDefaults INSTANCE;
@@ -485,14 +395,6 @@
     method @androidx.compose.runtime.Composable public static void DropdownMenuItem(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, kotlin.jvm.functions.Function0<kotlin.Unit> content);
   }
 
-  @Deprecated public final class ModalBottomSheetConstants {
-    method @Deprecated public float getDefaultElevation-D9Ej5fM();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultScrimColor-0d7_KjU();
-    property public final float DefaultElevation;
-    property @androidx.compose.runtime.Composable public final long DefaultScrimColor;
-    field @Deprecated public static final androidx.compose.material.ModalBottomSheetConstants INSTANCE;
-  }
-
   public final class ModalBottomSheetDefaults {
     method public float getElevation-D9Ej5fM();
     method @androidx.compose.runtime.Composable public long getScrimColor-0d7_KjU();
@@ -530,15 +432,6 @@
     method @androidx.compose.runtime.Composable public static void OutlinedTextField-TM4hwe4(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 isErrorValue, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional boolean singleLine, optional int maxLines, optional kotlin.jvm.functions.Function2<? super androidx.compose.ui.text.input.ImeAction,? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onImeActionPerformed, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional androidx.compose.foundation.InteractionState interactionState, optional long activeColor, optional long inactiveColor, optional long errorColor);
   }
 
-  @Deprecated public final class ProgressIndicatorConstants {
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultProgressAnimationSpec();
-    method @Deprecated public float getDefaultStrokeWidth-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultProgressAnimationSpec;
-    property public final float DefaultStrokeWidth;
-    field @Deprecated public static final float DefaultIndicatorBackgroundOpacity = 0.24f;
-    field @Deprecated public static final androidx.compose.material.ProgressIndicatorConstants INSTANCE;
-  }
-
   public final class ProgressIndicatorDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getProgressAnimationSpec();
     method public float getStrokeWidth-D9Ej5fM();
@@ -559,11 +452,6 @@
     method public long radioColor-0d7_KjU(boolean enabled, boolean selected);
   }
 
-  @Deprecated public final class RadioButtonConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors defaultColors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
-    field @Deprecated public static final androidx.compose.material.RadioButtonConstants INSTANCE;
-  }
-
   public final class RadioButtonDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.RadioButtonColors colors-xS_xkl8(optional long selectedColor, optional long unselectedColor, optional long disabledColor);
     field public static final androidx.compose.material.RadioButtonDefaults INSTANCE;
@@ -619,12 +507,6 @@
   public final class ShapesKt {
   }
 
-  @Deprecated public final class SliderConstants {
-    field @Deprecated public static final androidx.compose.material.SliderConstants INSTANCE;
-    field @Deprecated public static final float InactiveTrackColorAlpha = 0.24f;
-    field @Deprecated public static final float TickColorAlpha = 0.54f;
-  }
-
   public final class SliderDefaults {
     field public static final androidx.compose.material.SliderDefaults INSTANCE;
     field public static final float InactiveTrackColorAlpha = 0.24f;
@@ -635,14 +517,6 @@
     method @androidx.compose.runtime.Composable public static void Slider-B7rb6FQ(float value, kotlin.jvm.functions.Function1<? super java.lang.Float,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional kotlin.ranges.ClosedFloatingPointRange<java.lang.Float> valueRange, optional @IntRange(from=0) int steps, optional kotlin.jvm.functions.Function0<kotlin.Unit> onValueChangeEnd, optional androidx.compose.foundation.InteractionState interactionState, optional long thumbColor, optional long activeTrackColor, optional long inactiveTrackColor, optional long activeTickColor, optional long inactiveTickColor);
   }
 
-  @Deprecated public final class SnackbarConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultActionPrimaryColor-0d7_KjU();
-    method @Deprecated @androidx.compose.runtime.Composable public long getDefaultBackgroundColor-0d7_KjU();
-    property @androidx.compose.runtime.Composable public final long defaultActionPrimaryColor;
-    property @androidx.compose.runtime.Composable public final long defaultBackgroundColor;
-    field @Deprecated public static final androidx.compose.material.SnackbarConstants INSTANCE;
-  }
-
   @androidx.compose.material.ExperimentalMaterialApi public interface SnackbarData {
     method public void dismiss();
     method public String? getActionLabel();
@@ -713,17 +587,6 @@
     method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.DismissState rememberDismissState(optional androidx.compose.material.DismissValue initialValue, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.DismissValue,java.lang.Boolean> confirmStateChange);
   }
 
-  @Deprecated public final class SwipeableConstants {
-    method @Deprecated public androidx.compose.material.ResistanceConfig? defaultResistanceConfig(java.util.Set<java.lang.Float> anchors, optional float factorAtMin, optional float factorAtMax);
-    method @Deprecated public androidx.compose.animation.core.SpringSpec<java.lang.Float> getDefaultAnimationSpec();
-    method @Deprecated public float getDefaultVelocityThreshold-D9Ej5fM();
-    property public final androidx.compose.animation.core.SpringSpec<java.lang.Float> DefaultAnimationSpec;
-    property public final float DefaultVelocityThreshold;
-    field @Deprecated public static final androidx.compose.material.SwipeableConstants INSTANCE;
-    field @Deprecated public static final float StandardResistanceFactor = 10.0f;
-    field @Deprecated public static final float StiffResistanceFactor = 20.0f;
-  }
-
   public final class SwipeableDefaults {
     method public androidx.compose.animation.core.SpringSpec<java.lang.Float> getAnimationSpec();
     method public float getVelocityThreshold-D9Ej5fM();
@@ -772,11 +635,6 @@
     method public long trackColor-0d7_KjU(boolean enabled, boolean checked);
   }
 
-  @Deprecated public final class SwitchConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors defaultColors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
-    field @Deprecated public static final androidx.compose.material.SwitchConstants INSTANCE;
-  }
-
   public final class SwitchDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.material.SwitchColors colors-R8aI8sA(optional long checkedThumbColor, optional long checkedTrackColor, optional float checkedTrackAlpha, optional long uncheckedThumbColor, optional long uncheckedTrackColor, optional float uncheckedTrackAlpha, optional long disabledCheckedThumbColor, optional long disabledCheckedTrackColor, optional long disabledUncheckedThumbColor, optional long disabledUncheckedTrackColor);
     field public static final androidx.compose.material.SwitchDefaults INSTANCE;
@@ -786,20 +644,6 @@
     method @androidx.compose.runtime.Composable public static void Switch(boolean checked, kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onCheckedChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional androidx.compose.foundation.InteractionState interactionState, optional androidx.compose.material.SwitchColors colors);
   }
 
-  @Deprecated public final class TabConstants {
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultDivider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
-    method @Deprecated @androidx.compose.runtime.Composable public void DefaultIndicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
-    method @Deprecated public androidx.compose.ui.Modifier defaultTabIndicatorOffset(androidx.compose.ui.Modifier, androidx.compose.material.TabPosition currentTabPosition);
-    method @Deprecated public float getDefaultDividerThickness-D9Ej5fM();
-    method @Deprecated public float getDefaultIndicatorHeight-D9Ej5fM();
-    method @Deprecated public float getDefaultScrollableTabRowPadding-D9Ej5fM();
-    property public final float DefaultDividerThickness;
-    property public final float DefaultIndicatorHeight;
-    property public final float DefaultScrollableTabRowPadding;
-    field @Deprecated public static final float DefaultDividerOpacity = 0.12f;
-    field @Deprecated public static final androidx.compose.material.TabConstants INSTANCE;
-  }
-
   public final class TabDefaults {
     method @androidx.compose.runtime.Composable public void Divider-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float thickness, optional long color);
     method @androidx.compose.runtime.Composable public void Indicator-Z-uBYeE(optional androidx.compose.ui.Modifier modifier, optional float height, optional long color);
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
index c219aec..107384f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomNavigationTest.kt
@@ -21,8 +21,8 @@
 import androidx.compose.material.samples.BottomNavigationSample
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertCountEquals
 import androidx.compose.ui.test.assertHeightIsEqualTo
@@ -105,7 +105,7 @@
             itemCoords.forEach { (index, coord) ->
                 Truth.assertThat(coord.size.width).isEqualTo(expectedItemWidth)
                 Truth.assertThat(coord.size.height).isEqualTo(expectedItemHeight)
-                Truth.assertThat(coord.globalPosition.x)
+                Truth.assertThat(coord.positionInWindow().x)
                     .isEqualTo((expectedItemWidth * index).toFloat())
             }
         }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
index 34f926a..7349c2a 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
@@ -280,7 +280,7 @@
                     Box(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.localToGlobal(Offset.Zero)
+                                appbarPosition = positioned.localToWindow(Offset.Zero)
                                 appbarSize = positioned.size
                             }
                             .fillMaxWidth()
@@ -294,7 +294,7 @@
             ) {
                 Box(
                     Modifier
-                        .onGloballyPositioned { contentPosition = it.localToGlobal(Offset.Zero) }
+                        .onGloballyPositioned { contentPosition = it.localToWindow(Offset.Zero) }
                         .fillMaxWidth()
                         .preferredHeight(50.dp)
                         .background(Color.Blue)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
index a8745fb..9deab151 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ButtonTest.kt
@@ -540,8 +540,8 @@
         }
 
         rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot
-            val contentBounds = contentCoordinates!!.boundsInRoot
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val contentBounds = contentCoordinates!!.boundsInRoot()
             assertThat(contentBounds.width).isLessThan(buttonBounds.width)
             assertThat(contentBounds.height).isLessThan(buttonBounds.height)
             with(rule.density) {
@@ -577,14 +577,14 @@
             Column {
                 Spacer(
                     Modifier.size(10.dp).weight(1f).onGloballyPositioned {
-                        item1Bounds = it.boundsInRoot
+                        item1Bounds = it.boundsInRoot()
                     }
                 )
 
                 Button(
                     onClick = {},
                     modifier = Modifier.weight(1f).onGloballyPositioned {
-                        buttonBounds = it.boundsInRoot
+                        buttonBounds = it.boundsInRoot()
                     }
                 ) {
                     Text("Button")
@@ -607,17 +607,17 @@
             Button(
                 onClick = {},
                 modifier = Modifier.onGloballyPositioned {
-                    buttonBounds = it.boundsInRoot
+                    buttonBounds = it.boundsInRoot()
                 }
             ) {
                 Spacer(
                     Modifier.size(10.dp).onGloballyPositioned {
-                        item1Bounds = it.boundsInRoot
+                        item1Bounds = it.boundsInRoot()
                     }
                 )
                 Spacer(
                     Modifier.width(10.dp).height(5.dp).onGloballyPositioned {
-                        item2Bounds = it.boundsInRoot
+                        item2Bounds = it.boundsInRoot()
                     }
                 )
             }
@@ -647,8 +647,8 @@
         }
 
         rule.runOnIdle {
-            val topLeft = childCoordinates!!.localToGlobal(Offset.Zero).x -
-                parentCoordinates!!.localToGlobal(Offset.Zero).x
+            val topLeft = childCoordinates!!.localToWindow(Offset.Zero).x -
+                parentCoordinates!!.localToWindow(Offset.Zero).x
             val currentPadding = with(rule.density) {
                 padding.toIntPx().toFloat()
             }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
index 9f831e1..10d8101 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/FloatingActionButtonTest.kt
@@ -144,7 +144,7 @@
             Column {
                 Spacer(
                     Modifier.size(10.dp).weight(1f).onGloballyPositioned {
-                        item1Bounds = it.boundsInRoot
+                        item1Bounds = it.boundsInRoot()
                     }
                 )
 
@@ -152,7 +152,7 @@
                     onClick = {},
                     modifier = Modifier.weight(1f)
                         .onGloballyPositioned {
-                            buttonBounds = it.boundsInRoot
+                            buttonBounds = it.boundsInRoot()
                         }
                 ) {
                     Text("Button")
@@ -260,8 +260,8 @@
         }
 
         rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot
-            val contentBounds = contentCoordinates!!.boundsInRoot
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val contentBounds = contentCoordinates!!.boundsInRoot()
             assertThat(contentBounds.width).isLessThan(buttonBounds.width)
             assertThat(contentBounds.height).isLessThan(buttonBounds.height)
             with(rule.density) {
@@ -292,8 +292,8 @@
         }
 
         rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot
-            val contentBounds = contentCoordinates!!.boundsInRoot
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val contentBounds = contentCoordinates!!.boundsInRoot()
             assertThat(contentBounds.width).isLessThan(buttonBounds.width)
             assertThat(contentBounds.height).isLessThan(buttonBounds.height)
             with(rule.density) {
@@ -331,9 +331,9 @@
         }
 
         rule.runOnIdle {
-            val buttonBounds = buttonCoordinates!!.boundsInRoot
-            val textBounds = textCoordinates!!.boundsInRoot
-            val iconBounds = iconCoordinates!!.boundsInRoot
+            val buttonBounds = buttonCoordinates!!.boundsInRoot()
+            val textBounds = textCoordinates!!.boundsInRoot()
+            val iconBounds = iconCoordinates!!.boundsInRoot()
             with(rule.density) {
                 assertThat(textBounds.width).isEqualTo(2.dp.toIntPx().toFloat())
                 assertThat(textBounds.height).isEqualTo(2.dp.toIntPx().toFloat())
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
index 3a66e02..aa6181f0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ScaffoldTest.kt
@@ -114,7 +114,7 @@
                     Box(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                appbarPosition = positioned.localToGlobal(Offset.Zero)
+                                appbarPosition = positioned.localToWindow(Offset.Zero)
                                 appbarSize = positioned.size
                             }
                             .fillMaxWidth()
@@ -125,7 +125,7 @@
             ) {
                 Box(
                     Modifier
-                        .onGloballyPositioned { contentPosition = it.localToGlobal(Offset.Zero) }
+                        .onGloballyPositioned { contentPosition = it.localToWindow(Offset.Zero) }
                         .fillMaxWidth()
                         .preferredHeight(50.dp)
                         .background(Color.Blue)
@@ -281,7 +281,7 @@
                     FloatingActionButton(
                         modifier = Modifier.onGloballyPositioned { positioned ->
                             fabSize = positioned.size
-                            fabPosition = positioned.positionInRoot
+                            fabPosition = positioned.positionInRoot()
                         },
                         onClick = {}
                     ) {
@@ -294,7 +294,7 @@
                     BottomAppBar(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition = positioned.positionInRoot
+                                bottomBarPosition = positioned.positionInRoot()
                             }
                     ) {}
                 }
@@ -317,7 +317,7 @@
                     FloatingActionButton(
                         modifier = Modifier.onGloballyPositioned { positioned ->
                             fabSize = positioned.size
-                            fabPosition = positioned.positionInRoot
+                            fabPosition = positioned.positionInRoot()
                         },
                         onClick = {}
                     ) {
@@ -330,7 +330,7 @@
                     BottomAppBar(
                         Modifier
                             .onGloballyPositioned { positioned: LayoutCoordinates ->
-                                bottomBarPosition = positioned.positionInRoot
+                                bottomBarPosition = positioned.positionInRoot()
                             }
                     ) {}
                 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
index 21e4fa0..dd5677e 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SwipeableTest.kt
@@ -30,6 +30,7 @@
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.testutils.MockAnimationClock
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.nestedscroll.nestedScroll
@@ -1511,7 +1512,7 @@
 
     @Test
     fun testInspectorValue() {
-        val state = SwipeableState("A", clock)
+        val state = SwipeableState("A", MockAnimationClock())
         val anchors = mapOf(0f to "A", 100f to "B")
         rule.setContent {
             val modifier = Modifier.swipeable(
@@ -1576,7 +1577,6 @@
                 moveBy(Offset(x = 0f, y = -1500f))
                 up()
             }
-        advanceClock()
 
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("B")
@@ -1590,8 +1590,6 @@
                 up()
             }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("A")
             assertThat(scrollState.value).isEqualTo(0f)
@@ -1643,8 +1641,6 @@
                 )
             }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("B")
             // should eat all velocity, no internal scroll
@@ -1661,8 +1657,6 @@
                 )
             }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("A")
             assertThat(scrollState.value).isEqualTo(0f)
@@ -1717,8 +1711,6 @@
                 )
             }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("B")
             assertThat(scrollState.value).isEqualTo(0f)
@@ -1726,8 +1718,6 @@
             scrollState.scrollBy(500f)
         }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("B")
             assertThat(scrollState.value).isEqualTo(500f)
@@ -1744,8 +1734,6 @@
                 )
             }
 
-        advanceClock()
-
         rule.runOnIdle {
             assertThat(swipeableState.value).isEqualTo("A")
             assertThat(scrollState.value).isEqualTo(0f)
@@ -1789,4 +1777,4 @@
             Box(modifier = Modifier.fillMaxSize().testTag(swipeableTag).then(swipeableFactory()))
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
index 279c124..1a96cf0 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldTest.kt
@@ -176,7 +176,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                                 labelSize.value = it.size
                             }
                         )
@@ -214,7 +214,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                                 labelSize.value = it.size
                             }
                         )
@@ -252,7 +252,7 @@
                     Text(
                         text = "label",
                         modifier = Modifier.onGloballyPositioned {
-                            labelPosition.value = it.positionInRoot
+                            labelPosition.value = it.positionInRoot()
                             labelSize.value = it.size
                         }
                     )
@@ -288,7 +288,7 @@
                     Text(
                         text = "label",
                         modifier = Modifier.onGloballyPositioned {
-                            labelPosition.value = it.positionInRoot
+                            labelPosition.value = it.positionInRoot()
                             labelSize.value = it.size
                         }
                     )
@@ -324,7 +324,7 @@
                         Text(
                             text = "placeholder",
                             modifier = Modifier.onGloballyPositioned {
-                                placeholderPosition.value = it.positionInRoot
+                                placeholderPosition.value = it.positionInRoot()
                                 placeholderSize.value = it.size
                             }
                         )
@@ -367,7 +367,7 @@
                         Text(
                             text = "placeholder",
                             modifier = Modifier.onGloballyPositioned {
-                                placeholderPosition.value = it.positionInRoot
+                                placeholderPosition.value = it.positionInRoot()
                                 placeholderSize.value = it.size
                             }
                         )
@@ -409,7 +409,7 @@
                         Text(
                             text = "placeholder",
                             modifier = Modifier.onGloballyPositioned {
-                                placeholderPosition.value = it.positionInRoot
+                                placeholderPosition.value = it.positionInRoot()
                                 placeholderSize.value = it.size
                             }
                         )
@@ -475,7 +475,7 @@
                 leadingIcon = {
                     Box(
                         Modifier.preferredSize(size).onGloballyPositioned {
-                            leadingPosition.value = it.positionInRoot
+                            leadingPosition.value = it.positionInRoot()
                             leadingSize.value = it.size
                         }
                     )
@@ -483,7 +483,7 @@
                 trailingIcon = {
                     Box(
                         Modifier.preferredSize(size).onGloballyPositioned {
-                            trailingPosition.value = it.positionInRoot
+                            trailingPosition.value = it.positionInRoot()
                             trailingSize.value = it.size
                         }
                     )
@@ -524,7 +524,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                             }
                         )
                     },
@@ -554,7 +554,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                             }
                         )
                     },
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
index 81ac659..d7af753 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/TextFieldTest.kt
@@ -41,6 +41,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.testutils.assertShape
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusModifier
@@ -192,6 +193,7 @@
         }
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun testTextField_showHideKeyboardBasedOnFocus() {
         val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
@@ -221,6 +223,7 @@
         rule.runOnIdle { assertThat(hostView.isSoftwareKeyboardShown).isFalse() }
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun testTextField_clickingOnTextAfterDismissingKeyboard_showsKeyboard() {
         val (focusRequester, parentFocusRequester) = FocusRequester.createRefs()
@@ -271,7 +274,7 @@
                             fontSize = 10.sp,
                             modifier = Modifier
                                 .onGloballyPositioned {
-                                    labelPosition.value = it.positionInRoot
+                                    labelPosition.value = it.positionInRoot()
                                     labelSize.value = it.size
                                 }
                         )
@@ -312,7 +315,7 @@
                             fontSize = 10.sp,
                             modifier = Modifier
                                 .onGloballyPositioned {
-                                    labelPosition.value = it.positionInRoot
+                                    labelPosition.value = it.positionInRoot()
                                     labelSize.value = it.size
                                 }
                         )
@@ -352,7 +355,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                                 labelSize.value = it.size
                             }
                         )
@@ -391,7 +394,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                                 labelSize.value = it.size
                                 baseline.value = it[FirstBaseline].toFloat() +
                                     labelPosition.value!!.y
@@ -434,7 +437,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                                 labelSize.value = it.size
                                 baseline.value =
                                     it[FirstBaseline].toFloat() + labelPosition.value!!.y
@@ -477,7 +480,7 @@
                         Text(
                             text = "placeholder",
                             modifier = Modifier.onGloballyPositioned {
-                                placeholderPosition.value = it.positionInRoot
+                                placeholderPosition.value = it.positionInRoot()
                                 placeholderSize.value = it.size
                             }
                         )
@@ -522,7 +525,7 @@
                             text = "placeholder",
                             modifier = Modifier.height(20.dp)
                                 .onGloballyPositioned {
-                                    placeholderPosition.value = it.positionInRoot
+                                    placeholderPosition.value = it.positionInRoot()
                                     placeholderSize.value = it.size
                                 }
                         )
@@ -563,7 +566,7 @@
                         Text(
                             text = "placeholder",
                             modifier = Modifier.onGloballyPositioned {
-                                placeholderPosition.value = it.positionInRoot
+                                placeholderPosition.value = it.positionInRoot()
                                 placeholderSize.value = it.size
                             }
                         )
@@ -630,7 +633,7 @@
                 leadingIcon = {
                     Box(
                         Modifier.preferredSize(size).onGloballyPositioned {
-                            leadingPosition.value = it.positionInRoot
+                            leadingPosition.value = it.positionInRoot()
                             leadingSize.value = it.size
                         }
                     )
@@ -638,7 +641,7 @@
                 trailingIcon = {
                     Box(
                         Modifier.preferredSize(size).onGloballyPositioned {
-                            trailingPosition.value = it.positionInRoot
+                            trailingPosition.value = it.positionInRoot()
                             trailingSize.value = it.size
                         }
                     )
@@ -683,7 +686,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                             }
                         )
                     },
@@ -715,7 +718,7 @@
                         Text(
                             text = "label",
                             modifier = Modifier.onGloballyPositioned {
-                                labelPosition.value = it.positionInRoot
+                                labelPosition.value = it.positionInRoot()
                             }
                         )
                     },
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
index 4aa9d9e..d244464 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BackdropScaffold.kt
@@ -471,48 +471,6 @@
 private enum class BackdropLayers { Back, Front }
 
 /**
- * Contains useful constants for [BackdropScaffold].
- */
-@Deprecated(
-    "BackdropScaffoldConstants has been replaced with BackdropScaffoldDefaults",
-    ReplaceWith(
-        "BackdropScaffoldDefaults",
-        "androidx.compose.material.BackdropScaffoldDefaults"
-    )
-)
-object BackdropScaffoldConstants {
-
-    /**
-     * The default peek height of the back layer.
-     */
-    val DefaultPeekHeight = 56.dp
-
-    /**
-     * The default header height of the front layer.
-     */
-    val DefaultHeaderHeight = 48.dp
-
-    /**
-     * The default shape of the front layer.
-     */
-    val DefaultFrontLayerShape: Shape
-        @Composable
-        get() = MaterialTheme.shapes.large
-            .copy(topLeft = CornerSize(16.dp), topRight = CornerSize(16.dp))
-
-    /**
-     * The default elevation of the front layer.
-     */
-    val DefaultFrontLayerElevation = 1.dp
-
-    /**
-     * The default color of the scrim applied to the front layer.
-     */
-    val DefaultFrontLayerScrimColor: Color
-        @Composable get() = MaterialTheme.colors.surface.copy(alpha = 0.60f)
-}
-
-/**
  * Contains useful defaults for [BackdropScaffold].
  */
 object BackdropScaffoldDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index c210065..cf9b77c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -425,29 +425,6 @@
 private val FabEndSpacing = 16.dp
 
 /**
- * Contains useful constants for [BottomSheetScaffold].
- */
-@Deprecated(
-    message = "BottomSheetScaffoldConstants has been replaced with BottomSheetScaffoldDefaults",
-    ReplaceWith(
-        "BottomSheetScaffoldDefaults",
-        "androidx.compose.material.BottomSheetScaffoldDefaults"
-    )
-)
-object BottomSheetScaffoldConstants {
-
-    /**
-     * The default elevation used by [BottomSheetScaffold].
-     */
-    val DefaultSheetElevation = 8.dp
-
-    /**
-     * The default peek height used by [BottomSheetScaffold].
-     */
-    val DefaultSheetPeekHeight = 56.dp
-}
-
-/**
  * Contains useful defaults for [BottomSheetScaffold].
  */
 object BottomSheetScaffoldDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
index d82ede6..9354d16 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Button.kt
@@ -313,217 +313,6 @@
 /**
  * Contains the default values used by [Button]
  */
-@Deprecated(
-    "ButtonConstants has been replaced with ButtonDefaults",
-    ReplaceWith(
-        "ButtonDefaults",
-        "androidx.compose.material.ButtonDefaults"
-    )
-)
-object ButtonConstants {
-    private val ButtonHorizontalPadding = 16.dp
-    private val ButtonVerticalPadding = 8.dp
-
-    /**
-     * The default content padding used by [Button]
-     */
-    val DefaultContentPadding = PaddingValues(
-        start = ButtonHorizontalPadding,
-        top = ButtonVerticalPadding,
-        end = ButtonHorizontalPadding,
-        bottom = ButtonVerticalPadding
-    )
-
-    /**
-     * The default min width applied for the [Button].
-     * Note that you can override it by applying Modifier.widthIn directly on [Button].
-     */
-    val DefaultMinWidth = 64.dp
-
-    /**
-     * The default min width applied for the [Button].
-     * Note that you can override it by applying Modifier.heightIn directly on [Button].
-     */
-    val DefaultMinHeight = 36.dp
-
-    /**
-     * The default size of the icon when used inside a [Button].
-     *
-     * @sample androidx.compose.material.samples.ButtonWithIconSample
-     */
-    val DefaultIconSize = 18.dp
-
-    /**
-     * The default size of the spacing between an icon and a text when they used inside a [Button].
-     *
-     * @sample androidx.compose.material.samples.ButtonWithIconSample
-     */
-    val DefaultIconSpacing = 8.dp
-
-    // TODO: b/152525426 add support for focused and hovered states
-    /**
-     * Creates a [ButtonElevation] that will animate between the provided values according to the
-     * Material specification for a [Button].
-     *
-     * @param defaultElevation the elevation to use when the [Button] is enabled, and has no
-     * other [Interaction]s.
-     * @param pressedElevation the elevation to use when the [Button] is enabled and
-     * is [Interaction.Pressed].
-     * @param disabledElevation the elevation to use when the [Button] is not enabled.
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "ButtonConstants has been replaced with ButtonDefaults",
-        ReplaceWith(
-            "ButtonDefaults.elevation(elevation, pressedElevation, disabledElevation)",
-            "androidx.compose.material.ButtonDefaults"
-        )
-    )
-    fun defaultElevation(
-        defaultElevation: Dp = 2.dp,
-        pressedElevation: Dp = 8.dp,
-        // focused: Dp = 4.dp,
-        // hovered: Dp = 4.dp,
-        disabledElevation: Dp = 0.dp
-    ): ButtonElevation {
-        val clock = AmbientAnimationClock.current.asDisposableClock()
-        return remember(defaultElevation, pressedElevation, disabledElevation, clock) {
-            DefaultButtonElevation(
-                defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation,
-                disabledElevation = disabledElevation,
-                clock = clock
-            )
-        }
-    }
-
-    /**
-     * Creates a [ButtonColors] that represents the default background and content colors used in
-     * a [Button].
-     *
-     * @param backgroundColor the background color of this [Button] when enabled
-     * @param disabledBackgroundColor the background color of this [Button] when not enabled
-     * @param contentColor the content color of this [Button] when enabled
-     * @param disabledContentColor the content color of this [Button] when not enabled
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "ButtonConstants has been replaced with ButtonDefaults",
-        ReplaceWith(
-            "ButtonDefaults.buttonColors(backgroundColor, disabledBackgroundColor, contentColor, " +
-                "disabledContentColor)",
-            "androidx.compose.material.ButtonDefaults"
-        )
-    )
-    fun defaultButtonColors(
-        backgroundColor: Color = MaterialTheme.colors.primary,
-        disabledBackgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.12f)
-            .compositeOver(MaterialTheme.colors.surface),
-        contentColor: Color = contentColorFor(backgroundColor),
-        disabledContentColor: Color = MaterialTheme.colors.onSurface
-            .copy(alpha = ContentAlpha.disabled)
-    ): ButtonColors = DefaultButtonColors(
-        backgroundColor,
-        disabledBackgroundColor,
-        contentColor,
-        disabledContentColor
-    )
-
-    /**
-     * Creates a [ButtonColors] that represents the default background and content colors used in
-     * an [OutlinedButton].
-     *
-     * @param backgroundColor the background color of this [OutlinedButton]
-     * @param contentColor the content color of this [OutlinedButton] when enabled
-     * @param disabledContentColor the content color of this [OutlinedButton] when not enabled
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "ButtonConstants has been replaced with ButtonDefaults",
-        ReplaceWith(
-            "ButtonDefaults.outlinedButtonColors(backgroundColor, disabledBackgroundColor, " +
-                "contentColor, disabledContentColor)",
-            "androidx.compose.material.ButtonDefaults"
-        )
-    )
-    fun defaultOutlinedButtonColors(
-        backgroundColor: Color = MaterialTheme.colors.surface,
-        contentColor: Color = MaterialTheme.colors.primary,
-        disabledContentColor: Color = MaterialTheme.colors.onSurface
-            .copy(alpha = ContentAlpha.disabled)
-    ): ButtonColors = DefaultButtonColors(
-        backgroundColor,
-        backgroundColor,
-        contentColor,
-        disabledContentColor
-    )
-
-    /**
-     * Creates a [ButtonColors] that represents the default background and content colors used in
-     * a [TextButton].
-     *
-     * @param backgroundColor the background color of this [TextButton]
-     * @param contentColor the content color of this [TextButton] when enabled
-     * @param disabledContentColor the content color of this [TextButton] when not enabled
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "ButtonConstants has been replaced with ButtonDefaults",
-        ReplaceWith(
-            "ButtonDefaults.textButtonColors(backgroundColor, disabledBackgroundColor, " +
-                "contentColor, disabledContentColor)",
-            "androidx.compose.material.ButtonDefaults"
-        )
-    )
-    fun defaultTextButtonColors(
-        backgroundColor: Color = Color.Transparent,
-        contentColor: Color = MaterialTheme.colors.primary,
-        disabledContentColor: Color = MaterialTheme.colors.onSurface
-            .copy(alpha = ContentAlpha.disabled)
-    ): ButtonColors = DefaultButtonColors(
-        backgroundColor,
-        backgroundColor,
-        contentColor,
-        disabledContentColor
-    )
-
-    /**
-     * The default color opacity used for an [OutlinedButton]'s border color
-     */
-    const val OutlinedBorderOpacity = 0.12f
-
-    /**
-     * The default [OutlinedButton]'s border size
-     */
-    val OutlinedBorderSize = 1.dp
-
-    /**
-     * The default disabled content color used by all types of [Button]s
-     */
-    val defaultOutlinedBorder: BorderStroke
-        @Composable
-        get() = BorderStroke(
-            OutlinedBorderSize, MaterialTheme.colors.onSurface.copy(alpha = OutlinedBorderOpacity)
-        )
-
-    private val TextButtonHorizontalPadding = 8.dp
-
-    /**
-     * The default content padding used by [TextButton]
-     */
-    val DefaultTextContentPadding = DefaultContentPadding.copy(
-        start = TextButtonHorizontalPadding,
-        end = TextButtonHorizontalPadding
-    )
-}
-
-/**
- * Contains the default values used by [Button]
- */
 object ButtonDefaults {
     private val ButtonHorizontalPadding = 16.dp
     private val ButtonVerticalPadding = 8.dp
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
index e16def2..d2493d0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Checkbox.kt
@@ -190,72 +190,6 @@
 }
 
 /**
- * Constants used in [Checkbox] and [TriStateCheckbox].
- */
-@Deprecated(
-    "CheckboxConstants has been replaced with CheckboxDefaults",
-    ReplaceWith(
-        "CheckboxDefaults",
-        "androidx.compose.material.CheckboxDefaults"
-    )
-)
-object CheckboxConstants {
-    /**
-     * Creates a [CheckboxColors] that will animate between the provided colors according to the
-     * Material specification.
-     *
-     * @param checkedColor the color that will be used for the border and box when checked
-     * @param uncheckedColor color that will be used for the border when unchecked
-     * @param checkmarkColor color that will be used for the checkmark when checked
-     * @param disabledColor color that will be used for the box and border when disabled
-     * @param disabledIndeterminateColor color that will be used for the box and
-     * border in a [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state.
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "CheckboxConstants has been replaced with CheckboxDefaults",
-        ReplaceWith(
-            "CheckboxDefaults.colors(checkedColor, uncheckedColor, checkmarkColor, disabledColor," +
-                " disabledIndeterminateColor)",
-            "androidx.compose.material.CheckboxDefaults"
-        )
-    )
-    fun defaultColors(
-        checkedColor: Color = MaterialTheme.colors.secondary,
-        uncheckedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
-        checkmarkColor: Color = MaterialTheme.colors.surface,
-        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled),
-        disabledIndeterminateColor: Color = checkedColor.copy(alpha = ContentAlpha.disabled)
-    ): CheckboxColors {
-        val clock = AmbientAnimationClock.current.asDisposableClock()
-        return remember(
-            checkedColor,
-            uncheckedColor,
-            checkmarkColor,
-            disabledColor,
-            disabledIndeterminateColor,
-            clock
-        ) {
-            DefaultCheckboxColors(
-                checkedBorderColor = checkedColor,
-                checkedBoxColor = checkedColor,
-                checkedCheckmarkColor = checkmarkColor,
-                uncheckedCheckmarkColor = checkmarkColor.copy(alpha = 0f),
-                uncheckedBoxColor = checkedColor.copy(alpha = 0f),
-                disabledCheckedBoxColor = disabledColor,
-                disabledUncheckedBoxColor = disabledColor.copy(alpha = 0f),
-                disabledIndeterminateBoxColor = disabledIndeterminateColor,
-                uncheckedBorderColor = uncheckedColor,
-                disabledBorderColor = disabledColor,
-                disabledIndeterminateBorderColor = disabledIndeterminateColor,
-                clock = clock
-            )
-        }
-    }
-}
-
-/**
  * Defaults used in [Checkbox] and [TriStateCheckbox].
  */
 object CheckboxDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
index 95656b7..ee2cbd9 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Drawer.kt
@@ -536,33 +536,6 @@
 /**
  * Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
  */
-@Deprecated(
-    "DrawerConstants has been replaced with DrawerDefaults",
-    ReplaceWith(
-        "DrawerDefaults",
-        "androidx.compose.material.DrawerDefaults"
-    )
-)
-object DrawerConstants {
-
-    /**
-     * Default Elevation for drawer sheet as specified in material specs
-     */
-    val DefaultElevation = 16.dp
-
-    val defaultScrimColor: Color
-        @Composable
-        get() = MaterialTheme.colors.onSurface.copy(alpha = ScrimDefaultOpacity)
-
-    /**
-     * Default alpha for scrim color
-     */
-    const val ScrimDefaultOpacity = 0.32f
-}
-
-/**
- * Object to hold default values for [ModalDrawerLayout] and [BottomDrawerLayout]
- */
 object DrawerDefaults {
 
     /**
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
index daa9ef7..3f8da84 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Elevation.kt
@@ -67,55 +67,6 @@
  *
  * @see animateElevation
  */
-@Deprecated(
-    "ElevationConstants has been replaced with ElevationDefaults",
-    ReplaceWith(
-        "ElevationDefaults",
-        "androidx.compose.material.ElevationDefaults"
-    )
-)
-object ElevationConstants {
-    /**
-     * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
-     * previous [Interaction], or from the default state. If [interaction] is unknown, then
-     * returns `null`.
-     *
-     * @param interaction the [Interaction] that is being animated to
-     */
-    fun incomingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
-        return when (interaction) {
-            is Interaction.Pressed -> DefaultIncomingSpec
-            is Interaction.Dragged -> DefaultIncomingSpec
-            else -> null
-        }
-    }
-
-    /**
-     * Returns the [AnimationSpec]s used when animating elevation away from [interaction], to the
-     * default state. If [interaction] is unknown, then returns `null`.
-     *
-     * @param interaction the [Interaction] that is being animated away from
-     */
-    fun outgoingAnimationSpecForInteraction(interaction: Interaction): AnimationSpec<Dp>? {
-        return when (interaction) {
-            is Interaction.Pressed -> DefaultOutgoingSpec
-            is Interaction.Dragged -> DefaultOutgoingSpec
-            // TODO: use [HoveredOutgoingSpec] when hovered
-            else -> null
-        }
-    }
-}
-
-/**
- * Contains default [AnimationSpec]s used for animating elevation between different [Interaction]s.
- *
- * Typically you should use [animateElevation] instead, which uses these [AnimationSpec]s
- * internally. [animateElevation] in turn is used by the defaults for [Button] and
- * [FloatingActionButton] - inside [ButtonDefaults.elevation] and
- * [FloatingActionButtonDefaults.elevation] respectively.
- *
- * @see animateElevation
- */
 object ElevationDefaults {
     /**
      * Returns the [AnimationSpec]s used when animating elevation to [interaction], either from a
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
deleted file mode 100644
index e92850d..0000000
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Emphasis.kt
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-@file:Suppress("DEPRECATION")
-
-package androidx.compose.material
-
-import androidx.compose.runtime.Ambient
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.Providers
-import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.luminance
-import androidx.compose.ui.util.annotation.FloatRange
-
-/**
- * Emphasis allows certain parts of a component to be accentuated, or shown with lower contrast
- * to reflect importance / state inside a component. For example, inside a disabled button, text
- * should have an emphasis level of [EmphasisLevels.disabled], to show that the button is
- * currently not active / able to be interacted with.
- *
- * Emphasis works by adjusting the color provided by [AmbientContentColor], so that emphasis levels
- * cascade through a subtree without requiring components to be aware of their context.
- *
- * The default implementations convey emphasis by changing the alpha / opacity of [color], to
- * increase / reduce contrast for a particular element.
- *
- * To set emphasis for a particular subtree, see [ProvideEmphasis].
- *
- * To define the emphasis levels in your application, see [EmphasisLevels] and [AmbientEmphasisLevels]
- * - note that this should not typically be customized, as the default values are optimized for
- * accessibility and contrast on different surfaces.
- *
- * For more information on emphasis and ensuring legibility for content, see
- * [Text legibility](https://material.io/design/color/text-legibility.html)
- */
-@Deprecated(
-    message = "Emphasis has been simplified and replaced with ContentAlpha"
-)
-@Immutable
-interface Emphasis {
-    /**
-     * Applies emphasis to the given [color].
-     */
-    fun applyEmphasis(color: Color): Color
-}
-
-/**
- * EmphasisLevels represents the different levels of [Emphasis] that can be applied to a component.
- *
- * By default, the [Emphasis] implementation for each level varies depending on the color being
- * emphasized (typically [AmbientContentColor]). This ensures that the [Emphasis] has the correct
- * contrast for the background they are on, as [Colors.primary] surfaces typically require
- * higher contrast for the content color than [Colors.surface] surfaces to ensure they are
- * accessible.
- *
- * This typically should not be customized as the default implementation is optimized for
- * correct accessibility and contrast on different surfaces.
- *
- * See [AmbientEmphasisLevels] to retrieve the current [EmphasisLevels]
- */
-@Deprecated(
-    message = "Emphasis has been simplified and replaced with ContentAlpha"
-)
-interface EmphasisLevels {
-    /**
-     * Emphasis used to express high emphasis, such as for selected text fields.
-     */
-    val high: Emphasis @Composable get
-    /**
-     * Emphasis used to express medium emphasis, such as for placeholder text in a text field.
-     */
-    val medium: Emphasis @Composable get
-    /**
-     * Emphasis used to express disabled state, such as for a disabled button.
-     */
-    val disabled: Emphasis @Composable get
-}
-
-/**
- * Applies [emphasis] to [content], by modifying the value of [AmbientContentColor].
- *
- * See [AmbientEmphasisLevels] to retrieve the levels of emphasis provided in the theme,
- * so they can be applied with this function.
- */
-@Deprecated(
-    message = "Emphasis has been simplified and replaced with ContentAlpha",
-    replaceWith = ReplaceWith(
-        "Providers(AmbientContentAlpha provides ContentAlpha.high, children = content)",
-        "androidx.compose.runtime.Providers",
-        "androidx.compose.material.ContentAlpha"
-    )
-)
-@Composable
-fun ProvideEmphasis(emphasis: Emphasis, content: @Composable () -> Unit) {
-    val emphasizedColor = emphasis.applyEmphasis(AmbientContentColor.current)
-    Providers(AmbientContentColor provides emphasizedColor, content = content)
-}
-
-@Deprecated(
-    message = "Emphasis has been simplified and replaced with ContentAlpha"
-)
-/**
- * Ambient containing the current [EmphasisLevels] in this hierarchy.
- */
-val AmbientEmphasisLevels: Ambient<EmphasisLevels> = staticAmbientOf { DefaultEmphasisLevels }
-
-private object DefaultEmphasisLevels : EmphasisLevels {
-
-    /**
-     * This default implementation uses separate alpha levels depending on the luminance of the
-     * incoming color, and whether the theme is light or dark. This is to ensure correct contrast
-     * and accessibility on all surfaces.
-     *
-     * See [HighContrastAlphaLevels] and [ReducedContrastAlphaLevels] for what the levels are
-     * used for, and under what circumstances.
-     */
-    private class AlphaEmphasis(
-        private val lightTheme: Boolean,
-        @FloatRange(from = 0.0, to = 1.0) private val highContrastAlpha: Float,
-        @FloatRange(from = 0.0, to = 1.0) private val reducedContrastAlpha: Float
-    ) : Emphasis {
-        override fun applyEmphasis(color: Color): Color {
-            if (color.alpha != 1f) return color
-            val alpha = if (lightTheme) {
-                if (color.luminance() > 0.5) highContrastAlpha else reducedContrastAlpha
-            } else {
-                if (color.luminance() < 0.5) highContrastAlpha else reducedContrastAlpha
-            }
-            return color.copy(alpha = alpha)
-        }
-    }
-
-    override val high: Emphasis
-        @Composable
-        get() = AlphaEmphasis(
-            lightTheme = MaterialTheme.colors.isLight,
-            highContrastAlpha = HighContrastAlphaLevels.high,
-            reducedContrastAlpha = ReducedContrastAlphaLevels.high
-        )
-
-    override val medium: Emphasis
-        @Composable
-        get() = AlphaEmphasis(
-            lightTheme = MaterialTheme.colors.isLight,
-            highContrastAlpha = HighContrastAlphaLevels.medium,
-            reducedContrastAlpha = ReducedContrastAlphaLevels.medium
-        )
-
-    override val disabled: Emphasis
-        @Composable
-        get() = AlphaEmphasis(
-            lightTheme = MaterialTheme.colors.isLight,
-            highContrastAlpha = HighContrastAlphaLevels.disabled,
-            reducedContrastAlpha = ReducedContrastAlphaLevels.disabled
-        )
-}
-
-/**
- * Alpha levels for high luminance content in light theme, or low luminance content in dark theme.
- *
- * This content will typically be placed on colored surfaces, so it is important that the
- * contrast here is higher to meet accessibility standards, and increase legibility.
- *
- * These levels are typically used for text / iconography in primary colored tabs /
- * bottom navigation / etc.
- */
-private object HighContrastAlphaLevels {
-    const val high: Float = 1.00f
-    const val medium: Float = 0.74f
-    const val disabled: Float = 0.38f
-}
-
-/**
- * Alpha levels for low luminance content in light theme, or high luminance content in dark theme.
- *
- * This content will typically be placed on grayscale surfaces, so the contrast here can be lower
- * without sacrificing accessibility and legibility.
- *
- * These levels are typically used for body text on the main surface (white in light theme, grey
- * in dark theme) and text / iconography in surface colored tabs / bottom navigation / etc.
- */
-private object ReducedContrastAlphaLevels {
-    const val high: Float = 0.87f
-    const val medium: Float = 0.60f
-    const val disabled: Float = 0.38f
-}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
index fe931083..7ccbb7e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/FloatingActionButton.kt
@@ -207,55 +207,6 @@
 /**
  * Contains the default values used by [FloatingActionButton]
  */
-@Deprecated(
-    "FloatingActionButtonConstants has been replaced with FloatingActionButtonDefaults",
-    ReplaceWith(
-        "FloatingActionButtonDefaults",
-        "androidx.compose.material.FloatingActionButtonDefaults"
-    )
-)
-object FloatingActionButtonConstants {
-    // TODO: b/152525426 add support for focused and hovered states
-    /**
-     * Creates a [FloatingActionButtonElevation] that will animate between the provided values
-     * according to the Material specification.
-     *
-     * @param defaultElevation the elevation to use when the [FloatingActionButton] has no
-     * [Interaction]s
-     * @param pressedElevation the elevation to use when the [FloatingActionButton] is
-     * [Interaction.Pressed].
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "FloatingActionButtonConstants has been replaced with " +
-            "FloatingActionButtonDefaults",
-        ReplaceWith(
-            "FloatingActionButtonDefaults.elevation(elevation, pressedElevation, " +
-                "disabledElevation)",
-            "androidx.compose.material.FloatingActionButtonDefaults"
-        )
-    )
-    fun defaultElevation(
-        defaultElevation: Dp = 6.dp,
-        pressedElevation: Dp = 12.dp
-        // focused: Dp = 8.dp,
-        // hovered: Dp = 8.dp,
-    ): FloatingActionButtonElevation {
-        val clock = AmbientAnimationClock.current.asDisposableClock()
-        return remember(defaultElevation, pressedElevation, clock) {
-            DefaultFloatingActionButtonElevation(
-                defaultElevation = defaultElevation,
-                pressedElevation = pressedElevation,
-                clock = clock
-            )
-        }
-    }
-}
-
-/**
- * Contains the default values used by [FloatingActionButton]
- */
 object FloatingActionButtonDefaults {
     // TODO: b/152525426 add support for focused and hovered states
     /**
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
index f663035..e21c901 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Icon.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.graphics.painter.ImagePainter
 import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.toolingGraphicsLayer
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.rememberVectorPainter
 import androidx.compose.ui.unit.dp
@@ -96,7 +97,10 @@
     // size that this icon will be forced to take up.
     // TODO: b/149735981 semantics for content description
     val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
-    Box(modifier.defaultSizeFor(painter).paint(painter, colorFilter = colorFilter))
+    Box(
+        modifier.toolingGraphicsLayer().defaultSizeFor(painter)
+            .paint(painter, colorFilter = colorFilter)
+    )
 }
 
 private fun Modifier.defaultSizeFor(painter: Painter) =
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index ae32700..3ea26dd 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -330,31 +330,6 @@
 private enum class BottomSheetStackSlot { SheetContent, Content }
 
 /**
- * Contains useful constants for [ModalBottomSheetLayout].
- */
-@Deprecated(
-    "ModalBottomSheetConstants has been replaced with ModalBottomSheetDefaults",
-    ReplaceWith(
-        "ModalBottomSheetDefaults",
-        "androidx.compose.material.ModalBottomSheetDefaults"
-    )
-)
-object ModalBottomSheetConstants {
-
-    /**
-     * The default elevation used by [ModalBottomSheetLayout].
-     */
-    val DefaultElevation = 16.dp
-
-    /**
-     * The default scrim color used by [ModalBottomSheetLayout].
-     */
-    val DefaultScrimColor: Color
-        @Composable
-        get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
-}
-
-/**
  * Contains useful Defaults for [ModalBottomSheetLayout].
  */
 object ModalBottomSheetDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
index 096155b..e5c9414 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ProgressIndicator.kt
@@ -258,45 +258,6 @@
 /**
  * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
  */
-@Deprecated(
-    "ProgressIndicatorConstants has been replaced with ProgressIndicatorDefaults",
-    ReplaceWith(
-        "ProgressIndicatorDefaults",
-        "androidx.compose.material.ProgressIndicatorDefaults"
-    )
-)
-object ProgressIndicatorConstants {
-    /**
-     * Default stroke width for [CircularProgressIndicator], and default height for
-     * [LinearProgressIndicator].
-     *
-     * This can be customized with the `strokeWidth` parameter on [CircularProgressIndicator],
-     * and by passing a layout modifier setting the height for [LinearProgressIndicator].
-     */
-    val DefaultStrokeWidth = 4.dp
-
-    /**
-     * The default opacity applied to the indicator color to create the background color in a
-     * [LinearProgressIndicator].
-     */
-    const val DefaultIndicatorBackgroundOpacity = 0.24f
-
-    /**
-     * The default [AnimationSpec] that should be used when animating between progress in a
-     * determinate progress indicator.
-     */
-    val DefaultProgressAnimationSpec = SpringSpec(
-        dampingRatio = Spring.DampingRatioNoBouncy,
-        stiffness = Spring.StiffnessVeryLow,
-        // The default threshold is 0.01, or 1% of the overall progress range, which is quite
-        // large and noticeable.
-        visibilityThreshold = 1 / 1000f
-    )
-}
-
-/**
- * Contains the default values used for [LinearProgressIndicator] and [CircularProgressIndicator].
- */
 object ProgressIndicatorDefaults {
     /**
      * Default stroke width for [CircularProgressIndicator], and default height for
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
index 2e66236..a8855a7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/RadioButton.kt
@@ -126,52 +126,6 @@
 }
 
 /**
- * Constants used in [RadioButton].
- */
-@Deprecated(
-    "RadioButtonConstants has been replaced with RadioButtonDefaults",
-    ReplaceWith(
-        "RadioButtonDefaults",
-        "androidx.compose.material.RadioButtonDefaults"
-    )
-)
-object RadioButtonConstants {
-    /**
-     * Creates a [RadioButtonColors] that will animate between the provided colors according to
-     * the Material specification.
-     *
-     * @param selectedColor the color to use for the RadioButton when selected and enabled.
-     * @param unselectedColor the color to use for the RadioButton when unselected and enabled.
-     * @param disabledColor the color to use for the RadioButton when disabled.
-     * @return the resulting [Color] used for the RadioButton
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "RadioButtonConstants has been replaced with RadioButtonDefaults",
-        ReplaceWith(
-            "RadioButtonDefaults.colors(selectedColor, unselectedColor, disabledColor)",
-            "androidx.compose.material.RadioButtonDefaults"
-        )
-    )
-    fun defaultColors(
-        selectedColor: Color = MaterialTheme.colors.secondary,
-        unselectedColor: Color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
-        disabledColor: Color = MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
-    ): RadioButtonColors {
-        val clock = AmbientAnimationClock.current.asDisposableClock()
-        return remember(
-            selectedColor,
-            unselectedColor,
-            disabledColor,
-            clock
-        ) {
-            DefaultRadioButtonColors(selectedColor, unselectedColor, disabledColor, clock)
-        }
-    }
-}
-
-/**
  * Defaults used in [RadioButton].
  */
 object RadioButtonDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
index 0c05a50..e688d8c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Slider.kt
@@ -189,28 +189,6 @@
 }
 
 /**
- * Object to hold constants used by the [Slider]
- */
-@Deprecated(
-    "SliderConstants has been replaced with SliderDefaults",
-    ReplaceWith(
-        "SliderDefaults",
-        "androidx.compose.material.SliderDefaults"
-    )
-)
-object SliderConstants {
-    /**
-     * Default alpha of the inactive part of the track
-     */
-    const val InactiveTrackColorAlpha = 0.24f
-
-    /**
-     * Default alpha of the ticks that are drawn on top of the track
-     */
-    const val TickColorAlpha = 0.54f
-}
-
-/**
  * Object to hold defaults used by [Slider]
  */
 object SliderDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
index dcdafed..256a30b 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Snackbar.kt
@@ -154,7 +154,7 @@
 ) {
     val actionLabel = snackbarData.actionLabel
     val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
-        {
+        @Composable {
             TextButton(
                 colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
                 onClick = { snackbarData.performAction() },
@@ -177,62 +177,6 @@
 }
 
 /**
- * Object to hold constants used by the [Snackbar]
- */
-@Deprecated(
-    "SnackbarConstants has been replaced with SnackbarDefaults",
-    ReplaceWith(
-        "SnackbarDefaults",
-        "androidx.compose.material.SnackbarDefaults"
-    )
-)
-object SnackbarConstants {
-
-    /**
-     * Default alpha of the overlay in the [defaultBackgroundColor]
-     */
-    private const val SnackbarOverlayAlpha = 0.8f
-
-    /**
-     * Default background color of the [Snackbar]
-     */
-    val defaultBackgroundColor: Color
-        @Composable
-        get() =
-            MaterialTheme.colors.onSurface
-                .copy(alpha = SnackbarOverlayAlpha)
-                .compositeOver(MaterialTheme.colors.surface)
-
-    /**
-     * Provides a best-effort 'primary' color to be used as the primary color inside a [Snackbar].
-     * Given that [Snackbar]s have an 'inverted' theme, i.e in a light theme they appear dark, and
-     * in a dark theme they appear light, just using [Colors.primary] will not work, and has
-     * incorrect contrast.
-     *
-     * If your light theme has a corresponding dark theme, you should instead directly use
-     * [Colors.primary] from the dark theme when in a light theme, and use
-     * [Colors.primaryVariant] from the dark theme when in a dark theme.
-     *
-     * When in a light theme, this function applies a color overlay to [Colors.primary] from
-     * [MaterialTheme.colors] to attempt to reduce the contrast, and when in a dark theme this
-     * function uses [Colors.primaryVariant].
-     */
-    val defaultActionPrimaryColor: Color
-        @Composable
-        get() {
-            val colors = MaterialTheme.colors
-            return if (colors.isLight) {
-                val primary = colors.primary
-                val overlayColor = colors.surface.copy(alpha = 0.6f)
-
-                overlayColor.compositeOver(primary)
-            } else {
-                colors.primaryVariant
-            }
-        }
-}
-
-/**
  * Object to hold defaults used by [Snackbar]
  */
 object SnackbarDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
index cde0667..ec711a5 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Swipeable.kt
@@ -722,57 +722,6 @@
 }
 
 /**
- * Contains useful constants for [swipeable] and [SwipeableState].
- */
-@Deprecated(
-    "SwipeableConstants has been replaced with SwipeableDefaults",
-    ReplaceWith(
-        "SwipeableDefaults",
-        "androidx.compose.material.SwipeableDefaults"
-    )
-)
-object SwipeableConstants {
-    /**
-     * The default animation used by [SwipeableState].
-     */
-    val DefaultAnimationSpec = SpringSpec<Float>()
-
-    /**
-     * The default velocity threshold (1.8 dp per millisecond) used by [swipeable].
-     */
-    val DefaultVelocityThreshold = 125.dp
-
-    /**
-     * A stiff resistance factor which indicates that swiping isn't available right now.
-     */
-    const val StiffResistanceFactor = 20f
-
-    /**
-     * A standard resistance factor which indicates that the user has run out of things to see.
-     */
-    const val StandardResistanceFactor = 10f
-
-    /**
-     * The default resistance config used by [swipeable].
-     *
-     * This returns `null` if there is one anchor. If there are at least two anchors, it returns
-     * a [ResistanceConfig] with the resistance basis equal to the distance between the two bounds.
-     */
-    fun defaultResistanceConfig(
-        anchors: Set<Float>,
-        factorAtMin: Float = StandardResistanceFactor,
-        factorAtMax: Float = StandardResistanceFactor
-    ): ResistanceConfig? {
-        return if (anchors.size <= 1) {
-            null
-        } else {
-            val basis = anchors.maxOrNull()!! - anchors.minOrNull()!!
-            ResistanceConfig(basis, factorAtMin, factorAtMax)
-        }
-    }
-}
-
-/**
  * Contains useful defaults for [swipeable] and [SwipeableState].
  */
 object SwipeableDefaults {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
index 3146d9a..d0d8f9c 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Switch.kt
@@ -212,77 +212,6 @@
 /**
  * Contains the default values used by [Switch]
  */
-@Deprecated(
-    "SwitchConstants has been replaced with SwitchDefaults",
-    ReplaceWith(
-        "SwitchDefaults",
-        "androidx.compose.material.SwitchDefaults"
-    )
-)
-object SwitchConstants {
-    /**
-     * Creates a [SwitchColors] that represents the different colors used in a [Switch] in
-     * different states.
-     *
-     * @param checkedThumbColor the color used for the thumb when enabled and checked
-     * @param checkedTrackColor the color used for the track when enabled and checked
-     * @param checkedTrackAlpha the alpha applied to [checkedTrackColor] and
-     * [disabledCheckedTrackColor]
-     * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked
-     * @param uncheckedTrackColor the color used for the track when enabled and unchecked
-     * @param uncheckedTrackAlpha the alpha applied to [uncheckedTrackColor] and
-     * [disabledUncheckedTrackColor]
-     * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked
-     * @param disabledCheckedTrackColor the color used for the track when disabled and checked
-     * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked
-     * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked
-     */
-    @OptIn(ExperimentalMaterialApi::class)
-    @Composable
-    @Deprecated(
-        "SwitchConstants has been replaced with SwitchDefaults",
-        ReplaceWith(
-            "SwitchDefaults.colors(checkedThumbColor, checkedTrackColor, checkedTrackAlpha, " +
-                "uncheckedThumbColor, uncheckedTrackColor, uncheckedTrackAlpha, " +
-                "disabledCheckedThumbColor, disabledCheckedTrackColor, " +
-                "disabledUncheckedThumbColor, disabledUncheckedTrackColor)",
-            "androidx.compose.material.SwitchDefaults"
-        )
-    )
-    fun defaultColors(
-        checkedThumbColor: Color = MaterialTheme.colors.secondaryVariant,
-        checkedTrackColor: Color = checkedThumbColor,
-        checkedTrackAlpha: Float = 0.54f,
-        uncheckedThumbColor: Color = MaterialTheme.colors.surface,
-        uncheckedTrackColor: Color = MaterialTheme.colors.onSurface,
-        uncheckedTrackAlpha: Float = 0.38f,
-        disabledCheckedThumbColor: Color = checkedThumbColor
-            .copy(alpha = ContentAlpha.disabled)
-            .compositeOver(MaterialTheme.colors.surface),
-        disabledCheckedTrackColor: Color = checkedTrackColor
-            .copy(alpha = ContentAlpha.disabled)
-            .compositeOver(MaterialTheme.colors.surface),
-        disabledUncheckedThumbColor: Color = uncheckedThumbColor
-            .copy(alpha = ContentAlpha.disabled)
-            .compositeOver(MaterialTheme.colors.surface),
-        disabledUncheckedTrackColor: Color = uncheckedTrackColor
-            .copy(alpha = ContentAlpha.disabled)
-            .compositeOver(MaterialTheme.colors.surface)
-    ): SwitchColors = DefaultSwitchColors(
-        checkedThumbColor = checkedThumbColor,
-        checkedTrackColor = checkedTrackColor.copy(alpha = checkedTrackAlpha),
-        uncheckedThumbColor = uncheckedThumbColor,
-        uncheckedTrackColor = uncheckedTrackColor.copy(alpha = uncheckedTrackAlpha),
-        disabledCheckedThumbColor = disabledCheckedThumbColor,
-        disabledCheckedTrackColor = disabledCheckedTrackColor.copy(alpha = checkedTrackAlpha),
-        disabledUncheckedThumbColor = disabledUncheckedThumbColor,
-        disabledUncheckedTrackColor = disabledUncheckedTrackColor.copy(alpha = uncheckedTrackAlpha)
-    )
-}
-
-/**
- * Contains the default values used by [Switch]
- */
 object SwitchDefaults {
     /**
      * Creates a [SwitchColors] that represents the different colors used in a [Switch] in
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
index 976e4ee..753cc9a 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Tab.kt
@@ -95,7 +95,7 @@
     selectedContentColor: Color = AmbientContentColor.current,
     unselectedContentColor: Color = selectedContentColor.copy(alpha = ContentAlpha.medium)
 ) {
-    val styledText = @Composable {
+    val styledText: @Composable () -> Unit = @Composable {
         val style = MaterialTheme.typography.button.copy(textAlign = TextAlign.Center)
         ProvideTextStyle(style, content = text)
     }
@@ -168,105 +168,6 @@
 /**
  * Contains default values used by tabs from the Material specification.
  */
-@Deprecated(
-    "TabConstants has been replaced with TabDefaults",
-    ReplaceWith(
-        "TabDefaults",
-        "androidx.compose.material.TabDefaults"
-    )
-)
-object TabConstants {
-    /**
-     * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
-     * indicator.
-     *
-     * @param modifier modifier for the divider's layout
-     * @param thickness thickness of the divider
-     * @param color color of the divider
-     */
-    @Composable
-    fun DefaultDivider(
-        modifier: Modifier = Modifier,
-        thickness: Dp = DefaultDividerThickness,
-        color: Color = AmbientContentColor.current.copy(alpha = DefaultDividerOpacity)
-    ) {
-        Divider(modifier = modifier, thickness = thickness, color = color)
-    }
-
-    /**
-     * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the
-     * divider.
-     *
-     * @param modifier modifier for the indicator's layout
-     * @param height height of the indicator
-     * @param color color of the indicator
-     */
-    @Composable
-    fun DefaultIndicator(
-        modifier: Modifier = Modifier,
-        height: Dp = DefaultIndicatorHeight,
-        color: Color = AmbientContentColor.current
-    ) {
-        Box(
-            modifier
-                .fillMaxWidth()
-                .preferredHeight(height)
-                .background(color = color)
-        )
-    }
-
-    /**
-     * [Modifier] that takes up all the available width inside the [TabRow], and then animates
-     * the offset of the indicator it is applied to, depending on the [currentTabPosition].
-     *
-     * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to
-     * calculate the offset of the indicator this modifier is applied to, as well as its width.
-     */
-    fun Modifier.defaultTabIndicatorOffset(
-        currentTabPosition: TabPosition
-    ): Modifier = composed(
-        inspectorInfo = debugInspectorInfo {
-            name = "defaultTabIndicatorOffset"
-            value = currentTabPosition
-        }
-    ) {
-        // TODO: should we animate the width of the indicator as it moves between tabs of different
-        // sizes inside a scrollable tab row?
-        val currentTabWidth = currentTabPosition.width
-        val indicatorOffset by animateAsState(
-            targetValue = currentTabPosition.left,
-            animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
-        )
-        fillMaxWidth()
-            .wrapContentSize(Alignment.BottomStart)
-            .offset(x = indicatorOffset)
-            .preferredWidth(currentTabWidth)
-    }
-
-    /**
-     * Default opacity for the color of [DefaultDivider]
-     */
-    const val DefaultDividerOpacity = 0.12f
-
-    /**
-     * Default thickness for [DefaultDivider]
-     */
-    val DefaultDividerThickness = 1.dp
-
-    /**
-     * Default height for [DefaultIndicator]
-     */
-    val DefaultIndicatorHeight = 2.dp
-
-    /**
-     * The default padding from the starting edge before a tab in a [ScrollableTabRow].
-     */
-    val DefaultScrollableTabRowPadding = 52.dp
-}
-
-/**
- * Contains default values used by tabs from the Material specification.
- */
 object TabDefaults {
     /**
      * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/AnimationClocks.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/AnimationClocks.kt
new file mode 100644
index 0000000..a8b9069
--- /dev/null
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/AnimationClocks.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.testutils
+
+import androidx.compose.animation.core.ManualFrameClock
+import androidx.compose.animation.core.MonotonicFrameAnimationClock
+import androidx.compose.animation.core.advanceClockMillis
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
+import kotlin.coroutines.CoroutineContext
+
+/**
+ * Creates a [MonotonicFrameAnimationClock] from the given [coroutineContext]'s clock. A new
+ * coroutine scope is created from the [coroutineContext].
+ *
+ * @see MonotonicFrameAnimationClock
+ */
+fun monotonicFrameAnimationClockOf(
+    coroutineContext: CoroutineContext
+): MonotonicFrameAnimationClock =
+    MonotonicFrameAnimationClock(
+        CoroutineScope(coroutineContext)
+    )
+
+/**
+ * Advances the clock on the main dispatcher.
+ *
+ * @see ManualFrameClock.advanceClock
+ */
+suspend fun ManualFrameClock.advanceClockOnMainThread(nanos: Long) {
+    withContext(Dispatchers.Main) {
+        advanceClock(nanos)
+        // Give awaiters the chance to await again
+        yield()
+    }
+}
+
+/**
+ * Advances the clock on the main dispatcher
+ *
+ * @see ManualFrameClock.advanceClock
+ */
+suspend fun ManualFrameClock.advanceClockOnMainThreadMillis(millis: Long) {
+    withContext(Dispatchers.Main) {
+        advanceClockMillis(millis)
+        // Give awaiters the chance to await again
+        yield()
+    }
+}
diff --git a/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/MockAnimationClock.kt b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/MockAnimationClock.kt
new file mode 100644
index 0000000..373f20f
--- /dev/null
+++ b/compose/test-utils/src/commonMain/kotlin/androidx/compose/testutils/MockAnimationClock.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.testutils
+
+import androidx.compose.animation.core.AnimationClockObservable
+import androidx.compose.animation.core.AnimationClockObserver
+
+/**
+ * An [AnimationClockObservable] that does nothing. Can be useful in tests if the test does not
+ * require time to progress.
+ */
+class MockAnimationClock : AnimationClockObservable {
+    override fun subscribe(observer: AnimationClockObserver) {}
+    override fun unsubscribe(observer: AnimationClockObserver) {}
+}
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt
index 5c3897d..18dff85 100644
--- a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/Matrix.kt
@@ -48,9 +48,12 @@
     fun map(point: Offset): Offset {
         val x = point.x
         val y = point.y
+        val z = this[0, 3] * x + this[1, 3] * y + this[3, 3]
+        val pZ = if (z == 0f) 0f else 1f / z
+
         return Offset(
-            x = this[0, 0] * x + this[1, 0] * y + this[3, 0],
-            y = this[0, 1] * x + this[1, 1] * y + this[3, 1]
+            x = pZ * (this[0, 0] * x + this[1, 0] * y + this[3, 0]),
+            y = pZ * (this[0, 1] * x + this[1, 1] * y + this[3, 1])
         )
     }
 
diff --git a/compose/ui/ui-graphics/src/test/java/androidx/compose/ui/graphics/MatrixTest.kt b/compose/ui/ui-graphics/src/test/java/androidx/compose/ui/graphics/MatrixTest.kt
index a191752..5e2a12a 100644
--- a/compose/ui/ui-graphics/src/test/java/androidx/compose/ui/graphics/MatrixTest.kt
+++ b/compose/ui/ui-graphics/src/test/java/androidx/compose/ui/graphics/MatrixTest.kt
@@ -56,6 +56,27 @@
     }
 
     @Test
+    fun mapPoint() {
+        val matrix = Matrix()
+        matrix.rotateZ(45f)
+        val zPoint = matrix.map(Offset(10f, 0f))
+        assertEquals(7.071f, zPoint.x, 0.01f)
+        assertEquals(7.071f, zPoint.y, 0.01f)
+
+        matrix.reset()
+        matrix.rotateX(45f)
+        val xPoint = matrix.map(Offset(0f, 10f))
+        assertEquals(0f, xPoint.x, 0.01f)
+        assertEquals(7.071f, xPoint.y, 0.01f)
+
+        matrix.reset()
+        matrix.rotateY(45f)
+        val yPoint = matrix.map(Offset(10f, 0f))
+        assertEquals(7.071f, yPoint.x, 0.01f)
+        assertEquals(0f, yPoint.y, 0.01f)
+    }
+
+    @Test
     fun mapRect() {
         val matrix = Matrix()
 
diff --git a/compose/ui/ui-test/api/current.txt b/compose/ui/ui-test/api/current.txt
index cea9973..ecf23dd 100644
--- a/compose/ui/ui-test/api/current.txt
+++ b/compose/ui/ui-test/api/current.txt
@@ -23,11 +23,6 @@
   public final class AndroidOutputKt {
   }
 
-  public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
-  }
-
   public final class AssertionsKt {
     method public static androidx.compose.ui.test.SemanticsNodeInteraction assert(androidx.compose.ui.test.SemanticsNodeInteraction, androidx.compose.ui.test.SemanticsMatcher matcher, optional kotlin.jvm.functions.Function0<java.lang.String>? messagePrefixOnError);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection assertAll(androidx.compose.ui.test.SemanticsNodeInteractionCollection, androidx.compose.ui.test.SemanticsMatcher matcher);
diff --git a/compose/ui/ui-test/api/public_plus_experimental_current.txt b/compose/ui/ui-test/api/public_plus_experimental_current.txt
index cea9973..ecf23dd 100644
--- a/compose/ui/ui-test/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test/api/public_plus_experimental_current.txt
@@ -23,11 +23,6 @@
   public final class AndroidOutputKt {
   }
 
-  public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
-  }
-
   public final class AssertionsKt {
     method public static androidx.compose.ui.test.SemanticsNodeInteraction assert(androidx.compose.ui.test.SemanticsNodeInteraction, androidx.compose.ui.test.SemanticsMatcher matcher, optional kotlin.jvm.functions.Function0<java.lang.String>? messagePrefixOnError);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection assertAll(androidx.compose.ui.test.SemanticsNodeInteractionCollection, androidx.compose.ui.test.SemanticsMatcher matcher);
diff --git a/compose/ui/ui-test/api/restricted_current.txt b/compose/ui/ui-test/api/restricted_current.txt
index cea9973..ecf23dd 100644
--- a/compose/ui/ui-test/api/restricted_current.txt
+++ b/compose/ui/ui-test/api/restricted_current.txt
@@ -23,11 +23,6 @@
   public final class AndroidOutputKt {
   }
 
-  public final class AnimationClocksKt {
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext, androidx.compose.runtime.dispatch.MonotonicFrameClock clock);
-    method @androidx.compose.ui.test.ExperimentalTestApi public static androidx.compose.animation.core.MonotonicFrameAnimationClock monotonicFrameAnimationClockOf(kotlin.coroutines.CoroutineContext coroutineContext);
-  }
-
   public final class AssertionsKt {
     method public static androidx.compose.ui.test.SemanticsNodeInteraction assert(androidx.compose.ui.test.SemanticsNodeInteraction, androidx.compose.ui.test.SemanticsMatcher matcher, optional kotlin.jvm.functions.Function0<java.lang.String>? messagePrefixOnError);
     method public static androidx.compose.ui.test.SemanticsNodeInteractionCollection assertAll(androidx.compose.ui.test.SemanticsNodeInteractionCollection, androidx.compose.ui.test.SemanticsMatcher matcher);
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
index 0f78706..830cce9 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/LayoutCoordinatesHelperTest.kt
@@ -71,7 +71,7 @@
         assertTrue(latch.await(1, TimeUnit.SECONDS))
         assertEquals(
             Offset.Zero,
-            parentCoordinates!!.childToLocal(childCoordinates!!, Offset.Zero)
+            parentCoordinates!!.localPositionOf(childCoordinates!!, Offset.Zero)
         )
     }
 
@@ -106,7 +106,7 @@
         assertTrue(latch.await(1, TimeUnit.SECONDS))
         assertEquals(
             Offset(5f, 0f),
-            parentCoordinates!!.childToLocal(childCoordinates!!, Offset.Zero)
+            parentCoordinates!!.localPositionOf(childCoordinates!!, Offset.Zero)
         )
     }
 }
diff --git a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
index 214f43e..f288535 100644
--- a/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
+++ b/compose/ui/ui-test/src/androidAndroidTest/kotlin/androidx/compose/ui/test/gesturescope/SendSwipeTest.kt
@@ -16,8 +16,6 @@
 
 package androidx.compose.ui.test.gesturescope
 
-import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.AnimationClockObserver
 import androidx.compose.animation.core.FloatExponentialDecaySpec
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.ScrollableColumn
@@ -27,18 +25,17 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.runtime.Composable
+import androidx.compose.testutils.MockAnimationClock
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.TouchSlop
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.unit.milliseconds
-import androidx.test.filters.MediumTest
 import androidx.compose.ui.test.bottomCenter
 import androidx.compose.ui.test.bottomRight
-import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.down
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.moveTo
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.performGesture
@@ -59,11 +56,13 @@
 import androidx.compose.ui.test.util.assertTimestampsAreIncreasing
 import androidx.compose.ui.test.util.isAlmostEqualTo
 import androidx.compose.ui.test.util.verify
+import androidx.compose.ui.unit.milliseconds
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -168,11 +167,7 @@
         val scrollState = ScrollState(
             initial = 0f,
             flingConfig = FlingConfig(FloatExponentialDecaySpec()),
-            animationClock = object : AnimationClockObservable {
-                // Use a "broken" clock, we just want response to input, not to time
-                override fun subscribe(observer: AnimationClockObserver) {}
-                override fun unsubscribe(observer: AnimationClockObserver) {}
-            }
+            animationClock = MockAnimationClock()
         )
         rule.setContent {
             with(AmbientDensity.current) {
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
index 109ea4a..9c5ada8 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt
@@ -66,7 +66,7 @@
     // from any other ancestors.
     val viewPortInParent = scrollableNode.layoutInfo.coordinates.boundsInParent
     val parentInRoot = scrollableNode.layoutInfo.coordinates.parentCoordinates
-        ?.positionInRoot ?: Offset.Zero
+        ?.positionInRoot() ?: Offset.Zero
 
     val viewPort = viewPortInParent.translate(parentInRoot)
     val target = Rect(node.positionInRoot, node.size.toSize())
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
index c745d64..c6f6b57f 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/GestureScope.kt
@@ -285,6 +285,7 @@
  * @param position A position in local coordinates
  */
 private fun GestureScope.localToGlobal(position: Offset): Offset {
+    @Suppress("DEPRECATION")
     return position + semanticsNode.layoutInfo.coordinates.globalBounds.topLeft
 }
 
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestAnimationClock.kt
similarity index 67%
rename from compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
rename to compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestAnimationClock.kt
index d883a9e2..990dd91 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/AnimationClocks.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestAnimationClock.kt
@@ -17,10 +17,6 @@
 package androidx.compose.ui.test
 
 import androidx.compose.animation.core.AnimationClockObservable
-import androidx.compose.animation.core.MonotonicFrameAnimationClock
-import androidx.compose.runtime.dispatch.MonotonicFrameClock
-import kotlinx.coroutines.CoroutineScope
-import kotlin.coroutines.CoroutineContext
 
 /**
  * Interface for animation clocks that can report their idleness and can switch between ticking
@@ -65,33 +61,3 @@
      */
     fun advanceClock(milliseconds: Long)
 }
-
-/**
- * Creates a [MonotonicFrameAnimationClock] from the given [clock]. A new coroutine scope is
- * created from the [coroutineContext], dispatching on the main thread and using the [clock] as
- * the frame clock.
- *
- * @see MonotonicFrameAnimationClock
- */
-@ExperimentalTestApi
-fun monotonicFrameAnimationClockOf(
-    coroutineContext: CoroutineContext,
-    clock: MonotonicFrameClock
-): MonotonicFrameAnimationClock =
-    MonotonicFrameAnimationClock(
-        CoroutineScope(coroutineContext + clock)
-    )
-
-/**
- * Creates a [MonotonicFrameAnimationClock] from the given [coroutineContext]'s clock. A new
- * coroutine scope is created from the [coroutineContext].
- *
- * @see MonotonicFrameAnimationClock
- */
-@ExperimentalTestApi
-fun monotonicFrameAnimationClockOf(
-    coroutineContext: CoroutineContext
-): MonotonicFrameAnimationClock =
-    MonotonicFrameAnimationClock(
-        CoroutineScope(coroutineContext)
-    )
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.kt
index 7e1a2a6..06af24f 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopFont.kt
@@ -19,6 +19,7 @@
 import androidx.compose.ui.text.font.FontListFontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.font.GenericFontFamily
 import org.jetbrains.skija.FontMgr
 import org.jetbrains.skija.Typeface
 import org.jetbrains.skija.paragraph.FontCollection
@@ -27,6 +28,44 @@
 import java.nio.file.Files
 import java.nio.file.StandardCopyOption
 import androidx.compose.ui.text.font.Font as uiFont
+import org.jetbrains.skija.FontStyle as SkFontStyle
+
+internal val GenericFontFamiliesMapping by lazy {
+    when (Platform.Current) {
+        Platform.Windows ->
+            mapOf(
+                FontFamily.SansSerif.name to listOf("Arial"),
+                FontFamily.Serif.name to listOf("Times New Roman"),
+                FontFamily.Monospace.name to listOf("Consolas"),
+                FontFamily.Cursive.name to listOf("Comic Sans MS")
+            )
+        Platform.MacOS ->
+            mapOf(
+                FontFamily.SansSerif.name to listOf(
+                    "Helvetica Neue",
+                    "Helvetica"
+                ),
+                FontFamily.Serif.name to listOf("Times"),
+                FontFamily.Monospace.name to listOf("Courier"),
+                FontFamily.Cursive.name to listOf("Apple Chancery")
+            )
+        Platform.Linux ->
+            mapOf(
+                FontFamily.SansSerif.name to listOf("Noto Sans", "DejaVu Sans"),
+                FontFamily.Serif.name to listOf("Noto Serif", "DejaVu Serif", "Times New Roman"),
+                FontFamily.Monospace.name to listOf("Noto Sans Mono", "DejaVu Sans Mono"),
+                // better alternative?
+                FontFamily.Cursive.name to listOf("Comic Sans MS")
+            )
+        Platform.Unknown ->
+            mapOf(
+                FontFamily.SansSerif.name to listOf("Arial"),
+                FontFamily.Serif.name to listOf("Times New Roman"),
+                FontFamily.Monospace.name to listOf("Consolas"),
+                FontFamily.Cursive.name to listOf("Comic Sans MS")
+            )
+    }
+}
 
 data class Font(
     val alias: String,
@@ -51,9 +90,15 @@
         fonts.setAssetFontManager(fontProvider)
     }
 
+    private fun mapGenericFontFamily(generic: GenericFontFamily): List<String> {
+        return GenericFontFamiliesMapping[generic.name]
+            ?: error("Unknown generic font family ${generic.name}")
+    }
+
     fun ensureRegistered(fontFamily: FontFamily): List<String> =
         when (fontFamily) {
             is FontListFontFamily -> fontFamily.fonts.map { load(it) }
+            is GenericFontFamily -> mapGenericFontFamily(fontFamily)
             FontFamily.Default -> listOf()
             else -> throw IllegalArgumentException("Unknown font family type: $fontFamily")
         }
@@ -81,6 +126,9 @@
                 val alias = load(fontFamily.fonts.first())
                 return registered[alias]!!
             }
+            is GenericFontFamily -> {
+                Typeface.makeFromName(mapGenericFontFamily(fontFamily).first(), SkFontStyle.NORMAL)
+            }
             FontFamily.Default -> Typeface.makeDefault()
             else -> throw IllegalArgumentException("Unknown font family type: $fontFamily")
         }
@@ -108,4 +156,23 @@
     Files.createDirectories(tempPath.parent)
     Files.copy(stream, tempPath, StandardCopyOption.REPLACE_EXISTING)
     return tempFile.absolutePath
+}
+
+private enum class Platform {
+    Linux,
+    Windows,
+    MacOS,
+    Unknown;
+
+    companion object {
+        val Current by lazy {
+            val name = System.getProperty("os.name")
+            when {
+                name.startsWith("Linux") -> Linux
+                name.startsWith("Win") -> Windows
+                name == "Mac OS X" -> MacOS
+                else -> Unknown
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-tooling/api/current.txt b/compose/ui/ui-tooling/api/current.txt
index 2d94392..66d8799 100644
--- a/compose/ui/ui-tooling/api/current.txt
+++ b/compose/ui/ui-tooling/api/current.txt
@@ -135,7 +135,10 @@
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.compose.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
     method public java.util.List<androidx.compose.ui.tooling.inspector.NodeParameter> convertParameters(androidx.compose.ui.tooling.inspector.InspectorNode node);
+    method public boolean getHideSystemNodes();
     method public void resetGeneratedId();
+    method public void setHideSystemNodes(boolean p);
+    property public final boolean hideSystemNodes;
   }
 
   public final class LayoutInspectorTreeKt {
diff --git a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
index 2d94392..66d8799 100644
--- a/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-tooling/api/public_plus_experimental_current.txt
@@ -135,7 +135,10 @@
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.compose.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
     method public java.util.List<androidx.compose.ui.tooling.inspector.NodeParameter> convertParameters(androidx.compose.ui.tooling.inspector.InspectorNode node);
+    method public boolean getHideSystemNodes();
     method public void resetGeneratedId();
+    method public void setHideSystemNodes(boolean p);
+    property public final boolean hideSystemNodes;
   }
 
   public final class LayoutInspectorTreeKt {
diff --git a/compose/ui/ui-tooling/api/restricted_current.txt b/compose/ui/ui-tooling/api/restricted_current.txt
index 2d94392..66d8799 100644
--- a/compose/ui/ui-tooling/api/restricted_current.txt
+++ b/compose/ui/ui-tooling/api/restricted_current.txt
@@ -135,7 +135,10 @@
     ctor public LayoutInspectorTree();
     method public java.util.List<androidx.compose.ui.tooling.inspector.InspectorNode> convert(android.view.View view);
     method public java.util.List<androidx.compose.ui.tooling.inspector.NodeParameter> convertParameters(androidx.compose.ui.tooling.inspector.InspectorNode node);
+    method public boolean getHideSystemNodes();
     method public void resetGeneratedId();
+    method public void setHideSystemNodes(boolean p);
+    property public final boolean hideSystemNodes;
   }
 
   public final class LayoutInspectorTreeKt {
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
index a5070a6..51b3d3a 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/ModifierInfoTest.kt
@@ -83,14 +83,14 @@
                     "${boxModifierInfo[0].modifier}",
                 boxModifierInfo[0].modifier is LayoutModifier
             )
-            assertEquals(10f, boxModifierInfo[0].coordinates.positionInRoot.x)
+            assertEquals(10f, boxModifierInfo[0].coordinates.positionInRoot().x)
 
             assertTrue(
                 "Box should only have LayoutModifiers, but the second was " +
                     "${boxModifierInfo[1].modifier}",
                 boxModifierInfo[1].modifier is LayoutModifier
             )
-            assertEquals(15f, boxModifierInfo[1].coordinates.positionInRoot.x)
+            assertEquals(15f, boxModifierInfo[1].coordinates.positionInRoot().x)
 
             val columnModifierInfo = modifierInfo[1]
             assertEquals(3, columnModifierInfo.size)
@@ -99,20 +99,20 @@
                     "but was ${columnModifierInfo[0].modifier}",
                 columnModifierInfo[0].modifier is LayoutModifier
             )
-            assertEquals(0f, columnModifierInfo[0].coordinates.positionInRoot.x)
+            assertEquals(0f, columnModifierInfo[0].coordinates.positionInRoot().x)
             assertTrue(
                 "The second modifier in the column should be a LayoutModifier" +
                     "but was ${columnModifierInfo[1].modifier}",
                 columnModifierInfo[1].modifier is LayoutModifier
             )
             assertTrue(columnModifierInfo[2].extra is OwnedLayer)
-            assertEquals(10f, columnModifierInfo[1].coordinates.positionInRoot.x)
+            assertEquals(10f, columnModifierInfo[1].coordinates.positionInRoot().x)
             assertTrue(
                 "The third modifier in the column should be a DrawModifier" +
                     "but was ${columnModifierInfo[2].modifier}",
                 columnModifierInfo[2].modifier is DrawModifier
             )
-            assertEquals(10f, columnModifierInfo[2].coordinates.positionInRoot.x)
+            assertEquals(10f, columnModifierInfo[2].coordinates.positionInRoot().x)
         }
     }
 }
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
index 89c3db5..24829dd 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTreeTest.kt
@@ -24,22 +24,25 @@
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.material.Button
+import androidx.compose.material.Icon
 import androidx.compose.material.ModalDrawerLayout
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Call
+import androidx.compose.material.icons.filled.FavoriteBorder
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.resetSourceInfo
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.node.OwnedLayer
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.Group
 import androidx.compose.ui.tooling.Inspectable
 import androidx.compose.ui.tooling.R
-import androidx.compose.ui.tooling.CompositionDataRecord
 import androidx.compose.ui.tooling.ToolingTest
 import androidx.compose.ui.tooling.asTree
 import androidx.compose.ui.tooling.position
@@ -51,8 +54,8 @@
 import androidx.test.filters.SdkSuppress
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
+import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import kotlin.math.roundToInt
@@ -66,14 +69,20 @@
     private lateinit var view: View
 
     @Before
-    fun density() {
+    fun before() {
         @OptIn(InternalComposeApi::class)
         resetSourceInfo()
         density = Density(activity)
         view = activityTestRule.activity.findViewById<ViewGroup>(android.R.id.content)
+        isDebugInspectorInfoEnabled = true
     }
 
-    @Ignore("Manual test")
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29
     @Test
     fun buildTree() {
         val slotTableRecord = CompositionDataRecord.create()
@@ -82,6 +91,7 @@
             Inspectable(slotTableRecord) {
                 Column {
                     Text(text = "Hello World", color = Color.Green)
+                    Icon(Icons.Filled.FavoriteBorder)
                     Surface {
                         Button(onClick = {}) { Text(text = "OK") }
                     }
@@ -93,296 +103,56 @@
         view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
         val viewWidth = with(density) { view.width.toDp() }
         val viewHeight = with(density) { view.height.toDp() }
-        dumpSlotTableSet(slotTableRecord)
         val builder = LayoutInspectorTree()
         val nodes = builder.convert(view)
         dumpNodes(nodes, builder)
 
-        validate(nodes, builder, checkParameters = true) {
+        validate(nodes, builder, checkParameters = false) {
             node(
                 name = "Box",
-                fileName = "Box.kt",
-                left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
-                children = listOf("Column")
-            ) {
-                parameter(name = "shape", type = ParameterType.String, value = "Shape")
-                parameter(name = "backgroundColor", type = ParameterType.Color, value = 0x0)
-                parameter(name = "padding", type = ParameterType.DimensionDp, value = 0.0f)
-                parameter(
-                    name = "paddingStart",
-                    type = ParameterType.DimensionDp,
-                    value = Float.NaN
-                )
-                parameter(name = "paddingTop", type = ParameterType.DimensionDp, value = Float.NaN)
-                parameter(name = "paddingEnd", type = ParameterType.DimensionDp, value = Float.NaN)
-                parameter(
-                    name = "paddingBottom",
-                    type = ParameterType.DimensionDp,
-                    value = Float.NaN
-                )
-                parameter(name = "gravity", type = ParameterType.String, value = "TopStart")
-            }
-
-            node(
-                name = "Column",
-                fileName = "Box.kt",
+                fileName = "",
                 left = 0.0.dp, top = 0.0.dp, width = viewWidth, height = viewHeight,
                 children = listOf("Column")
             )
             node(
                 name = "Column",
                 fileName = "LayoutInspectorTreeTest.kt",
-                left = 0.0.dp,
-                top = 0.0.dp,
-                width = 70.5.dp,
-                height = 54.9.dp,
-                children = listOf("Text", "Surface")
-            ) {
-                parameter(name = "verticalArrangement", type = ParameterType.String, value = "Top")
-                parameter(name = "horizontalGravity", type = ParameterType.String, value = "Start")
-            }
+                left = 0.0.dp, top = 0.0.dp, width = 72.0.dp, height = 78.9.dp,
+                children = listOf("Text", "Icon", "Surface")
+            )
             node(
                 name = "Text",
-                fileName = "LayoutInspectorTreeTest.kt",
-                left = 0.0.dp,
-                top = 0.0.dp, width = 70.5.dp, height = 18.9.dp, children = listOf("Text")
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "Hello World")
-                parameter(name = "color", type = ParameterType.Color, value = 0xff00ff00.toInt())
-                parameter(name = "fontSize", type = ParameterType.String, value = "Inherit")
-                parameter(name = "letterSpacing", type = ParameterType.String, value = "Inherit")
-                parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
-            node(
-                name = "Text",
-                fileName = "Text.kt",
-                left = 0.0.dp,
-                top = 0.0.dp, width = 70.5.dp, height = 18.9.dp, children = listOf("CoreText")
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "Hello World")
-                parameter(name = "color", type = ParameterType.Color, value = 0xff00ff00.toInt())
-                parameter(name = "fontSize", type = ParameterType.String, value = "Inherit")
-                parameter(name = "letterSpacing", type = ParameterType.String, value = "Inherit")
-                parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
-            node(
-                name = "CoreText",
-                fileName = "CoreText.kt",
                 isRenderNode = true,
-                left = 0.0.dp, top = 0.0.dp, width = 70.5.dp, height = 18.9.dp
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "Hello World")
-                parameter(name = "style", type = ParameterType.String, value = "TextStyle") {
-                    parameter(
-                        name = "color",
-                        type = ParameterType.Color,
-                        value = 0xff00ff00.toInt()
-                    )
-                    parameter(name = "fontSize", type = ParameterType.String, value = "Inherit")
-                    parameter(
-                        name = "letterSpacing",
-                        type = ParameterType.String,
-                        value = "Inherit"
-                    )
-                    parameter(name = "background", type = ParameterType.String, value = "Unset")
-                    parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                }
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
+                fileName = "LayoutInspectorTreeTest.kt",
+                left = 0.0.dp, top = 0.0.dp, width = 72.0.dp, height = 18.9.dp,
+            )
+            node(
+                name = "Icon",
+                isRenderNode = true,
+                fileName = "LayoutInspectorTreeTest.kt",
+                left = 0.0.dp, top = 18.9.dp, width = 24.0.dp, height = 24.0.dp,
+            )
             node(
                 name = "Surface",
                 fileName = "LayoutInspectorTreeTest.kt",
                 isRenderNode = true,
                 left = 0.0.dp,
-                top = 18.9.dp, width = 64.0.dp, height = 36.0.dp, children = listOf("Button")
-            ) {
-                parameter(name = "shape", type = ParameterType.String, value = "Shape")
-                parameter(name = "color", type = ParameterType.Color, value = 0xffffffff.toInt())
-                parameter(
-                    name = "contentColor",
-                    type = ParameterType.Color,
-                    value = 0xff000000.toInt()
-                )
-                parameter(name = "elevation", type = ParameterType.DimensionDp, value = 0.0f)
-            }
+                top = 42.9.dp, width = 64.0.dp, height = 36.0.dp,
+                children = listOf("Button")
+            )
             node(
                 name = "Button",
                 fileName = "LayoutInspectorTreeTest.kt",
                 left = 0.0.dp,
-                top = 18.9.dp, width = 64.0.dp, height = 36.0.dp, children = listOf("Surface")
-            ) {
-                parameter(name = "enabled", type = ParameterType.Boolean, value = true)
-                parameter(name = "elevation", type = ParameterType.DimensionDp, value = 2.0f)
-                parameter(
-                    name = "disabledElevation",
-                    type = ParameterType.DimensionDp,
-                    value = 0.0f
-                )
-                parameter(
-                    name = "shape",
-                    type = ParameterType.String,
-                    value = "RoundedCornerShape"
-                ) {
-                    parameter(name = "topLeft", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "topRight", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "bottomLeft", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "bottomRight", type = ParameterType.DimensionDp, value = 4.0f)
-                }
-                parameter(
-                    name = "backgroundColor", type = ParameterType.Color,
-                    value = 0xff6200ee.toInt()
-                )
-                parameter(
-                    name = "disabledBackgroundColor", type = ParameterType.Color,
-                    value = 0xffe0e0e0.toInt()
-                )
-                parameter(
-                    name = "contentColor",
-                    type = ParameterType.Color,
-                    value = 0xffffffff.toInt()
-                )
-                parameter(
-                    name = "disabledContentColor",
-                    type = ParameterType.Color,
-                    value = 0x61000000
-                )
-                parameter(name = "padding", type = ParameterType.String, value = "PaddingValues") {
-                    parameter(name = "start", type = ParameterType.DimensionDp, value = 16.0f)
-                    parameter(name = "end", type = ParameterType.DimensionDp, value = 16.0f)
-                    parameter(name = "top", type = ParameterType.DimensionDp, value = 8.0f)
-                    parameter(name = "bottom", type = ParameterType.DimensionDp, value = 8.0f)
-                }
-            }
-            node(
-                name = "Surface",
-                fileName = "Button.kt",
-                isRenderNode = true,
-                left = 0.0.dp,
-                top = 18.9.dp,
-                width = 64.0.dp,
-                height = 36.0.dp,
-                children = listOf("ProvideTextStyle")
-            ) {
-                parameter(
-                    name = "shape",
-                    type = ParameterType.String,
-                    value = "RoundedCornerShape"
-                ) {
-                    parameter(name = "topLeft", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "topRight", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "bottomLeft", type = ParameterType.DimensionDp, value = 4.0f)
-                    parameter(name = "bottomRight", type = ParameterType.DimensionDp, value = 4.0f)
-                }
-                parameter(name = "color", type = ParameterType.Color, value = 0xff6200ee.toInt())
-                parameter(
-                    name = "contentColor",
-                    type = ParameterType.Color,
-                    value = 0xffffffff.toInt()
-                )
-                parameter(name = "elevation", type = ParameterType.DimensionDp, value = 2.0f)
-            }
-            node(
-                name = "ProvideTextStyle",
-                fileName = "Button.kt",
-                left = 16.0.dp,
-                top = 26.9.dp, width = 32.0.dp, height = 20.0.dp, children = listOf("Row")
-            ) {
-                parameter(name = "value", type = ParameterType.String, value = "TextStyle") {
-                    parameter(name = "color", type = ParameterType.String, value = "Unset")
-                    parameter(name = "fontSize", type = ParameterType.DimensionSp, value = 14.0f)
-                    parameter(name = "fontWeight", type = ParameterType.String, value = "Medium")
-                    parameter(name = "fontFamily", type = ParameterType.String, value = "Default")
-                    parameter(
-                        name = "letterSpacing",
-                        type = ParameterType.DimensionSp,
-                        value = 1.25f
-                    )
-                    parameter(name = "background", type = ParameterType.String, value = "Unset")
-                    parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                }
-            }
-            node(
-                name = "Row",
-                fileName = "Button.kt",
-                left = 16.0.dp,
-                top = 26.9.dp, width = 32.0.dp, height = 20.0.dp, children = listOf("Text")
-            ) {
-                parameter(
-                    name = "horizontalArrangement",
-                    type = ParameterType.String,
-                    value = "Center"
-                )
-                parameter(
-                    name = "verticalGravity", type = ParameterType.String,
-                    value = "CenterVertically"
-                )
-            }
+                top = 42.9.dp, width = 64.0.dp, height = 36.0.dp,
+                children = listOf("Text")
+            )
             node(
                 name = "Text",
+                isRenderNode = true,
                 fileName = "LayoutInspectorTreeTest.kt",
-                left = 21.8.dp,
-                top = 27.6.dp, width = 20.4.dp, height = 18.9.dp, children = listOf("Text")
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "OK")
-                parameter(name = "color", type = ParameterType.String, value = "Unset")
-                parameter(name = "fontSize", type = ParameterType.String, value = "Inherit")
-                parameter(name = "letterSpacing", type = ParameterType.String, value = "Inherit")
-                parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
-            node(
-                name = "Text",
-                fileName = "Text.kt",
-                left = 21.8.dp,
-                top = 27.6.dp, width = 20.4.dp, height = 18.9.dp, children = listOf("CoreText")
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "OK")
-                parameter(name = "color", type = ParameterType.String, value = "Unset")
-                parameter(name = "fontSize", type = ParameterType.String, value = "Inherit")
-                parameter(name = "letterSpacing", type = ParameterType.String, value = "Inherit")
-                parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
-            node(
-                name = "CoreText",
-                fileName = "CoreText.kt",
-                isRenderNode = true,
-                left = 21.8.dp, top = 27.6.dp, width = 20.4.dp, height = 18.9.dp
-            ) {
-                parameter(name = "text", type = ParameterType.String, value = "OK")
-                parameter(name = "style", type = ParameterType.String, value = "TextStyle") {
-                    parameter(
-                        name = "color",
-                        type = ParameterType.Color,
-                        value = 0xffffffff.toInt()
-                    )
-                    parameter(name = "fontSize", type = ParameterType.DimensionSp, value = 14.0f)
-                    parameter(name = "fontWeight", type = ParameterType.String, value = "Medium")
-                    parameter(name = "fontFamily", type = ParameterType.String, value = "Default")
-                    parameter(
-                        name = "letterSpacing",
-                        type = ParameterType.DimensionSp,
-                        value = 1.25f
-                    )
-                    parameter(name = "background", type = ParameterType.String, value = "Unset")
-                    parameter(name = "lineHeight", type = ParameterType.String, value = "Inherit")
-                }
-                parameter(name = "softWrap", type = ParameterType.Boolean, value = true)
-                parameter(name = "overflow", type = ParameterType.String, value = "Clip")
-                parameter(name = "maxLines", type = ParameterType.Int32, value = 2147483647)
-            }
+                left = 21.7.dp, top = 51.6.dp, width = 20.9.dp, height = 18.9.dp,
+            )
         }
     }
 
@@ -412,6 +182,44 @@
         if (DEBUG) {
             validate(nodes, builder, checkParameters = false) {
                 node("Box", children = listOf("ModalDrawerLayout"))
+                node("ModalDrawerLayout", children = listOf("Column", "Text"))
+                node("Column", children = listOf("Text", "Button"))
+                node("Text")
+                node("Button", children = listOf("Text"))
+                node("Text")
+                node("Text")
+            }
+        }
+        assertThat(nodes.size).isEqualTo(1)
+    }
+
+    @Test
+    fun testStitchTreeFromModelDrawerLayoutWithSystemNodes() {
+        val slotTableRecord = CompositionDataRecord.create()
+
+        show {
+            Inspectable(slotTableRecord) {
+                ModalDrawerLayout(
+                    drawerContent = { Text("Something") },
+                    bodyContent = {
+                        Column {
+                            Text(text = "Hello World", color = Color.Green)
+                            Button(onClick = {}) { Text(text = "OK") }
+                        }
+                    }
+                )
+            }
+        }
+        view.setTag(R.id.inspection_slot_table_set, slotTableRecord.store)
+        dumpSlotTableSet(slotTableRecord)
+        val builder = LayoutInspectorTree()
+        builder.hideSystemNodes = false
+        val nodes = builder.convert(view)
+        dumpNodes(nodes, builder)
+
+        if (DEBUG) {
+            validate(nodes, builder, checkParameters = false) {
+                node("Box", children = listOf("ModalDrawerLayout"))
                 node("ModalDrawerLayout", children = listOf("WithConstraints"))
                 node("WithConstraints", children = listOf("SubcomposeLayout"))
                 node("SubcomposeLayout", children = listOf("Box"))
@@ -488,9 +296,8 @@
         assertThat(node?.parameters).isNotEmpty()
     }
 
-    @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29:  b/171519437
+    @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29
     @Test
-    @Ignore("b/174152464")
     fun testTextId() {
         val slotTableRecord = CompositionDataRecord.create()
 
@@ -504,12 +311,13 @@
         val builder = LayoutInspectorTree()
         val node = builder.convert(view)
             .flatMap { flatten(it) }
-            .firstOrNull { it.name == "CoreText" }
+            .firstOrNull { it.name == "Text" }
 
-        // LayoutNode id should be captured by the CoreText node:
+        // LayoutNode id should be captured by the Text node:
         assertThat(node?.id).isGreaterThan(0)
     }
 
+    @Suppress("SameParameterValue")
     private fun validate(
         result: List<InspectorNode>,
         builder: LayoutInspectorTree,
@@ -542,24 +350,30 @@
             assertWithMessage("No such node found: $name").that(nodeIterator.hasNext()).isTrue()
             val node = nodeIterator.next()
             assertThat(node.name).isEqualTo(name)
-            assertThat(node.children.map { it.name }).containsExactlyElementsIn(children).inOrder()
-            fileName?.let { assertThat(node.fileName).isEqualTo(fileName) }
+            val message = "Node: $name"
+            assertWithMessage(message).that(node.children.map { it.name })
+                .containsExactlyElementsIn(children).inOrder()
+            fileName?.let { assertWithMessage(message).that(node.fileName).isEqualTo(fileName) }
             if (lineNumber != -1) {
-                assertThat(node.lineNumber).isEqualTo(lineNumber)
+                assertWithMessage(message).that(node.lineNumber).isEqualTo(lineNumber)
             }
             if (isRenderNode != null) {
                 if (isRenderNode) {
-                    assertThat(node.id).isGreaterThan(0L)
+                    assertWithMessage(message).that(node.id).isGreaterThan(0L)
                 } else {
-                    assertThat(node.id).isLessThan(0L)
+                    assertWithMessage(message).that(node.id).isLessThan(0L)
                 }
             }
             if (left != Dp.Unspecified) {
                 with(density) {
-                    assertThat(node.left.toDp().value).isWithin(2.0f).of(left.value)
-                    assertThat(node.top.toDp().value).isWithin(2.0f).of(top.value)
-                    assertThat(node.width.toDp().value).isWithin(2.0f).of(width.value)
-                    assertThat(node.height.toDp().value).isWithin(2.0f).of(height.value)
+                    assertWithMessage(message).that(node.left.toDp().value)
+                        .isWithin(2.0f).of(left.value)
+                    assertWithMessage(message).that(node.top.toDp().value)
+                        .isWithin(2.0f).of(top.value)
+                    assertWithMessage(message).that(node.width.toDp().value)
+                        .isWithin(2.0f).of(width.value)
+                    assertWithMessage(message).that(node.height.toDp().value)
+                        .isWithin(2.0f).of(height.value)
                 }
             }
 
@@ -602,7 +416,11 @@
         node.children.forEach { dumpNode(it, indent + 1) }
     }
 
-    private fun generateValidate(node: InspectorNode, builder: LayoutInspectorTree) {
+    private fun generateValidate(
+        node: InspectorNode,
+        builder: LayoutInspectorTree,
+        generateParameters: Boolean = false
+    ) {
         with(density) {
             val left = round(node.left.toDp())
             val top = round(node.top.toDp())
@@ -629,7 +447,7 @@
         }
         println()
         print(")")
-        if (node.parameters.isNotEmpty()) {
+        if (generateParameters && node.parameters.isNotEmpty()) {
             generateParameters(builder.convertParameters(node), 0)
         }
         println()
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
index f38aa50..0a5ee33 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/SlotTree.kt
@@ -18,9 +18,9 @@
 
 import androidx.compose.runtime.CompositionData
 import androidx.compose.runtime.CompositionGroup
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.LayoutInfo
 import androidx.compose.ui.layout.ModifierInfo
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.unit.IntBounds
 import java.lang.reflect.Field
 import kotlin.math.max
@@ -480,7 +480,7 @@
             bottom = node.height
         )
     }
-    val position = node.coordinates.globalPosition
+    val position = node.coordinates.positionInWindow()
     val size = node.coordinates.size
     val left = position.x.roundToInt()
     val top = position.y.roundToInt()
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
index f2acc87..3b90233 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/inspector/LayoutInspectorTree.kt
@@ -33,6 +33,25 @@
 import java.util.IdentityHashMap
 import kotlin.math.absoluteValue
 
+private val systemPackages = setOf(
+    packageNameHash("androidx.compose.animation"),
+    packageNameHash("androidx.compose.animation.core"),
+    packageNameHash("androidx.compose.desktop"),
+    packageNameHash("androidx.compose.foundation"),
+    packageNameHash("androidx.compose.foundation.layout"),
+    packageNameHash("androidx.compose.foundation.text"),
+    packageNameHash("androidx.compose.material"),
+    packageNameHash("androidx.compose.material.ripple"),
+    packageNameHash("androidx.compose.runtime"),
+    packageNameHash("androidx.compose.ui"),
+    packageNameHash("androidx.compose.ui.layout"),
+    packageNameHash("androidx.compose.ui.platform"),
+    packageNameHash("androidx.compose.ui.tooling"),
+    packageNameHash("androidx.compose.ui.selection"),
+    packageNameHash("androidx.compose.ui.semantics"),
+    packageNameHash("androidx.compose.ui.viewinterop")
+)
+
 private val unwantedPackages = setOf(
     -1,
     packageNameHash("androidx.compose.ui"),
@@ -59,6 +78,8 @@
  * Generator of a tree for the Layout Inspector.
  */
 class LayoutInspectorTree {
+    @Suppress("MemberVisibilityCanBePrivate")
+    var hideSystemNodes = true
     private val inlineClassConverter = InlineClassConverter()
     private val parameterFactory = ParameterFactory(inlineClassConverter)
     private val cache = ArrayDeque<MutableInspectorNode>()
@@ -98,6 +119,7 @@
     /**
      * Reset the generated id. Nodes are assigned an id if there isn't a layout node id present.
      */
+    @Suppress("unused")
     fun resetGeneratedId() {
         generatedId = -1L
     }
@@ -116,7 +138,7 @@
         val trees = tables.map { convert(it) }
         return when (trees.size) {
             0 -> listOf()
-            1 -> trees.first().children
+            1 -> addTree(mutableListOf(), trees.single())
             else -> stitchTreesByLayoutNode(trees)
         }
     }
@@ -155,7 +177,9 @@
             treeMap.remove(parentTree)
             parentTree = findDeepParentTree()
         }
-        return trees.asSequence().filter { !stitched.contains(it) }.flatMap { it.children }.toList()
+        val result = mutableListOf<InspectorNode>()
+        trees.asSequence().filter { !stitched.contains(it) }.forEach { addTree(result, it) }
+        return result
     }
 
     /**
@@ -194,16 +218,34 @@
         }
         val newCopy = newNode ?: newNode(node)
         if (trees != null) {
-            trees.flatMapTo(newCopy.children) { it.children }
+            trees.forEach { addTree(newCopy.children, it) }
             stitched.addAll(trees)
         }
         return buildAndRelease(newCopy)
     }
 
+    /**
+     * Add [tree] to the end of the [out] list.
+     * The root nodes of [tree] may be a fake node that hold a list of [LayoutInfo].
+     */
+    private fun addTree(
+        out: MutableList<InspectorNode>,
+        tree: MutableInspectorNode
+    ): List<InspectorNode> {
+        tree.children.forEach {
+            if (it.name.isNotEmpty()) {
+                out.add(it)
+            } else {
+                out.addAll(it.children)
+            }
+        }
+        return out
+    }
+
     @OptIn(InternalComposeApi::class)
     private fun convert(table: CompositionData): MutableInspectorNode {
         val fakeParent = newNode()
-        addToParent(fakeParent, listOf(convert(table.asTree())))
+        addToParent(fakeParent, listOf(convert(table.asTree())), buildFakeChildNodes = true)
         return fakeParent
     }
 
@@ -234,13 +276,17 @@
 
     /**
      * Adds the nodes in [input] to the children of [parentNode].
-     * Nodes without a reference to a Composable are skipped.
+     * Nodes without a reference to a wanted Composable are skipped unless [buildFakeChildNodes].
      * A single skipped render id and layoutNode will be added to [parentNode].
      */
-    private fun addToParent(parentNode: MutableInspectorNode, input: List<MutableInspectorNode>) {
+    private fun addToParent(
+        parentNode: MutableInspectorNode,
+        input: List<MutableInspectorNode>,
+        buildFakeChildNodes: Boolean = false
+    ) {
         var id: Long? = null
         input.forEach { node ->
-            if (node.name.isEmpty()) {
+            if (node.name.isEmpty() && !(buildFakeChildNodes && node.layoutNodes.isNotEmpty())) {
                 parentNode.children.addAll(node.children)
                 if (node.id != 0L) {
                     // If multiple siblings with a render ids are dropped:
@@ -322,7 +368,8 @@
     }
 
     private fun unwantedGroup(node: MutableInspectorNode): Boolean =
-        (node.packageHash in unwantedPackages && node.name in unwantedCalls)
+        (node.packageHash in unwantedPackages && node.name in unwantedCalls) ||
+            (hideSystemNodes && node.packageHash in systemPackages)
 
     private fun newNode(): MutableInspectorNode =
         if (cache.isNotEmpty()) cache.pop() else MutableInspectorNode()
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 3829487..076e58f 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -385,12 +385,12 @@
   }
 
   public static final class FocusRequester.Companion {
-    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
     method public androidx.compose.ui.focus.FocusRequester getDefault();
     property public final androidx.compose.ui.focus.FocusRequester Default;
   }
 
-  public static final class FocusRequester.Companion.FocusRequesterFactory {
+  @androidx.compose.ui.ExperimentalComposeUiApi public static final class FocusRequester.Companion.FocusRequesterFactory {
     method public operator androidx.compose.ui.focus.FocusRequester component1();
     method public operator androidx.compose.ui.focus.FocusRequester component10();
     method public operator androidx.compose.ui.focus.FocusRequester component11();
@@ -663,6 +663,7 @@
   public final class GraphicsLayerModifierKt {
     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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer-i_P0lGk(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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
   public interface GraphicsLayerScope extends androidx.compose.ui.unit.Density {
@@ -1770,16 +1771,20 @@
   }
 
   public interface LayoutCoordinates {
-    method public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
-    method public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
+    method @Deprecated public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
+    method @Deprecated public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
     method public operator int get(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.layout.LayoutCoordinates? getParentCoordinates();
     method public java.util.Set<androidx.compose.ui.layout.AlignmentLine> getProvidedAlignmentLines();
     method public long getSize-YbymL2g();
-    method public long globalToLocal-k-4lQ0M(long global);
+    method @Deprecated public long globalToLocal-k-4lQ0M(long global);
     method public boolean isAttached();
-    method public long localToGlobal-k-4lQ0M(long local);
-    method public long localToRoot-k-4lQ0M(long local);
+    method public androidx.compose.ui.geometry.Rect localBoundingBoxOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, optional boolean clipBounds);
+    method public long localPositionOf-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
+    method @Deprecated public long localToGlobal-k-4lQ0M(long local);
+    method public long localToRoot-k-4lQ0M(long relativeToLocal);
+    method public long localToWindow-k-4lQ0M(long relativeToLocal);
+    method public long windowToLocal-k-4lQ0M(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
     property public abstract java.util.Set<androidx.compose.ui.layout.AlignmentLine> providedAlignmentLines;
@@ -1787,12 +1792,16 @@
   }
 
   public final class LayoutCoordinatesKt {
+    method public static androidx.compose.ui.geometry.Rect boundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static androidx.compose.ui.geometry.Rect boundsInWindow(androidx.compose.ui.layout.LayoutCoordinates);
     method public static androidx.compose.ui.geometry.Rect getBoundsInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
     method public static long getPositionInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInWindow(androidx.compose.ui.layout.LayoutCoordinates);
   }
 
   public final class LayoutIdKt {
@@ -2074,6 +2083,7 @@
 
   public interface Owner {
     method public long calculatePosition-nOcc-ac();
+    method public long calculatePositionInWindow-nOcc-ac();
     method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
@@ -2091,7 +2101,8 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public androidx.compose.ui.platform.WindowManager getWindowManager();
+    method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
+    method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2117,7 +2128,8 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
+    property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
+    property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2189,7 +2201,8 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo> getAmbientWindowInfo();
+    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo>! getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
@@ -2399,13 +2412,18 @@
     property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
   }
 
-  @androidx.compose.runtime.Stable public interface WindowManager {
+  @androidx.compose.runtime.Stable public interface WindowInfo {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
   }
 
-  public final class WindowManagerKt {
-    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  public final class WindowInfoKt {
+    method @Deprecated @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
+  @Deprecated @androidx.compose.runtime.Stable public interface WindowManager {
+    method @Deprecated public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
   }
 
   @androidx.compose.ui.InternalComposeUiApi public fun interface WindowRecomposerFactory {
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 3829487..076e58f 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -385,12 +385,12 @@
   }
 
   public static final class FocusRequester.Companion {
-    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
     method public androidx.compose.ui.focus.FocusRequester getDefault();
     property public final androidx.compose.ui.focus.FocusRequester Default;
   }
 
-  public static final class FocusRequester.Companion.FocusRequesterFactory {
+  @androidx.compose.ui.ExperimentalComposeUiApi public static final class FocusRequester.Companion.FocusRequesterFactory {
     method public operator androidx.compose.ui.focus.FocusRequester component1();
     method public operator androidx.compose.ui.focus.FocusRequester component10();
     method public operator androidx.compose.ui.focus.FocusRequester component11();
@@ -663,6 +663,7 @@
   public final class GraphicsLayerModifierKt {
     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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer-i_P0lGk(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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
   public interface GraphicsLayerScope extends androidx.compose.ui.unit.Density {
@@ -1770,16 +1771,20 @@
   }
 
   public interface LayoutCoordinates {
-    method public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
-    method public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
+    method @Deprecated public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
+    method @Deprecated public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
     method public operator int get(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.layout.LayoutCoordinates? getParentCoordinates();
     method public java.util.Set<androidx.compose.ui.layout.AlignmentLine> getProvidedAlignmentLines();
     method public long getSize-YbymL2g();
-    method public long globalToLocal-k-4lQ0M(long global);
+    method @Deprecated public long globalToLocal-k-4lQ0M(long global);
     method public boolean isAttached();
-    method public long localToGlobal-k-4lQ0M(long local);
-    method public long localToRoot-k-4lQ0M(long local);
+    method public androidx.compose.ui.geometry.Rect localBoundingBoxOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, optional boolean clipBounds);
+    method public long localPositionOf-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
+    method @Deprecated public long localToGlobal-k-4lQ0M(long local);
+    method public long localToRoot-k-4lQ0M(long relativeToLocal);
+    method public long localToWindow-k-4lQ0M(long relativeToLocal);
+    method public long windowToLocal-k-4lQ0M(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
     property public abstract java.util.Set<androidx.compose.ui.layout.AlignmentLine> providedAlignmentLines;
@@ -1787,12 +1792,16 @@
   }
 
   public final class LayoutCoordinatesKt {
+    method public static androidx.compose.ui.geometry.Rect boundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static androidx.compose.ui.geometry.Rect boundsInWindow(androidx.compose.ui.layout.LayoutCoordinates);
     method public static androidx.compose.ui.geometry.Rect getBoundsInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
     method public static long getPositionInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInWindow(androidx.compose.ui.layout.LayoutCoordinates);
   }
 
   public final class LayoutIdKt {
@@ -2074,6 +2083,7 @@
 
   public interface Owner {
     method public long calculatePosition-nOcc-ac();
+    method public long calculatePositionInWindow-nOcc-ac();
     method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
@@ -2091,7 +2101,8 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public androidx.compose.ui.platform.WindowManager getWindowManager();
+    method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
+    method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2117,7 +2128,8 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
+    property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
+    property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2189,7 +2201,8 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo> getAmbientWindowInfo();
+    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo>! getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
@@ -2399,13 +2412,18 @@
     property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
   }
 
-  @androidx.compose.runtime.Stable public interface WindowManager {
+  @androidx.compose.runtime.Stable public interface WindowInfo {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
   }
 
-  public final class WindowManagerKt {
-    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  public final class WindowInfoKt {
+    method @Deprecated @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
+  @Deprecated @androidx.compose.runtime.Stable public interface WindowManager {
+    method @Deprecated public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
   }
 
   @androidx.compose.ui.InternalComposeUiApi public fun interface WindowRecomposerFactory {
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index b4de3f0..8eefb7e 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -385,12 +385,12 @@
   }
 
   public static final class FocusRequester.Companion {
-    method public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
+    method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory createRefs();
     method public androidx.compose.ui.focus.FocusRequester getDefault();
     property public final androidx.compose.ui.focus.FocusRequester Default;
   }
 
-  public static final class FocusRequester.Companion.FocusRequesterFactory {
+  @androidx.compose.ui.ExperimentalComposeUiApi public static final class FocusRequester.Companion.FocusRequesterFactory {
     method public operator androidx.compose.ui.focus.FocusRequester component1();
     method public operator androidx.compose.ui.focus.FocusRequester component10();
     method public operator androidx.compose.ui.focus.FocusRequester component11();
@@ -663,6 +663,7 @@
   public final class GraphicsLayerModifierKt {
     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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier graphicsLayer-i_P0lGk(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 @androidx.compose.runtime.Stable public static androidx.compose.ui.Modifier toolingGraphicsLayer(androidx.compose.ui.Modifier);
   }
 
   public interface GraphicsLayerScope extends androidx.compose.ui.unit.Density {
@@ -1816,16 +1817,20 @@
   }
 
   public interface LayoutCoordinates {
-    method public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
-    method public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
+    method @Deprecated public androidx.compose.ui.geometry.Rect childBoundingBox(androidx.compose.ui.layout.LayoutCoordinates child);
+    method @Deprecated public long childToLocal-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates child, long childLocal);
     method public operator int get(androidx.compose.ui.layout.AlignmentLine line);
     method public androidx.compose.ui.layout.LayoutCoordinates? getParentCoordinates();
     method public java.util.Set<androidx.compose.ui.layout.AlignmentLine> getProvidedAlignmentLines();
     method public long getSize-YbymL2g();
-    method public long globalToLocal-k-4lQ0M(long global);
+    method @Deprecated public long globalToLocal-k-4lQ0M(long global);
     method public boolean isAttached();
-    method public long localToGlobal-k-4lQ0M(long local);
-    method public long localToRoot-k-4lQ0M(long local);
+    method public androidx.compose.ui.geometry.Rect localBoundingBoxOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, optional boolean clipBounds);
+    method public long localPositionOf-YJiYy8w(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
+    method @Deprecated public long localToGlobal-k-4lQ0M(long local);
+    method public long localToRoot-k-4lQ0M(long relativeToLocal);
+    method public long localToWindow-k-4lQ0M(long relativeToLocal);
+    method public long windowToLocal-k-4lQ0M(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
     property public abstract java.util.Set<androidx.compose.ui.layout.AlignmentLine> providedAlignmentLines;
@@ -1833,12 +1838,16 @@
   }
 
   public final class LayoutCoordinatesKt {
+    method public static androidx.compose.ui.geometry.Rect boundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static androidx.compose.ui.geometry.Rect boundsInWindow(androidx.compose.ui.layout.LayoutCoordinates);
     method public static androidx.compose.ui.geometry.Rect getBoundsInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getBoundsInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static androidx.compose.ui.geometry.Rect getGlobalBounds(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getGlobalPosition(androidx.compose.ui.layout.LayoutCoordinates);
     method public static long getPositionInParent(androidx.compose.ui.layout.LayoutCoordinates);
-    method public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method @Deprecated public static inline long getPositionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInRoot(androidx.compose.ui.layout.LayoutCoordinates);
+    method public static long positionInWindow(androidx.compose.ui.layout.LayoutCoordinates);
   }
 
   public final class LayoutIdKt {
@@ -2136,6 +2145,7 @@
 
   public interface Owner {
     method public long calculatePosition-nOcc-ac();
+    method public long calculatePositionInWindow-nOcc-ac();
     method public androidx.compose.ui.node.OwnedLayer createLayer(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.Canvas,kotlin.Unit> drawBlock, kotlin.jvm.functions.Function0<kotlin.Unit> invalidateParentLayer);
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.Autofill? getAutofill();
     method @androidx.compose.ui.ExperimentalComposeUiApi public androidx.compose.ui.autofill.AutofillTree getAutofillTree();
@@ -2153,7 +2163,8 @@
     method public androidx.compose.ui.text.input.TextInputService getTextInputService();
     method public androidx.compose.ui.platform.TextToolbar getTextToolbar();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
-    method public androidx.compose.ui.platform.WindowManager getWindowManager();
+    method public androidx.compose.ui.platform.WindowInfo getWindowInfo();
+    method @Deprecated public default androidx.compose.ui.platform.WindowInfo! getWindowManager();
     method public void measureAndLayout();
     method public void onAttach(androidx.compose.ui.node.LayoutNode node);
     method public void onDetach(androidx.compose.ui.node.LayoutNode node);
@@ -2179,7 +2190,8 @@
     property public abstract androidx.compose.ui.text.input.TextInputService textInputService;
     property public abstract androidx.compose.ui.platform.TextToolbar textToolbar;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
-    property public abstract androidx.compose.ui.platform.WindowManager windowManager;
+    property public abstract androidx.compose.ui.platform.WindowInfo windowInfo;
+    property @Deprecated public default androidx.compose.ui.platform.WindowInfo! windowManager;
     field public static final androidx.compose.ui.node.Owner.Companion Companion;
   }
 
@@ -2251,7 +2263,8 @@
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.TextToolbar> getAmbientTextToolbar();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.UriHandler> getAmbientUriHandler();
     method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ViewConfiguration> getAmbientViewConfiguration();
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowManager> getAmbientWindowManager();
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo> getAmbientWindowInfo();
+    method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.WindowInfo>! getAmbientWindowManager();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.animation.core.AnimationClockObservable>! getAnimationClockAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.platform.ClipboardManager>! getClipboardManagerAmbient();
     method @Deprecated public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.ui.unit.Density>! getDensityAmbient();
@@ -2461,13 +2474,18 @@
     property public final kotlin.jvm.functions.Function1<androidx.compose.ui.platform.ViewRootForTest,kotlin.Unit>? onViewCreatedCallback;
   }
 
-  @androidx.compose.runtime.Stable public interface WindowManager {
+  @androidx.compose.runtime.Stable public interface WindowInfo {
     method public boolean isWindowFocused();
     property public abstract boolean isWindowFocused;
   }
 
-  public final class WindowManagerKt {
-    method @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  public final class WindowInfoKt {
+    method @Deprecated @androidx.compose.runtime.Composable public static void WindowFocusObserver(kotlin.jvm.functions.Function1<? super java.lang.Boolean,kotlin.Unit> onWindowFocusChanged);
+  }
+
+  @Deprecated @androidx.compose.runtime.Stable public interface WindowManager {
+    method @Deprecated public boolean isWindowFocused();
+    property public abstract boolean isWindowFocused;
   }
 
   @androidx.compose.ui.InternalComposeUiApi public fun interface WindowRecomposerFactory {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
index bd6598f..9ec65f5 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/autofill/ExplicitAutofillTypesDemo.kt
@@ -130,6 +130,7 @@
     }
 }
 
+@Suppress("DEPRECATION")
 private fun LayoutCoordinates.boundingBox() = localToGlobal(Offset.Zero).run {
     Rect(
         x.toInt(),
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
index fa0e8de..e8e4549 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/CustomFocusOrderDemo.kt
@@ -29,6 +29,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusModifier
@@ -43,6 +44,7 @@
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
+@OptIn(ExperimentalComposeUiApi::class)
 @Composable
 fun CustomFocusOrderDemo() {
     Column {
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
index 2769825..861a17f 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInDialog.kt
@@ -31,7 +31,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.graphics.Color.Companion.White
-import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.platform.AmbientWindowInfo
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Dialog
@@ -41,9 +41,9 @@
     var showDialog by remember { mutableStateOf(false) }
     var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
     var dialogText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
-    val windowManager = AmbientWindowManager.current
+    val windowInfo = AmbientWindowInfo.current
 
-    Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+    Column(Modifier.background(if (windowInfo.isWindowFocused) White else LightGray)) {
         Text("Click the button to show the dialog. Click outside the dialog to dismiss it.")
         Spacer(Modifier.height(10.dp))
         Button(onClick = { showDialog = true }) {
@@ -70,6 +70,6 @@
 
 @Composable
 private fun FocusStatus() {
-    val windowManager = AmbientWindowManager.current
-    Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+    val windowInfo = AmbientWindowInfo.current
+    Text("Status: Window ${if (windowInfo.isWindowFocused) "is" else "is not"} focused.")
 }
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
index e040b3c..09d81c5 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/focus/FocusInPopup.kt
@@ -32,7 +32,7 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color.Companion.LightGray
 import androidx.compose.ui.graphics.Color.Companion.White
-import androidx.compose.ui.platform.AmbientWindowManager
+import androidx.compose.ui.platform.AmbientWindowInfo
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.window.Popup
@@ -42,9 +42,9 @@
     var showPopup by remember { mutableStateOf(false) }
     var mainText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
     var popupText by remember { mutableStateOf(TextFieldValue("Enter Value")) }
-    val windowManager = AmbientWindowManager.current
+    val windowInfo = AmbientWindowInfo.current
 
-    Column(Modifier.background(if (windowManager.isWindowFocused) White else LightGray)) {
+    Column(Modifier.background(if (windowInfo.isWindowFocused) White else LightGray)) {
         Text("Click the button to show the popup. Click outside the popup to dismiss it.")
         Spacer(Modifier.height(10.dp))
         Button(onClick = { showPopup = true }) {
@@ -75,6 +75,6 @@
 
 @Composable
 private fun FocusStatus() {
-    val windowManager = AmbientWindowManager.current
-    Text("Status: Window ${if (windowManager.isWindowFocused) "is" else "is not"} focused.")
+    val windowInfo = AmbientWindowInfo.current
+    Text("Status: Window ${if (windowInfo.isWindowFocused) "is" else "is not"} focused.")
 }
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/OnGloballyPositionedSamples.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/OnGloballyPositionedSamples.kt
index 66f4b60..3ed8955 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/OnGloballyPositionedSamples.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/OnGloballyPositionedSamples.kt
@@ -24,9 +24,9 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -37,9 +37,9 @@
             // This will be the size of the Column.
             coordinates.size
             // The position of the Column relative to the application window.
-            coordinates.globalPosition
+            coordinates.positionInWindow()
             // The position of the Column relative to the Compose root.
-            coordinates.positionInRoot
+            coordinates.positionInRoot()
             // These will be the alignment lines provided to the layout (empty here for Column).
             coordinates.providedAlignmentLines
             // This will a LayoutCoordinates instance corresponding to the parent of Column.
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 8060931..39769fe 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
@@ -2252,7 +2252,7 @@
         activity.runOnUiThread {
             assertEquals(size, resultCoordinates?.size?.height)
             assertEquals(size, resultCoordinates?.size?.width)
-            assertEquals(IntOffset(offset, offset).toOffset(), resultCoordinates?.positionInRoot)
+            assertEquals(IntOffset(offset, offset).toOffset(), resultCoordinates?.positionInRoot())
         }
     }
 
@@ -2307,7 +2307,7 @@
         activity.runOnUiThread {
             assertEquals(coordinates?.size?.height, convenienceCoordinates?.size?.height)
             assertEquals(coordinates?.size?.width, convenienceCoordinates?.size?.width)
-            assertEquals(coordinates?.positionInRoot, convenienceCoordinates?.positionInRoot)
+            assertEquals(coordinates?.positionInRoot(), convenienceCoordinates?.positionInRoot())
         }
     }
 
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 4d650c4..7d9c808 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
@@ -24,6 +24,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.FixedSize
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Padding
@@ -40,10 +41,10 @@
 import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.boundsInRoot
-import androidx.compose.ui.layout.globalBounds
-import androidx.compose.ui.layout.globalPosition
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.padding
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.testTag
@@ -88,11 +89,11 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            assertEquals(Offset(10f, 10f), layoutCoordinates.positionInRoot)
-            val bounds = layoutCoordinates.boundsInRoot
+            assertEquals(Offset(10f, 10f), layoutCoordinates.positionInRoot())
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(10f, 10f, 40f, 40f), bounds)
-            val global = layoutCoordinates.globalBounds
-            val position = layoutCoordinates.globalPosition
+            val global = layoutCoordinates.boundsInWindow()
+            val position = layoutCoordinates.positionInWindow()
             assertEquals(position.x, global.left)
             assertEquals(position.y, global.top)
             assertEquals(30f, global.width)
@@ -117,9 +118,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(5f, 0f, 25f, 30f), bounds)
-            assertEquals(Offset(5f, 0f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(5f, 0f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -140,9 +141,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(5f, 0f, 25f, 30f), bounds)
-            assertEquals(Offset(5f, 0f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(5f, 0f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -163,9 +164,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(5f, 5f, 25f, 25f), bounds)
-            assertEquals(Offset(5f, 5f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(5f, 5f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -186,9 +187,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(0f, 10f, 30f, 20f), bounds)
-            assertEquals(Offset(30f, 10f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(30f, 10f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -209,9 +210,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(10.0f, 10f, 20f, 20f), bounds)
-            assertEquals(Offset(20f, 10f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(20f, 10f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -234,9 +235,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(20f, 10f, 30f, 20f), bounds)
-            assertEquals(Offset(30f, 10f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(30f, 10f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -259,9 +260,9 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(15f, 18f, 25f, 28f), bounds)
-            assertEquals(Offset(15f, 18f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(15f, 18f), layoutCoordinates.positionInRoot())
         }
     }
 
@@ -284,10 +285,66 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             assertEquals(Rect(10f, 10f, 20f, 20f), bounds)
             // Positions aren't clipped
-            assertEquals(Offset(5f, 10f), layoutCoordinates.positionInRoot)
+            assertEquals(Offset(5f, 10f), layoutCoordinates.positionInRoot())
+        }
+    }
+
+    @Test
+    fun testSiblingComparisons() {
+        var coords1: LayoutCoordinates? = null
+        var coords2: LayoutCoordinates? = null
+        rule.setContent {
+            with(AmbientDensity.current) {
+                Box(
+                    Modifier.size(25.toDp())
+                        .graphicsLayer(
+                            rotationZ = 30f,
+                            clip = true
+                        )
+                ) {
+                    Box(
+                        Modifier.graphicsLayer(
+                            rotationZ = 90f,
+                            transformOrigin = TransformOrigin(0f, 1f),
+                            clip = true
+                        )
+                            .size(20.toDp(), 10.toDp())
+                            .onGloballyPositioned {
+                                coords1 = it
+                            }
+                            .align(AbsoluteAlignment.TopLeft)
+                    )
+                    Box(
+                        Modifier
+                            .graphicsLayer(
+                                rotationZ = -90f,
+                                transformOrigin = TransformOrigin(0f, 1f),
+                                clip = true
+                            )
+                            .size(10.toDp())
+                            .onGloballyPositioned {
+                                coords2 = it
+                            }
+                            .align(AbsoluteAlignment.BottomRight)
+                    )
+                }
+            }
+        }
+
+        rule.onRoot().apply {
+            assertEquals(Offset(15f, 5f), coords2!!.localPositionOf(coords1!!, Offset.Zero))
+            assertEquals(Offset(-5f, 5f), coords2!!.localPositionOf(coords1!!, Offset(20f, 0f)))
+            assertEquals(
+                Rect(-5f, -5f, 15f, 5f),
+                coords2!!.localBoundingBoxOf(coords1!!, false)
+            )
+            assertEquals(
+                Rect(0f, 0f, 10f, 5f),
+                coords2!!.localBoundingBoxOf(coords1!!, true)
+            )
         }
     }
 
@@ -364,7 +421,7 @@
 
         rule.onRoot().apply {
             val layoutCoordinates = coords!!
-            val bounds = layoutCoordinates.boundsInRoot
+            val bounds = layoutCoordinates.boundsInRoot()
             // should be completely clipped out
             assertEquals(0f, bounds.width)
             assertEquals(0f, bounds.height)
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
index d0f63f77..9f90f4b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/CustomFocusTraversalTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.key.Key.Companion.Tab
 import androidx.compose.ui.input.key.Key.Companion.DPadUp
@@ -40,6 +41,7 @@
 import org.junit.runner.RunWith
 import com.google.common.truth.Truth.assertThat
 
+@ExperimentalComposeUiApi
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class CustomFocusTraversalTest {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
index e3b305c..4f5cb1b 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusChangedTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.focus
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.ActiveParent
@@ -60,6 +61,7 @@
         }
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun activeParent_requestFocus() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
index 05c83b8..f88d22f 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/FocusRequesterTest.kt
@@ -19,6 +19,7 @@
 import android.view.View
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
@@ -250,6 +251,7 @@
         }
     }
 
+    @ExperimentalComposeUiApi
     @Test
     fun requestFocusForAnyChild_triggersOnFocusChangedInParent() {
         // Arrange.
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
index 408da38..e769f7a 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/focus/OwnerFocusTest.kt
@@ -23,7 +23,10 @@
 import androidx.compose.ui.focus.FocusState.Active
 import androidx.compose.ui.focus.FocusState.Inactive
 import androidx.compose.ui.platform.AmbientView
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -31,6 +34,8 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
@@ -179,6 +184,33 @@
         }
     }
 
+    @Test
+    fun clickingOnNonClickableSpaceInApp_bringsViewInFocus() {
+        // Arrange.
+        val nonClickable = "notClickable"
+        val viewFocused = CountDownLatch(1)
+        lateinit var ownerView: View
+        rule.setFocusableContent {
+            ownerView = getOwner()
+            Box(Modifier.testTag(nonClickable))
+        }
+        rule.runOnIdle { assertThat(ownerView.isFocused).isFalse() }
+        ownerView.setOnFocusChangeListener { _, hasFocus ->
+            if (hasFocus) {
+                viewFocused.countDown()
+            }
+        }
+
+        // Act.
+        rule.onNodeWithTag(nonClickable).performClick()
+
+        // Assert.
+        rule.runOnIdle {
+            viewFocused.await(1L, TimeUnit.SECONDS)
+            assertThat(ownerView.isFocused).isTrue()
+        }
+    }
+
     @Composable
     private fun getOwner() = AmbientView.current
 }
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 e2fb631..0ee85ee 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
@@ -3939,15 +3939,30 @@
         TODO("not implemented")
     }
 
+    override fun windowToLocal(relativeToWindow: Offset): Offset {
+        TODO("Not yet implemented")
+    }
+
     override fun localToGlobal(local: Offset): Offset {
         assertThat(isAttached).isTrue()
         return local + additionalOffset
     }
 
-    override fun localToRoot(local: Offset): Offset {
+    override fun localToWindow(relativeToLocal: Offset): Offset {
+        TODO("Not yet implemented")
+    }
+
+    override fun localToRoot(relativeToLocal: Offset): Offset {
         TODO("not implemented")
     }
 
+    override fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset
+    ): Offset {
+        TODO("Not yet implemented")
+    }
+
     override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset {
         TODO("not implemented")
     }
@@ -3956,6 +3971,13 @@
         TODO("not implemented")
     }
 
+    override fun localBoundingBoxOf(
+        sourceCoordinates: LayoutCoordinates,
+        clipBounds: Boolean
+    ): Rect {
+        TODO("Not yet implemented")
+    }
+
     override fun get(line: AlignmentLine): Int {
         TODO("not implemented")
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
index 93b3be6..ecb6f682 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessorTest.kt
@@ -37,6 +37,7 @@
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
@@ -48,7 +49,6 @@
 import androidx.compose.ui.unit.Uptime
 import androidx.compose.ui.unit.milliseconds
 import androidx.compose.ui.unit.minus
-import androidx.compose.ui.platform.WindowManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
@@ -2972,6 +2972,8 @@
     private val targetRoot: LayoutNode
 ) : Owner {
     override fun calculatePosition(): IntOffset = position
+    override fun calculatePositionInWindow(): IntOffset = position
+
     override fun requestFocus(): Boolean = false
     override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
     override val root: LayoutNode
@@ -2994,7 +2996,7 @@
         get() = TODO("Not yet implemented")
     override val focusManager: FocusManager
         get() = TODO("Not yet implemented")
-    override val windowManager: WindowManager
+    override val windowInfo: WindowInfo
         get() = TODO("Not yet implemented")
     override val fontLoader: Font.ResourceLoader
         get() = TODO("Not yet implemented")
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
index 03e43b3..06113a2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/PointerInteropFilterTest.kt
@@ -4370,15 +4370,25 @@
             get() = true
 
         override fun globalToLocal(global: Offset): Offset = Offset.Zero
+        override fun windowToLocal(relativeToWindow: Offset): Offset = Offset.Zero
 
         override fun localToGlobal(local: Offset): Offset = Offset.Zero
+        override fun localToWindow(relativeToLocal: Offset): Offset = Offset.Zero
 
-        override fun localToRoot(local: Offset): Offset = Offset.Zero
+        override fun localToRoot(relativeToLocal: Offset): Offset = Offset.Zero
+        override fun localPositionOf(
+            sourceCoordinates: LayoutCoordinates,
+            relativeToSource: Offset
+        ): Offset = Offset.Zero
 
         override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset =
             Offset.Zero
 
         override fun childBoundingBox(child: LayoutCoordinates): Rect = Rect.Zero
+        override fun localBoundingBoxOf(
+            sourceCoordinates: LayoutCoordinates,
+            clipBounds: Boolean
+        ): Rect = Rect.Zero
 
         override fun get(line: AlignmentLine): Int = 0
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
index 38d987a..6ea8fb6 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/OnGloballyPositionedTest.kt
@@ -90,7 +90,7 @@
                                 minWidth = size,
                                 minHeight = size,
                                 modifier = Modifier.onGloballyPositioned { coordinates ->
-                                    wrap1Position = coordinates.globalPosition.x
+                                    wrap1Position = coordinates.positionInWindow().x
                                     latch.countDown()
                                 }
                             )
@@ -99,7 +99,7 @@
                                 minWidth = size,
                                 minHeight = size,
                                 modifier = Modifier.onGloballyPositioned { coordinates ->
-                                    wrap2Position = coordinates.globalPosition.x
+                                    wrap2Position = coordinates.positionInWindow().x
                                     latch.countDown()
                                 }
                             )
@@ -173,7 +173,7 @@
                                 minWidth = 10,
                                 minHeight = 10,
                                 modifier = Modifier.onGloballyPositioned { coordinates ->
-                                    childGlobalPosition = coordinates.positionInRoot
+                                    childGlobalPosition = coordinates.positionInRoot()
                                     latch.countDown()
                                 }
                             )
@@ -347,7 +347,7 @@
         rule.runOnUiThread {
             view?.getLocationOnScreen(position)
         }
-        assertEquals(position[1].toFloat(), coordinates!!.globalPosition.y)
+        assertEquals(position[1].toFloat(), coordinates!!.positionInWindow().y)
     }
 
     @Test
@@ -377,7 +377,7 @@
             }
         }
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
-        val startY = coordinates!!.globalPosition.y
+        val startY = coordinates!!.positionInWindow().y
         positionedLatch = CountDownLatch(1)
 
         rule.runOnUiThread {
@@ -388,7 +388,7 @@
             "OnPositioned is not called when the container moved",
             positionedLatch.await(1, TimeUnit.SECONDS)
         )
-        assertEquals(startY - 100f, coordinates!!.globalPosition.y)
+        assertEquals(startY - 100f, coordinates!!.positionInWindow().y)
     }
 
     @Test
@@ -501,10 +501,10 @@
         assertTrue(positionedLatch.await(1, TimeUnit.SECONDS))
 
         // global position
-        val gPos = childCoordinates!!.localToGlobal(Offset.Zero).x
+        val gPos = childCoordinates!!.localToWindow(Offset.Zero).x
         assertThat(gPos).isEqualTo((firstPaddingPx + secondPaddingPx + thirdPaddingPx))
         // Position in grandparent Px(value=50.0)
-        val gpPos = gpCoordinates!!.childToLocal(childCoordinates!!, Offset.Zero).x
+        val gpPos = gpCoordinates!!.localPositionOf(childCoordinates!!, Offset.Zero).x
         assertThat(gpPos).isEqualTo((secondPaddingPx + thirdPaddingPx))
         // local position
         assertThat(childCoordinates!!.positionInParent.x).isEqualTo(thirdPaddingPx)
@@ -532,8 +532,8 @@
             composeView.setContent {
                 Box(
                     Modifier.fillMaxSize().onGloballyPositioned {
-                        realGlobalPosition = it.localToGlobal(localPosition)
-                        realLocalPosition = it.globalToLocal(
+                        realGlobalPosition = it.localToWindow(localPosition)
+                        realLocalPosition = it.windowToLocal(
                             framePadding + frameGlobalPosition!!
                         )
                         positionedLatch.countDown()
@@ -616,7 +616,7 @@
                                 Box(Modifier.size(10.toDp())) {
                                     Box(
                                         Modifier.onGloballyPositioned {
-                                            realLeft = it.positionInRoot.x
+                                            realLeft = it.positionInRoot().x
                                             positionedLatch.countDown()
                                         }.size(10.toDp())
                                     )
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
index 0beec16e..68740df 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RootNodeLayoutTest.kt
@@ -95,7 +95,7 @@
         assertNotNull(coordinates)
         assertEquals(
             Rect(left = 0f, top = 0f, right = 10f, bottom = 10f),
-            coordinates!!.boundsInRoot
+            coordinates!!.boundsInRoot()
         )
     }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
index 03ea485..7510c25 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/AndroidViewCompatTest.kt
@@ -57,8 +57,8 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.node.LayoutEmitHelper
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.Owner
@@ -437,14 +437,14 @@
         var inner: Offset = Offset.Zero
 
         rule.setContent {
-            Box(Modifier.onGloballyPositioned { outer = it.globalPosition }) {
+            Box(Modifier.onGloballyPositioned { outer = it.positionInWindow() }) {
                 val paddingDp = with(AmbientDensity.current) { padding.toDp() }
                 Box(Modifier.padding(paddingDp)) {
                     AndroidView(::ComposeView) {
                         it.setContent {
                             Box(
                                 Modifier.padding(paddingDp)
-                                    .onGloballyPositioned { inner = it.globalPosition }
+                                    .onGloballyPositioned { inner = it.positionInWindow() }
                             )
                         }
                     }
@@ -492,12 +492,12 @@
                 }
             }
         }
-        rule.runOnIdle { startX = coordinates.globalPosition.x.roundToInt() }
+        rule.runOnIdle { startX = coordinates.positionInWindow().x.roundToInt() }
 
         rule.runOnIdle { topView.visibility = View.GONE }
 
         rule.runOnIdle {
-            assertEquals(100, startX - coordinates.globalPosition.x.roundToInt())
+            assertEquals(100, startX - coordinates.positionInWindow().x.roundToInt())
         }
     }
 
@@ -513,17 +513,17 @@
         var inner1: Offset = Offset.Zero
         var inner2: Offset = Offset.Zero
         rule.setContent {
-            Box(Modifier.onGloballyPositioned { outer = it.globalPosition }) {
+            Box(Modifier.onGloballyPositioned { outer = it.positionInWindow() }) {
                 Box(Modifier.padding(start = paddingDp, top = paddingDp)) {
                     emitView(::LinearLayout, {}) {
                         Box(
                             Modifier.size(sizeDp).background(Color.Blue).onGloballyPositioned {
-                                inner1 = it.globalPosition
+                                inner1 = it.positionInWindow()
                             }
                         )
                         Box(
                             Modifier.size(sizeDp).background(Color.Gray).onGloballyPositioned {
-                                inner2 = it.globalPosition
+                                inner2 = it.positionInWindow()
                             }
                         )
                     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowInfoAmbientTest.kt
similarity index 72%
rename from compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
rename to compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowInfoAmbientTest.kt
index d50dc78..7cd024c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowManagerAmbientTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/platform/WindowInfoAmbientTest.kt
@@ -33,7 +33,7 @@
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
-class WindowManagerAmbientTest {
+class WindowInfoAmbientTest {
     @get:Rule
     val rule = createComposeRule()
 
@@ -41,12 +41,13 @@
     @Test
     fun windowIsFocused_onLaunch() {
         // Arrange.
-        lateinit var windowManager: WindowManager
+        lateinit var windowInfo: WindowInfo
         val windowFocusGain = CountDownLatch(1)
         rule.setContent {
             BasicText("Main Window")
-            windowManager = AmbientWindowManager.current
-            WindowFocusObserver { if (it) windowFocusGain.countDown() }
+            windowInfo = AmbientWindowInfo.current
+            @Suppress("DEPRECATION")
+            WindowFocusObserver1 { if (it) windowFocusGain.countDown() }
         }
 
         // Act.
@@ -54,26 +55,26 @@
 
         // Assert.
         windowFocusGain.await(5, SECONDS)
-        assertThat(windowManager.isWindowFocused).isTrue()
+        assertThat(windowInfo.isWindowFocused).isTrue()
     }
 
     @Test
     fun mainWindowIsNotFocused_whenPopupIsVisible() {
         // Arrange.
-        lateinit var mainWindowManager: WindowManager
-        lateinit var popupWindowManager: WindowManager
+        lateinit var mainWindowInfo: WindowInfo
+        lateinit var popupWindowInfo: WindowInfo
         val mainWindowFocusLoss = CountDownLatch(1)
         val popupFocusGain = CountDownLatch(1)
         val showPopup = mutableStateOf(false)
         rule.setContent {
             BasicText("Main Window")
-            mainWindowManager = AmbientWindowManager.current
-            WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+            mainWindowInfo = AmbientWindowInfo.current
+            WindowFocusObserver1 { if (!it) mainWindowFocusLoss.countDown() }
             if (showPopup.value) {
                 Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
                     BasicText("Popup Window")
-                    popupWindowManager = AmbientWindowManager.current
-                    WindowFocusObserver { if (it) popupFocusGain.countDown() }
+                    popupWindowInfo = AmbientWindowInfo.current
+                    WindowFocusObserver1 { if (it) popupFocusGain.countDown() }
                 }
             }
         }
@@ -85,25 +86,25 @@
         rule.waitForIdle()
         assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
         assertThat(popupFocusGain.await(5, SECONDS)).isTrue()
-        assertThat(mainWindowManager.isWindowFocused).isFalse()
-        assertThat(popupWindowManager.isWindowFocused).isTrue()
+        assertThat(mainWindowInfo.isWindowFocused).isFalse()
+        assertThat(popupWindowInfo.isWindowFocused).isTrue()
     }
 
     @Test
     fun windowIsFocused_whenPopupIsDismissed() {
         // Arrange.
-        lateinit var mainWindowManager: WindowManager
+        lateinit var mainWindowInfo: WindowInfo
         var mainWindowFocusGain = CountDownLatch(1)
         val popupFocusGain = CountDownLatch(1)
         val showPopup = mutableStateOf(false)
         rule.setContent {
             BasicText(text = "Main Window")
-            mainWindowManager = AmbientWindowManager.current
-            WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+            mainWindowInfo = AmbientWindowInfo.current
+            WindowFocusObserver1 { if (it) mainWindowFocusGain.countDown() }
             if (showPopup.value) {
                 Popup(isFocusable = true, onDismissRequest = { showPopup.value = false }) {
                     BasicText(text = "Popup Window")
-                    WindowFocusObserver { if (it) popupFocusGain.countDown() }
+                    WindowFocusObserver1 { if (it) popupFocusGain.countDown() }
                 }
             }
         }
@@ -118,26 +119,26 @@
         // Assert.
         rule.waitForIdle()
         assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
-        assertThat(mainWindowManager.isWindowFocused).isTrue()
+        assertThat(mainWindowInfo.isWindowFocused).isTrue()
     }
 
     @Test
     fun mainWindowIsNotFocused_whenDialogIsVisible() {
         // Arrange.
-        lateinit var mainWindowManager: WindowManager
-        lateinit var dialogWindowManager: WindowManager
+        lateinit var mainWindowInfo: WindowInfo
+        lateinit var dialogWindowInfo: WindowInfo
         val mainWindowFocusLoss = CountDownLatch(1)
         val dialogFocusGain = CountDownLatch(1)
         val showDialog = mutableStateOf(false)
         rule.setContent {
             BasicText("Main Window")
-            mainWindowManager = AmbientWindowManager.current
-            WindowFocusObserver { if (!it) mainWindowFocusLoss.countDown() }
+            mainWindowInfo = AmbientWindowInfo.current
+            WindowFocusObserver1 { if (!it) mainWindowFocusLoss.countDown() }
             if (showDialog.value) {
                 Dialog(onDismissRequest = { showDialog.value = false }) {
                     BasicText("Popup Window")
-                    dialogWindowManager = AmbientWindowManager.current
-                    WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+                    dialogWindowInfo = AmbientWindowInfo.current
+                    WindowFocusObserver1 { if (it) dialogFocusGain.countDown() }
                 }
             }
         }
@@ -149,25 +150,25 @@
         rule.waitForIdle()
         assertThat(mainWindowFocusLoss.await(5, SECONDS)).isTrue()
         assertThat(dialogFocusGain.await(5, SECONDS)).isTrue()
-        assertThat(mainWindowManager.isWindowFocused).isFalse()
-        assertThat(dialogWindowManager.isWindowFocused).isTrue()
+        assertThat(mainWindowInfo.isWindowFocused).isFalse()
+        assertThat(dialogWindowInfo.isWindowFocused).isTrue()
     }
 
     @Test
     fun windowIsFocused_whenDialogIsDismissed() {
         // Arrange.
-        lateinit var mainWindowManager: WindowManager
+        lateinit var mainWindowInfo: WindowInfo
         var mainWindowFocusGain = CountDownLatch(1)
         val dialogFocusGain = CountDownLatch(1)
         val showDialog = mutableStateOf(false)
         rule.setContent {
             BasicText(text = "Main Window")
-            mainWindowManager = AmbientWindowManager.current
-            WindowFocusObserver { if (it) mainWindowFocusGain.countDown() }
+            mainWindowInfo = AmbientWindowInfo.current
+            WindowFocusObserver1 { if (it) mainWindowFocusGain.countDown() }
             if (showDialog.value) {
                 Dialog(onDismissRequest = { showDialog.value = false }) {
                     BasicText(text = "Popup Window")
-                    WindowFocusObserver { if (it) dialogFocusGain.countDown() }
+                    WindowFocusObserver1 { if (it) dialogFocusGain.countDown() }
                 }
             }
         }
@@ -182,6 +183,6 @@
         // Assert.
         rule.waitForIdle()
         assertThat(mainWindowFocusGain.await(5, SECONDS)).isTrue()
-        assertThat(mainWindowManager.isWindowFocused).isTrue()
+        assertThat(mainWindowInfo.isWindowFocused).isTrue()
     }
 }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
index 5169d5f..2a8ddd0 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/ComposeViewTest.kt
@@ -27,7 +27,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.globalBounds
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.AbstractComposeView
 import androidx.compose.ui.platform.ComposeView
@@ -229,7 +229,7 @@
                     Modifier.testTag("box").fillMaxSize().onGloballyPositioned {
                         val position = IntArray(2)
                         composeView.getLocationOnScreen(position)
-                        globalBounds = it.globalBounds.translate(
+                        globalBounds = it.boundsInWindow().translate(
                             -position[0].toFloat(), -position[1].toFloat()
                         )
                         latch.countDown()
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
index 7f7caf0..7ab8fb2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/window/PopupDismissTest.kt
@@ -78,6 +78,7 @@
                     onClick = { btnClicksCounter++ },
                     modifier = Modifier.onGloballyPositioned {
                         // UiDevice needs screen relative coordinates
+                        @Suppress("DEPRECATION")
                         btnPos = it.localToGlobal(Offset.Zero)
                     }
                 )
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
index 45698e4..720d3896 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/ViewInterop.kt
@@ -169,7 +169,7 @@
 }
 
 private fun View.layoutAccordingTo(layoutNode: LayoutNode) {
-    val position = layoutNode.coordinates.positionInRoot
+    val position = layoutNode.coordinates.positionInRoot()
     val x = position.x.roundToInt()
     val y = position.y.roundToInt()
     layout(x, y, x + measuredWidth, y + measuredHeight)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
index ef157b9..6313be9 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.kt
@@ -114,9 +114,9 @@
     override val focusManager: FocusManager
         get() = _focusManager
 
-    private val _windowManager: WindowManagerImpl = WindowManagerImpl()
-    override val windowManager: WindowManager
-        get() = _windowManager
+    private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
+    override val windowInfo: WindowInfo
+        get() = _windowInfo
 
     private val keyInputModifier = KeyInputModifier(null, null)
 
@@ -297,7 +297,7 @@
     }
 
     override fun onWindowFocusChanged(hasWindowFocus: Boolean) {
-        _windowManager.isWindowFocused = hasWindowFocus
+        _windowInfo.isWindowFocused = hasWindowFocus
         super.onWindowFocusChanged(hasWindowFocus)
     }
 
@@ -679,6 +679,11 @@
 
     override fun calculatePosition(): IntOffset = globalPosition
 
+    override fun calculatePositionInWindow(): IntOffset {
+        getLocationOnScreen(tmpPositionArray)
+        return IntOffset(tmpPositionArray[0], tmpPositionArray[1])
+    }
+
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
         density = Density(context)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
index d15f5ae..0616186 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidPopup.kt
@@ -143,6 +143,7 @@
         modifier = Modifier.onGloballyPositioned { childCoordinates ->
             val coordinates = childCoordinates.parentCoordinates!!
             // Get the global position of the parent
+            @Suppress("DEPRECATION")
             val layoutPosition = coordinates.localToGlobal(Offset.Zero).round()
             val layoutSize = coordinates.size
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
index a3f1672..3e2826d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt
@@ -42,9 +42,26 @@
 internal class FocusManagerImpl(
     private val focusModifier: FocusModifier = FocusModifier(Inactive)
 ) : FocusManager {
+
+    /**
+     * This gesture is fired when the user clicks on a non-clickable / non-focusable part of the
+     * screen. Since no other gesture handled this click, we handle it here.
+     */
     private val passThroughClickModifier = PointerInputModifierImpl(
         TapGestureFilter().apply {
-            onTap = { clearFocus() }
+            onTap = {
+                if (focusModifier.focusState == Inactive) {
+
+                    // This view does not have focus, and the user clicked on some non-clickable
+                    // part of the screen. This is an indication that the user wants to bring
+                    // this view (this app) in focus.
+                    focusModifier.focusNode.requestFocus(propagateFocus = false)
+                } else {
+                    // The user clicked on a non-clickable part of the screen when something was
+                    // focused. This is an indication that the user wants to clear focus.
+                    clearFocus()
+                }
+            }
             consumeChanges = false
         }
     )
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
index 55e1cda..0dded38 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
+import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.node.ModifiedFocusRequesterNode
 
 private const val focusRequesterNotInitialized = "FocusRequester is not initialized. One reason " +
@@ -110,6 +111,7 @@
         /**
          * Convenient way to create multiple [FocusRequester] instances.
          */
+        @ExperimentalComposeUiApi
         object FocusRequesterFactory {
             operator fun component1() = FocusRequester()
             operator fun component2() = FocusRequester()
@@ -133,6 +135,7 @@
          * Convenient way to create multiple [FocusRequester]s, which can to be used to request
          * focus, or to specify a focus traversal order.
          */
+        @ExperimentalComposeUiApi
         fun createRefs() = FocusRequesterFactory
     }
 }
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 6a03a86..626f9d8 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
@@ -25,6 +25,7 @@
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
 import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.unit.Constraints
 
 /**
@@ -132,6 +133,14 @@
         )
     )
 
+/**
+ * A [Modifier.Element] that adds a draw layer such that tooling can identify an element
+ * in the drawn image.
+ */
+@Stable
+fun Modifier.toolingGraphicsLayer() =
+    if (isDebugInspectorInfoEnabled) this.then(Modifier.graphicsLayer()) else this
+
 private class BlockGraphicsLayerModifier(
     private val layerBlock: GraphicsLayerScope.() -> Unit,
     inspectorInfo: InspectorInfo.() -> Unit
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
index 09cb19b..018e32e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerEvent.kt
@@ -102,6 +102,7 @@
      */
     val size: IntSize
         get() = layoutCoordinates?.size ?: IntSize.Zero
+    @Suppress("DEPRECATION")
     internal val position: IntOffset
         get() = layoutCoordinates?.run { localToGlobal(Offset.Zero).round() } ?: IntOffset.Zero
     internal val isAttached: Boolean
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 37087f1..2f5bae4 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.node.LayoutNodeWrapper
 import androidx.compose.ui.unit.IntSize
 
 /**
@@ -48,30 +49,76 @@
     /**
      * Converts a global position into a local position within this layout.
      */
+    @Deprecated(
+        "Use windowToLocal instead",
+        replaceWith = ReplaceWith("windowToLocal(global)")
+    )
     fun globalToLocal(global: Offset): Offset
 
     /**
+     * Converts [relativeToWindow] relative to the window's origin into an [Offset] relative to
+     * this layout.
+     */
+    fun windowToLocal(relativeToWindow: Offset): Offset
+
+    /**
      * Converts a local position within this layout into a global one.
      */
+    @Deprecated("Use localToWindow instead", ReplaceWith("localToWindow(local)"))
     fun localToGlobal(local: Offset): Offset
 
     /**
+     * Converts [relativeToLocal] position within this layout into an [Offset] relative to the
+     * window's origin.
+     */
+    fun localToWindow(relativeToLocal: Offset): Offset
+
+    /**
      * Converts a local position within this layout into an offset from the root composable.
      */
-    fun localToRoot(local: Offset): Offset
+    fun localToRoot(relativeToLocal: Offset): Offset
+
+    /**
+     * Converts an [relativeToSource] in [sourceCoordinates] space into local coordinates.
+     * [sourceCoordinates] may be any [LayoutCoordinates] that belong to the same
+     * compose layout hierarchy.
+     */
+    fun localPositionOf(sourceCoordinates: LayoutCoordinates, relativeToSource: Offset): Offset
 
     /**
      * Converts a child layout position into a local position within this layout.
      */
+    @Deprecated("Use localPositionOf instead", ReplaceWith("localPositionOf(child, childLocal)"))
     fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset
 
     /**
-     * Returns the child bounding box, discarding clipped rectangles, in local coordinates.
+     * Returns the child bounding box, in local coordinates. A child that is rotated or scaled
+     * will have the bounding box of rotated or scaled content in local coordinates. If a child
+     * is clipped, the clipped rectangle will be returned.
      */
+    @Deprecated(
+        message = "Use localBoundingBoxOf instead",
+        replaceWith = ReplaceWith("localBoundingBoxOf(child)")
+    )
     fun childBoundingBox(child: LayoutCoordinates): Rect
 
     /**
-     * Returns the position of an [alignment line][AlignmentLine],
+     * Returns the bounding box of [sourceCoordinates] in the local coordinates.
+     * If [clipBounds] is `true`, any clipping that occurs between [sourceCoordinates] and
+     * this layout will affect the returned bounds, and can even result in an empty rectangle
+     * if clipped regions do not overlap. If [clipBounds] is false, the bounding box of
+     * [sourceCoordinates] will be converted to local coordinates irrespective of any clipping
+     * applied between the layouts.
+     *
+     * When rotation or scaling is applied, the bounding box of the rotated or scaled value
+     * will be computed in the local coordinates. For example, if a 40 pixels x 20 pixel layout
+     * is rotated 90 degrees, the bounding box will be 20 pixels x 40 pixels in its parent's
+     * coordinates.
+     */
+    fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect
+
+    /**
+     * Returns the position in pixels of an [alignment line][AlignmentLine],
      * or [AlignmentLine.Unspecified] if the line is not provided.
      */
     operator fun get(line: AlignmentLine): Int
@@ -80,27 +127,53 @@
 /**
  * The global position of this layout.
  */
+@Suppress("DEPRECATION")
+@Deprecated("Use positionInWindow() instead", ReplaceWith("positionInWindow()"))
 inline val LayoutCoordinates.globalPosition: Offset get() = localToGlobal(Offset.Zero)
 
 /**
  * The position of this layout inside the root composable.
  */
+fun LayoutCoordinates.positionInRoot(): Offset = localToRoot(Offset.Zero)
+
+@Deprecated("Use positionInRoot() instead", ReplaceWith("positionInRoot()"))
 inline val LayoutCoordinates.positionInRoot: Offset get() = localToRoot(Offset.Zero)
 
 /**
+ * The position of this layout relative to the window.
+ */
+fun LayoutCoordinates.positionInWindow(): Offset = localToWindow(Offset.Zero)
+
+/**
  * The boundaries of this layout inside the root composable.
  */
-val LayoutCoordinates.boundsInRoot: Rect
-    get() {
-        return findRoot(this).childBoundingBox(this)
-    }
+fun LayoutCoordinates.boundsInRoot(): Rect =
+    findRoot().localBoundingBoxOf(this)
+
+@Deprecated("Use boundsInRoot()", ReplaceWith("boundsInRoot()"))
+val LayoutCoordinates.boundsInRoot: Rect get() = boundsInRoot()
+
+/**
+ * The boundaries of this layout relative to the window's origin.
+ */
+fun LayoutCoordinates.boundsInWindow(): Rect {
+    val root = findRoot()
+    val bounds = boundsInRoot()
+    val windowPosition = root.positionInWindow()
+    return Rect(
+        left = bounds.left + windowPosition.x,
+        top = bounds.top + windowPosition.y,
+        right = bounds.right + windowPosition.x,
+        bottom = bounds.bottom + windowPosition.y
+    )
+}
 
 /**
  * Returns the position of the top-left in the parent's content area or (0, 0)
  * for the root.
  */
 val LayoutCoordinates.positionInParent: Offset
-    get() = parentCoordinates?.childToLocal(this, Offset.Zero) ?: Offset.Zero
+    get() = parentCoordinates?.localPositionOf(this, Offset.Zero) ?: Offset.Zero
 
 /**
  * Returns the bounding box of the child in the parent's content area, including any clipping
@@ -108,17 +181,19 @@
  * to the size of the root.
  */
 val LayoutCoordinates.boundsInParent: Rect
-    get() = parentCoordinates?.childBoundingBox(this)
+    get() = parentCoordinates?.localBoundingBoxOf(this)
         ?: Rect(0f, 0f, size.width.toFloat(), size.height.toFloat())
 
 /**
  * The global boundaries of this layout inside.
  */
+@Deprecated("Use boundsInWindow instead", ReplaceWith("boundsInWindow"))
 val LayoutCoordinates.globalBounds: Rect
     get() {
-        val root = findRoot(this)
+        val root = findRoot()
+        @Suppress("DEPRECATION")
         val rootPosition = root.localToGlobal(Offset.Zero)
-        val bounds = root.childBoundingBox(this)
+        val bounds = root.localBoundingBoxOf(this)
         return Rect(
             left = bounds.left + rootPosition.x,
             top = bounds.top + rootPosition.y,
@@ -127,12 +202,22 @@
         )
     }
 
-private fun findRoot(layoutCoordinates: LayoutCoordinates): LayoutCoordinates {
-    var root = layoutCoordinates
+/**
+ * Returns the [LayoutCoordinates] of the root layout element in the hierarchy. This will have
+ * the size of the entire compose UI.
+ */
+internal fun LayoutCoordinates.findRoot(): LayoutCoordinates {
+    var root = this
     var parent = root.parentCoordinates
     while (parent != null) {
         root = parent
         parent = root.parentCoordinates
     }
-    return root
-}
\ No newline at end of file
+    var rootLayoutNodeWrapper = root as? LayoutNodeWrapper ?: return root
+    var parentLayoutNodeWrapper = rootLayoutNodeWrapper.wrappedBy
+    while (parentLayoutNodeWrapper != null) {
+        rootLayoutNodeWrapper = parentLayoutNodeWrapper
+        parentLayoutNodeWrapper = parentLayoutNodeWrapper.wrappedBy
+    }
+    return rootLayoutNodeWrapper
+}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 449bd47..287f969 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -36,7 +36,8 @@
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.layout.Placeable
-import androidx.compose.ui.layout.globalPosition
+import androidx.compose.ui.layout.findRoot
+import androidx.compose.ui.layout.positionInWindow
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
@@ -131,6 +132,7 @@
     fun isGlobalPointerInBounds(globalPointerPosition: Offset): Boolean {
         // TODO(shepshapard): Right now globalToLocal has to traverse the tree all the way back up
         //  so calling this is expensive.  Would be nice to cache data such that this is cheap.
+        @Suppress("DEPRECATION")
         val localPointerPosition = globalToLocal(globalPointerPosition)
         return localPointerPosition.x >= 0 &&
             localPointerPosition.x < measuredSize.width &&
@@ -322,20 +324,105 @@
 
     override fun globalToLocal(global: Offset): Offset {
         check(isAttached) { ExpectAttachedLayoutCoordinates }
-        val wrapper = wrappedBy ?: return fromParentPosition(
-            global - layoutNode.requireOwner().calculatePosition().toOffset()
-        )
-        return fromParentPosition(wrapper.globalToLocal(global))
+        val positionOnScreen = layoutNode.requireOwner().calculatePosition()
+        val root = findRoot()
+        val rootPosition = (root as LayoutNodeWrapper).position.toOffset()
+        return localPositionOf(root, global - positionOnScreen - rootPosition)
+    }
+
+    override fun windowToLocal(relativeToWindow: Offset): Offset {
+        check(isAttached) { ExpectAttachedLayoutCoordinates }
+        val root = findRoot()
+        val rootPosition = (root as LayoutNodeWrapper).position.toOffset()
+        val positionInRoot =
+            relativeToWindow - layoutNode.requireOwner().calculatePositionInWindow().toOffset() -
+                rootPosition
+        return localPositionOf(root, positionInRoot)
     }
 
     override fun localToGlobal(local: Offset): Offset {
         return localToRoot(local) + layoutNode.requireOwner().calculatePosition()
     }
 
-    override fun localToRoot(local: Offset): Offset {
+    override fun localToWindow(relativeToLocal: Offset): Offset {
+        return localToRoot(relativeToLocal) + layoutNode.requireOwner().calculatePositionInWindow()
+    }
+
+    override fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset
+    ): Offset {
+        val layoutNodeWrapper = sourceCoordinates as LayoutNodeWrapper
+        val commonAncestor = findCommonAncestor(sourceCoordinates)
+
+        var position = relativeToSource
+        var wrapper = layoutNodeWrapper
+        while (wrapper !== commonAncestor) {
+            position = wrapper.toParentPosition(position)
+            wrapper = wrapper.wrappedBy!!
+        }
+
+        return ancestorToLocal(commonAncestor, position)
+    }
+
+    override fun localBoundingBoxOf(
+        sourceCoordinates: LayoutCoordinates,
+        clipBounds: Boolean
+    ): Rect {
+        check(isAttached) { ExpectAttachedLayoutCoordinates }
+        check(sourceCoordinates.isAttached) {
+            "LayoutCoordinates $sourceCoordinates is not attached!"
+        }
+        val layoutNodeWrapper = sourceCoordinates as LayoutNodeWrapper
+        val commonAncestor = findCommonAncestor(sourceCoordinates)
+
+        val bounds = rectCache
+        bounds.left = 0f
+        bounds.top = 0f
+        bounds.right = sourceCoordinates.size.width.toFloat()
+        bounds.bottom = sourceCoordinates.size.height.toFloat()
+
+        var wrapper = layoutNodeWrapper
+        while (wrapper !== commonAncestor) {
+            wrapper.rectInParent(bounds, clipBounds)
+            if (bounds.isEmpty) {
+                return Rect.Zero
+            }
+
+            wrapper = wrapper.wrappedBy!!
+        }
+
+        ancestorToLocal(commonAncestor, bounds, clipBounds)
+        return bounds.toRect()
+    }
+
+    private fun ancestorToLocal(ancestor: LayoutNodeWrapper, offset: Offset): Offset {
+        if (ancestor === this) {
+            return offset
+        }
+        val wrappedBy = wrappedBy
+        if (wrappedBy == null || ancestor == wrappedBy) {
+            return fromParentPosition(offset)
+        }
+        return fromParentPosition(wrappedBy.ancestorToLocal(ancestor, offset))
+    }
+
+    private fun ancestorToLocal(
+        ancestor: LayoutNodeWrapper,
+        rect: MutableRect,
+        clipBounds: Boolean
+    ) {
+        if (ancestor === this) {
+            return
+        }
+        wrappedBy?.ancestorToLocal(ancestor, rect, clipBounds)
+        return fromParentRect(rect, clipBounds)
+    }
+
+    override fun localToRoot(relativeToLocal: Offset): Offset {
         check(isAttached) { ExpectAttachedLayoutCoordinates }
         var wrapper: LayoutNodeWrapper? = this
-        var position = local
+        var position = relativeToLocal
         while (wrapper != null) {
             position = wrapper.toParentPosition(position)
             wrapper = wrapper.wrappedBy
@@ -343,6 +430,14 @@
         return position
     }
 
+    protected inline fun withPositionTranslation(canvas: Canvas, block: (Canvas) -> Unit) {
+        val x = position.x.toFloat()
+        val y = position.y.toFloat()
+        canvas.translate(x, y)
+        block(canvas)
+        canvas.translate(-x, -y)
+    }
+
     /**
      * Converts [position] in the local coordinate system to a [Offset] in the
      * [parentCoordinates] coordinate system.
@@ -353,6 +448,7 @@
             position
         } else {
             val matrix = matrixCache
+            layer.getMatrix(matrix)
             matrix.map(position)
         }
         return targetPosition + this.position
@@ -422,12 +518,12 @@
 
     /**
      * Modifies bounds to be in the parent LayoutNodeWrapper's coordinates, including clipping,
-     * scaling, etc.
+     * if [clipBounds] is true.
      */
-    protected open fun rectInParent(bounds: MutableRect) {
+    private fun rectInParent(bounds: MutableRect, clipBounds: Boolean) {
         val layer = layer
         if (layer != null) {
-            if (isClipping) {
+            if (isClipping && clipBounds) {
                 bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
                 if (bounds.isEmpty) {
                     return
@@ -447,6 +543,34 @@
         bounds.bottom += y
     }
 
+    /**
+     * Modifies bounds in the parent's coordinates to be in this LayoutNodeWrapper's
+     * coordinates, including clipping, if [clipBounds] is true.
+     */
+    private fun fromParentRect(bounds: MutableRect, clipBounds: Boolean) {
+        val x = position.x
+        bounds.left -= x
+        bounds.right -= x
+
+        val y = position.y
+        bounds.top -= y
+        bounds.bottom -= y
+
+        val layer = layer
+        if (layer != null) {
+            val matrix = matrixCache
+            layer.getMatrix(matrix)
+            matrix.invert()
+            matrix.map(bounds)
+            if (isClipping && clipBounds) {
+                bounds.intersect(0f, 0f, size.width.toFloat(), size.height.toFloat())
+                if (bounds.isEmpty) {
+                    return
+                }
+            }
+        }
+    }
+
     override fun childBoundingBox(child: LayoutCoordinates): Rect {
         check(isAttached) { ExpectAttachedLayoutCoordinates }
         check(child.isAttached) { "Child $child is not attached!" }
@@ -457,7 +581,7 @@
         bounds.bottom = child.size.height.toFloat()
         var wrapper = child as LayoutNodeWrapper
         while (wrapper !== this) {
-            wrapper.rectInParent(bounds)
+            wrapper.rectInParent(bounds, clipBounds = true)
             if (bounds.isEmpty) {
                 return Rect.Zero
             }
@@ -473,8 +597,8 @@
 
     protected fun withinLayerBounds(pointerPositionRelativeToScreen: Offset): Boolean {
         if (layer != null && isClipping) {
-            val l = globalPosition.x
-            val t = globalPosition.y
+            val l = positionInWindow().x
+            val t = positionInWindow().y
             val r = l + width
             val b = t + height
 
@@ -644,6 +768,47 @@
         layer?.invalidate()
     }
 
+    internal fun findCommonAncestor(other: LayoutNodeWrapper): LayoutNodeWrapper {
+        var ancestor1 = other.layoutNode
+        var ancestor2 = layoutNode
+        if (ancestor1 === ancestor2) {
+            // They are on the same node, but we don't know which is the deeper of the two
+            val tooFar = layoutNode.outerLayoutNodeWrapper
+            var tryMe = this
+            while (tryMe !== tooFar && tryMe !== other) {
+                tryMe = tryMe.wrappedBy!!
+            }
+            if (tryMe === other) {
+                return other
+            }
+            return this
+        }
+
+        while (ancestor1.depth > ancestor2.depth) {
+            ancestor1 = ancestor1.parent!!
+        }
+
+        while (ancestor2.depth > ancestor1.depth) {
+            ancestor2 = ancestor2.parent!!
+        }
+
+        while (ancestor1 !== ancestor2) {
+            val parent1 = ancestor1.parent
+            val parent2 = ancestor2.parent
+            if (parent1 == null || parent2 == null) {
+                throw IllegalArgumentException("layouts are not part of the same hierarchy")
+            }
+            ancestor1 = parent1
+            ancestor2 = parent2
+        }
+
+        return when {
+            ancestor2 === layoutNode -> this
+            ancestor1 === other.layoutNode -> other
+            else -> ancestor1.innerLayoutNodeWrapper
+        }
+    }
+
     internal companion object {
         const val ExpectAttachedLayoutCoordinates = "LayoutCoordinate operations are only valid " +
             "when isAttached is true"
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
index c775aa0..531b356 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifiedFocusNode.kt
@@ -314,7 +314,7 @@
     // TODO(b/175900268): Add API to allow a parent to extends the bounds of the focus Modifier.
     //  For now we just use the bounds of this node.
     internal val focusRect: Rect
-        get() = boundsInRoot
+        get() = boundsInRoot()
 
     // TODO(b/152051577): Measure the performance of focusableChildren.
     //  Consider caching the children.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
index 76fd874..9ec0253 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/Owner.kt
@@ -25,13 +25,13 @@
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.platform.WindowManager
 
 /**
  * Owner implements the connection to the underlying view system. On Android, this connects
@@ -91,7 +91,17 @@
     /**
      * Provide information about the window that hosts this [Owner].
      */
-    val windowManager: WindowManager
+    @Deprecated(
+        message = "Renamed to windowInfo",
+        replaceWith = ReplaceWith("windowInfo"),
+        level = DeprecationLevel.ERROR
+    )
+    val windowManager get() = windowInfo
+
+    /**
+     * Provide information about the window that hosts this [Owner].
+     */
+    val windowInfo: WindowInfo
 
     val fontLoader: Font.ResourceLoader
 
@@ -136,6 +146,11 @@
     fun calculatePosition(): IntOffset
 
     /**
+     * Returns the most position of the owner relative to the window.
+     */
+    fun calculatePositionInWindow(): IntOffset
+
+    /**
      * Ask the system to provide focus to this owner.
      *
      * @return true if the system granted focus to this owner. False otherwise.
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
index 5b060f3d..735945d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/Ambients.kt
@@ -260,10 +260,24 @@
  */
 val AmbientViewConfiguration = staticAmbientOf<ViewConfiguration>()
 
+// TODO(b/177085155): Remove after Alpha 11.
 /**
  * The ambient that provides information about the window that hosts the current [Owner].
  */
-val AmbientWindowManager = staticAmbientOf<WindowManager>()
+@Deprecated(
+    message = "Renamed to AmbientWindowInfo",
+    replaceWith = ReplaceWith(
+        "AmbientWindowInfo",
+        "androidx.compose.ui.platform.AmbientWindowInfo"
+    ),
+    level = DeprecationLevel.ERROR
+)
+val AmbientWindowManager get() = AmbientWindowInfo
+
+/**
+ * The ambient that provides information about the window that hosts the current [Owner].
+ */
+val AmbientWindowInfo = staticAmbientOf<WindowInfo>()
 
 @ExperimentalComposeUiApi
 @Composable
@@ -287,7 +301,7 @@
         AmbientTextToolbar provides owner.textToolbar,
         AmbientUriHandler provides uriHandler,
         AmbientViewConfiguration provides owner.viewConfiguration,
-        AmbientWindowManager provides owner.windowManager,
+        AmbientWindowInfo provides owner.windowInfo,
         content = content
     )
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
similarity index 63%
rename from compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
rename to compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
index 0c20424..6eea8f3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/WindowInfo.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 The Android Open Source Project
+ * Copyright 2021 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,14 +23,13 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshots.snapshotFlow
-import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.flow.collect
 
 /**
  * Provides information about the Window that is hosting this compose hierarchy.
  */
 @Stable
-interface WindowManager {
+interface WindowInfo {
     /**
      * Indicates whether the window hosting this compose hierarchy is in focus.
      *
@@ -41,23 +40,45 @@
     val isWindowFocused: Boolean
 }
 
+// TODO(b/177085155): Remove after Alpha 11.
+/**
+ * Provides information about the Window that is hosting this compose hierarchy.
+ */
+@Stable
+@Deprecated(
+    message = "Use WindowInfo instead.",
+    replaceWith = ReplaceWith("WindowInfo", "androidx.compose.ui.platform.WindowInfo"),
+    level = DeprecationLevel.ERROR
+)
+interface WindowManager {
+    val isWindowFocused: Boolean
+}
+
+// TODO(b/177085040):  Remove after Alpha 11.
 /**
  * Provides a callback that is called whenever the window gains or loses focus.
  */
-@OptIn(
-    ExperimentalComposeApi::class,
-    InternalCoroutinesApi::class
+@Deprecated(
+    message = "Use AmbientWindowInfo.current.isWIndowFocused instead.",
+    level = DeprecationLevel.ERROR
 )
 @Composable
 fun WindowFocusObserver(onWindowFocusChanged: (isWindowFocused: Boolean) -> Unit) {
-    val windowManager = AmbientWindowManager.current
+    WindowFocusObserver1(onWindowFocusChanged)
+}
+
+// TODO(b/177085040): Rename this to WindowFocusObserver after removing WindowFocusObserver.
+@OptIn(ExperimentalComposeApi::class)
+@Composable
+internal fun WindowFocusObserver1(onWindowFocusChanged: (isWindowFocused: Boolean) -> Unit) {
+    val windowInfo = AmbientWindowInfo.current
     val callback = rememberUpdatedState(onWindowFocusChanged)
-    LaunchedEffect(windowManager) {
-        snapshotFlow { windowManager.isWindowFocused }.collect { callback.value(it) }
+    LaunchedEffect(windowInfo) {
+        snapshotFlow { windowInfo.isWindowFocused }.collect { callback.value(it) }
     }
 }
 
-internal class WindowManagerImpl : WindowManager {
+internal class WindowInfoImpl : WindowInfo {
     private val _isWindowFocused = mutableStateOf(false)
     override var isWindowFocused: Boolean
         set(value) { _isWindowFocused.value = value }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
index c9b5105..444c464 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionManager.kt
@@ -27,7 +27,7 @@
 import androidx.compose.ui.hapticfeedback.HapticFeedback
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.layout.LayoutCoordinates
-import androidx.compose.ui.layout.globalBounds
+import androidx.compose.ui.layout.boundsInWindow
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.TextToolbarStatus
@@ -183,14 +183,14 @@
             return
         }
 
-        val startHandlePosition = containerCoordinates.childToLocal(
+        val startHandlePosition = containerCoordinates.localPositionOf(
             startLayoutCoordinates,
             selection.start.selectable.getHandlePosition(
                 selection = selection,
                 isStartHandle = true
             )
         )
-        val endHandlePosition = containerCoordinates.childToLocal(
+        val endHandlePosition = containerCoordinates.localPositionOf(
             endLayoutCoordinates,
             selection.end.selectable.getHandlePosition(
                 selection = selection,
@@ -331,14 +331,14 @@
 
         val localLayoutCoordinates = containerLayoutCoordinates
         if (localLayoutCoordinates != null && localLayoutCoordinates.isAttached) {
-            var startOffset = localLayoutCoordinates.childToLocal(
+            var startOffset = localLayoutCoordinates.localPositionOf(
                 startLayoutCoordinates,
                 selection.start.selectable.getHandlePosition(
                     selection = selection,
                     isStartHandle = true
                 )
             )
-            var endOffset = localLayoutCoordinates.childToLocal(
+            var endOffset = localLayoutCoordinates.localPositionOf(
                 endLayoutCoordinates,
                 selection.end.selectable.getHandlePosition(
                     selection = selection,
@@ -352,7 +352,7 @@
             val left = min(startOffset.x, endOffset.x)
             val right = max(startOffset.x, endOffset.x)
 
-            var startTop = localLayoutCoordinates.childToLocal(
+            var startTop = localLayoutCoordinates.localPositionOf(
                 startLayoutCoordinates,
                 Offset(
                     0f,
@@ -360,7 +360,7 @@
                 )
             )
 
-            var endTop = localLayoutCoordinates.childToLocal(
+            var endTop = localLayoutCoordinates.localPositionOf(
                 endLayoutCoordinates,
                 Offset(
                     0.0f,
@@ -427,7 +427,7 @@
 
                 // Convert the position where drag gesture begins from composable coordinates to
                 // selection container coordinates.
-                dragBeginPosition = requireContainerCoordinates().childToLocal(
+                dragBeginPosition = requireContainerCoordinates().localPositionOf(
                     beginLayoutCoordinates,
                     beginCoordinates
                 )
@@ -443,7 +443,7 @@
                 val currentStart = if (isStartHandle) {
                     dragBeginPosition + dragTotalDistance
                 } else {
-                    requireContainerCoordinates().childToLocal(
+                    requireContainerCoordinates().localPositionOf(
                         selection.start.selectable.getLayoutCoordinates()!!,
                         getAdjustedCoordinates(
                             selection.start.selectable.getHandlePosition(
@@ -455,7 +455,7 @@
                 }
 
                 val currentEnd = if (isStartHandle) {
-                    requireContainerCoordinates().childToLocal(
+                    requireContainerCoordinates().localPositionOf(
                         selection.end.selectable.getLayoutCoordinates()!!,
                         getAdjustedCoordinates(
                             selection.end.selectable.getHandlePosition(
@@ -491,7 +491,7 @@
     ): Offset? {
         val coordinates = containerLayoutCoordinates
         if (coordinates == null || !coordinates.isAttached) return null
-        return requireContainerCoordinates().childToLocal(layoutCoordinates, offset)
+        return requireContainerCoordinates().localPositionOf(layoutCoordinates, offset)
     }
 
     private fun updateSelection(
@@ -565,9 +565,10 @@
     // globalBounds is the global boundaries of this LayoutCoordinates after it's clipped by
     // parents. We can think it as the global visible bounds of this Layout. Here globalBounds
     // is convert to local, which is the boundary of the visible area within the LayoutCoordinates.
+    val boundsInWindow = boundsInWindow()
     return Rect(
-        globalToLocal(globalBounds.topLeft),
-        globalToLocal(globalBounds.bottomRight)
+        windowToLocal(boundsInWindow.topLeft),
+        windowToLocal(boundsInWindow.bottomRight)
     )
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
index 809b977..a99995d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/selection/SelectionRegistrarImpl.kt
@@ -88,13 +88,13 @@
                     val layoutCoordinatesB = b.getLayoutCoordinates()
 
                     val positionA =
-                        if (layoutCoordinatesA != null) containerLayoutCoordinates.childToLocal(
+                        if (layoutCoordinatesA != null) containerLayoutCoordinates.localPositionOf(
                             layoutCoordinatesA,
                             Offset.Zero
                         )
                         else Offset.Zero
                     val positionB =
-                        if (layoutCoordinatesB != null) containerLayoutCoordinates.childToLocal(
+                        if (layoutCoordinatesB != null) containerLayoutCoordinates.localPositionOf(
                             layoutCoordinatesB,
                             Offset.Zero
                         )
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index a59fd7d..7b5b0f3 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -21,9 +21,9 @@
 import androidx.compose.ui.layout.AlignmentLine
 import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.layout.globalBounds
-import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.layout.LayoutInfo
+import androidx.compose.ui.layout.globalPosition
 import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNodeWrapper
 import androidx.compose.ui.node.Owner
@@ -100,7 +100,7 @@
      */
     val boundsInRoot: Rect
         get() {
-            return this.layoutNode.coordinates.boundsInRoot
+            return this.layoutNode.coordinates.boundsInRoot()
         }
 
     /**
@@ -109,7 +109,7 @@
      */
     val positionInRoot: Offset
         get() {
-            return this.layoutNode.coordinates.positionInRoot
+            return this.layoutNode.coordinates.positionInRoot()
         }
 
     /**
@@ -118,6 +118,7 @@
      */
     val globalBounds: Rect
         get() {
+            @Suppress("DEPRECATION")
             return this.layoutNode.coordinates.globalBounds
         }
 
@@ -126,6 +127,7 @@
      */
     val globalPosition: Offset
         get() {
+            @Suppress("DEPRECATION")
             return this.layoutNode.coordinates.globalPosition
         }
 
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
index c25fc94..5ba1e19 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwner.kt
@@ -84,10 +84,10 @@
     override val focusManager: FocusManager
         get() = _focusManager
 
-    // TODO: set/clear _windowManager.isWindowFocused when the window gains/loses focus.
-    private val _windowManager: WindowManagerImpl = WindowManagerImpl()
-    override val windowManager: WindowManager
-        get() = _windowManager
+    // TODO: set/clear _windowInfo.isWindowFocused when the window gains/loses focus.
+    private val _windowInfo: WindowInfoImpl = WindowInfoImpl()
+    override val windowInfo: WindowInfo
+        get() = _windowInfo
 
     private val keyInputModifier = KeyInputModifier(null, null)
 
@@ -195,6 +195,8 @@
 
     override fun calculatePosition() = IntOffset.Zero
 
+    override fun calculatePositionInWindow() = IntOffset.Zero
+
     fun setSize(width: Int, height: Int) {
         val constraints = Constraints(0, width, 0, height)
         this.size = IntSize(width, height)
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
index fdfe3c3..393c831 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopSelectionRegistrar.kt
@@ -52,13 +52,13 @@
                     val layoutCoordinatesB = b.getLayoutCoordinates()
 
                     val positionA =
-                        if (layoutCoordinatesA != null) containerLayoutCoordinates.childToLocal(
+                        if (layoutCoordinatesA != null) containerLayoutCoordinates.localPositionOf(
                             layoutCoordinatesA,
                             Offset.Zero
                         )
                         else Offset.Zero
                     val positionB =
-                        if (layoutCoordinatesB != null) containerLayoutCoordinates.childToLocal(
+                        if (layoutCoordinatesB != null) containerLayoutCoordinates.localPositionOf(
                             layoutCoordinatesB,
                             Offset.Zero
                         )
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
index 5b83e1a..67dc7d2 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DesktopPopup.kt
@@ -62,7 +62,7 @@
         modifier = Modifier.onGloballyPositioned { childCoordinates ->
             val coordinates = childCoordinates.parentCoordinates!!
             parentBounds.value = IntBounds(
-                coordinates.localToGlobal(Offset.Zero).round(),
+                coordinates.localToWindow(Offset.Zero).round(),
                 coordinates.size
             )
         },
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 217044c..09dfdc4 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
@@ -43,6 +43,7 @@
 import androidx.compose.ui.platform.ClipboardManager
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
+import androidx.compose.ui.platform.WindowInfo
 import androidx.compose.ui.semantics.SemanticsOwner
 import androidx.compose.ui.text.font.Font
 import androidx.compose.ui.text.input.TextInputService
@@ -51,7 +52,6 @@
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.platform.WindowManager
 import androidx.compose.ui.zIndex
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.spy
@@ -372,7 +372,7 @@
         val expectedY = globalPosition.y - y0.toFloat() - y1.toFloat()
         val expectedPosition = Offset(expectedX, expectedY)
 
-        val result = node1.coordinates.globalToLocal(globalPosition)
+        val result = node1.coordinates.windowToLocal(globalPosition)
 
         assertEquals(expectedPosition, result)
     }
@@ -397,7 +397,7 @@
         val expectedY = globalPosition.y - y0.toFloat() - y1.toFloat()
         val expectedPosition = Offset(expectedX, expectedY)
 
-        val result = node1.coordinates.globalToLocal(globalPosition)
+        val result = node1.coordinates.windowToLocal(globalPosition)
 
         assertEquals(expectedPosition, result)
     }
@@ -422,7 +422,7 @@
         val expectedY = localPosition.y + y0.toFloat() + y1.toFloat()
         val expectedPosition = Offset(expectedX, expectedY)
 
-        val result = node1.coordinates.localToGlobal(localPosition)
+        val result = node1.coordinates.localToWindow(localPosition)
 
         assertEquals(expectedPosition, result)
     }
@@ -447,7 +447,7 @@
         val expectedY = localPosition.y + y0.toFloat() + y1.toFloat()
         val expectedPosition = Offset(expectedX, expectedY)
 
-        val result = node1.coordinates.localToGlobal(localPosition)
+        val result = node1.coordinates.localToWindow(localPosition)
 
         assertEquals(expectedPosition, result)
     }
@@ -458,7 +458,7 @@
         node.attach(MockOwner(IntOffset(20, 20)))
         node.place(100, 10)
 
-        val result = node.coordinates.localToGlobal(Offset.Zero)
+        val result = node.coordinates.localToWindow(Offset.Zero)
 
         assertEquals(Offset(120f, 30f), result)
     }
@@ -469,7 +469,7 @@
         node.attach(MockOwner(IntOffset(20, 20)))
         node.place(100, 10)
 
-        val result = node.coordinates.localToGlobal(Offset.Zero)
+        val result = node.coordinates.localToWindow(Offset.Zero)
 
         assertEquals(Offset(120f, 30f), result)
     }
@@ -492,23 +492,24 @@
         val expectedY = localPosition.y + y1.toFloat()
         val expectedPosition = Offset(expectedX, expectedY)
 
-        val result = node0.coordinates.childToLocal(node1.coordinates, localPosition)
+        val result = node0.coordinates.localPositionOf(node1.coordinates, localPosition)
 
         assertEquals(expectedPosition, result)
     }
 
     @Test
-    fun testChildToLocalFailedWhenNotAncestor() {
+    fun testLocalPositionOfWithSiblings() {
         val node0 = LayoutNode()
         node0.attach(MockOwner())
         val node1 = LayoutNode()
         val node2 = LayoutNode()
         node0.insertAt(0, node1)
-        node1.insertAt(0, node2)
+        node0.insertAt(1, node2)
+        node1.place(10, 20)
+        node2.place(100, 200)
 
-        thrown.expect(IllegalStateException::class.java)
-
-        node2.coordinates.childToLocal(node1.coordinates, Offset(5f, 15f))
+        val offset = node2.coordinates.localPositionOf(node1.coordinates, Offset(5f, 15f))
+        assertEquals(Offset(-85f, -165f), offset)
     }
 
     @Test
@@ -519,9 +520,9 @@
         val node1 = LayoutNode()
         node1.attach(owner)
 
-        thrown.expect(IllegalStateException::class.java)
+        thrown.expect(IllegalArgumentException::class.java)
 
-        node1.coordinates.childToLocal(node0.coordinates, Offset(5f, 15f))
+        node1.coordinates.localPositionOf(node0.coordinates, Offset(5f, 15f))
     }
 
     @Test
@@ -530,7 +531,7 @@
         node.attach(MockOwner())
         val position = Offset(5f, 15f)
 
-        val result = node.coordinates.childToLocal(node.coordinates, position)
+        val result = node.coordinates.localPositionOf(node.coordinates, position)
 
         assertEquals(position, result)
     }
@@ -544,7 +545,7 @@
         parent.place(-100, 10)
         child.place(50, 80)
 
-        val actual = child.coordinates.positionInRoot
+        val actual = child.coordinates.positionInRoot()
 
         assertEquals(Offset(-50f, 90f), actual)
     }
@@ -557,7 +558,7 @@
         parent.insertAt(0, child)
         child.place(50, 80)
 
-        val actual = child.coordinates.positionInRoot
+        val actual = child.coordinates.positionInRoot()
 
         assertEquals(Offset(50f, 80f), actual)
     }
@@ -571,7 +572,7 @@
         parent.place(-100, 10)
         child.place(50, 80)
 
-        val actual = parent.coordinates.childToLocal(child.coordinates, Offset.Zero)
+        val actual = parent.coordinates.localPositionOf(child.coordinates, Offset.Zero)
 
         assertEquals(Offset(50f, 80f), actual)
     }
@@ -588,7 +589,7 @@
         parent.place(23, -13)
         child.place(-3, 11)
 
-        val actual = grandParent.coordinates.childToLocal(child.coordinates, Offset.Zero)
+        val actual = grandParent.coordinates.localPositionOf(child.coordinates, Offset.Zero)
 
         assertEquals(Offset(20f, -2f), actual)
     }
@@ -1739,7 +1740,7 @@
         get() = TODO("Not yet implemented")
     override val focusManager: FocusManager
         get() = TODO("Not yet implemented")
-    override val windowManager: WindowManager
+    override val windowInfo: WindowInfo
         get() = TODO("Not yet implemented")
     override val fontLoader: Font.ResourceLoader
         get() = TODO("Not yet implemented")
@@ -1766,6 +1767,7 @@
     }
 
     override fun calculatePosition(): IntOffset = position
+    override fun calculatePositionInWindow(): IntOffset = position
 
     override fun requestFocus(): Boolean = false
 
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockCoordinates.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockCoordinates.kt
index 7d3bbfd9..5286e4b 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockCoordinates.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/MockCoordinates.kt
@@ -26,14 +26,18 @@
     override var size: IntSize = IntSize.Zero,
     var localOffset: Offset = Offset.Zero,
     var globalOffset: Offset = Offset.Zero,
+    var windowOffset: Offset = Offset.Zero,
     var rootOffset: Offset = Offset.Zero,
     var childToLocalOffset: Offset = Offset.Zero,
     override var isAttached: Boolean = true
 ) : LayoutCoordinates {
     val globalToLocalParams = mutableListOf<Offset>()
+    val windowToLocalParams = mutableListOf<Offset>()
     val localToGlobalParams = mutableListOf<Offset>()
+    val localToWindowParams = mutableListOf<Offset>()
     val localToRootParams = mutableListOf<Offset>()
     val childToLocalParams = mutableListOf<Pair<LayoutCoordinates, Offset>>()
+    val localPositionOfParams = mutableListOf<Pair<LayoutCoordinates, Offset>>()
 
     override val providedAlignmentLines: Set<AlignmentLine>
         get() = emptySet()
@@ -44,22 +48,44 @@
         return localOffset
     }
 
+    override fun windowToLocal(relativeToWindow: Offset): Offset {
+        windowToLocalParams += relativeToWindow
+        return localOffset
+    }
+
     override fun localToGlobal(local: Offset): Offset {
         localToGlobalParams += local
         return globalOffset
     }
 
-    override fun localToRoot(local: Offset): Offset {
-        localToRootParams += local
+    override fun localToWindow(relativeToLocal: Offset): Offset {
+        localToWindowParams += relativeToLocal
+        return windowOffset
+    }
+
+    override fun localToRoot(relativeToLocal: Offset): Offset {
+        localToRootParams += relativeToLocal
         return rootOffset
     }
 
+    override fun localPositionOf(
+        sourceCoordinates: LayoutCoordinates,
+        relativeToSource: Offset
+    ): Offset {
+        localPositionOfParams += sourceCoordinates to relativeToSource
+        return childToLocalOffset
+    }
+
     override fun childToLocal(child: LayoutCoordinates, childLocal: Offset): Offset {
         childToLocalParams += child to childLocal
         return childToLocalOffset
     }
 
     override fun childBoundingBox(child: LayoutCoordinates): Rect = Rect.Zero
+    override fun localBoundingBoxOf(
+        sourceCoordinates: LayoutCoordinates,
+        clipBounds: Boolean
+    ): Rect = Rect.Zero
 
     override fun get(line: AlignmentLine): Int = 0
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
index ff19014..d545ed7 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerDragTest.kt
@@ -42,12 +42,14 @@
 
     private val size = IntSize(500, 600)
     private val globalOffset = Offset(100f, 200f)
+    private val windowOffset = Offset(100f, 200f)
     private val childToLocalOffset = Offset(300f, 400f)
 
     private val containerLayoutCoordinates = spy(
         MockCoordinates(
             size = size,
             globalOffset = globalOffset,
+            windowOffset = windowOffset,
             childToLocalOffset = childToLocalOffset,
             isAttached = true
         )
@@ -118,9 +120,9 @@
         selectionManager.handleDragObserver(isStartHandle = true).onStart(Offset.Zero)
 
         verify(containerLayoutCoordinates, times(1))
-            .childToLocal(
-                child = startLayoutCoordinates,
-                childLocal = getAdjustedCoordinates(Offset.Zero)
+            .localPositionOf(
+                sourceCoordinates = startLayoutCoordinates,
+                relativeToSource = getAdjustedCoordinates(Offset.Zero)
             )
         verify(spyLambda, times(0)).invoke(fakeResultSelection)
     }
@@ -130,9 +132,9 @@
         selectionManager.handleDragObserver(isStartHandle = false).onStart(Offset.Zero)
 
         verify(containerLayoutCoordinates, times(1))
-            .childToLocal(
-                child = endLayoutCoordinates,
-                childLocal = getAdjustedCoordinates(Offset.Zero)
+            .localPositionOf(
+                sourceCoordinates = endLayoutCoordinates,
+                relativeToSource = getAdjustedCoordinates(Offset.Zero)
             )
         verify(spyLambda, times(0)).invoke(fakeResultSelection)
     }
@@ -147,9 +149,9 @@
         val result = selectionManager.handleDragObserver(isStartHandle = true).onDrag(dragDistance)
 
         verify(containerLayoutCoordinates, times(1))
-            .childToLocal(
-                child = endLayoutCoordinates,
-                childLocal = getAdjustedCoordinates(Offset.Zero)
+            .localPositionOf(
+                sourceCoordinates = endLayoutCoordinates,
+                relativeToSource = getAdjustedCoordinates(Offset.Zero)
             )
         verify(selectable, times(1))
             .getSelection(
@@ -175,9 +177,9 @@
         val result = selectionManager.handleDragObserver(isStartHandle = false).onDrag(dragDistance)
 
         verify(containerLayoutCoordinates, times(1))
-            .childToLocal(
-                child = startLayoutCoordinates,
-                childLocal = getAdjustedCoordinates(Offset.Zero)
+            .localPositionOf(
+                sourceCoordinates = startLayoutCoordinates,
+                relativeToSource = getAdjustedCoordinates(Offset.Zero)
             )
         verify(selectable, times(1))
             .getSelection(
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
index ba89a5f..40bd59e 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionManagerTest.kt
@@ -52,7 +52,7 @@
 
     private val containerLayoutCoordinates = mock<LayoutCoordinates> {
         on { isAttached } doReturn true
-        on { childToLocal(any(), Offset(any())) } doAnswer Offset.Zero
+        on { localPositionOf(any(), Offset(any())) } doAnswer Offset.Zero
     }
     private val startSelectable = mock<Selectable>()
     private val endSelectable = mock<Selectable>()
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
index 4bf9891a..22fce62 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/selection/SelectionRegistrarImplTest.kt
@@ -81,10 +81,10 @@
         val relativeCoordinates3 = Offset(5f, 24f)
 
         val containerLayoutCoordinates = mock<LayoutCoordinates> {
-            on { childToLocal(layoutCoordinates0, Offset.Zero) } doAnswer relativeCoordinates0
-            on { childToLocal(layoutCoordinates1, Offset.Zero) } doAnswer relativeCoordinates1
-            on { childToLocal(layoutCoordinates2, Offset.Zero) } doAnswer relativeCoordinates2
-            on { childToLocal(layoutCoordinates3, Offset.Zero) } doAnswer relativeCoordinates3
+            on { localPositionOf(layoutCoordinates0, Offset.Zero) } doAnswer relativeCoordinates0
+            on { localPositionOf(layoutCoordinates1, Offset.Zero) } doAnswer relativeCoordinates1
+            on { localPositionOf(layoutCoordinates2, Offset.Zero) } doAnswer relativeCoordinates2
+            on { localPositionOf(layoutCoordinates3, Offset.Zero) } doAnswer relativeCoordinates3
         }
 
         val selectionRegistrar = SelectionRegistrarImpl()
@@ -129,10 +129,10 @@
         val relativeCoordinates3 = Offset(5f, 24f)
 
         val containerLayoutCoordinates = mock<LayoutCoordinates> {
-            on { childToLocal(layoutCoordinates0, Offset.Zero) } doAnswer relativeCoordinates0
-            on { childToLocal(layoutCoordinates1, Offset.Zero) } doAnswer relativeCoordinates1
-            on { childToLocal(layoutCoordinates2, Offset.Zero) } doAnswer relativeCoordinates2
-            on { childToLocal(layoutCoordinates3, Offset.Zero) } doAnswer relativeCoordinates3
+            on { localPositionOf(layoutCoordinates0, Offset.Zero) } doAnswer relativeCoordinates0
+            on { localPositionOf(layoutCoordinates1, Offset.Zero) } doAnswer relativeCoordinates1
+            on { localPositionOf(layoutCoordinates2, Offset.Zero) } doAnswer relativeCoordinates2
+            on { localPositionOf(layoutCoordinates3, Offset.Zero) } doAnswer relativeCoordinates3
         }
 
         val selectionRegistrar = SelectionRegistrarImpl()
@@ -159,7 +159,7 @@
         val layoutCoordinates0 = mock<LayoutCoordinates>()
         whenever(handler0.getLayoutCoordinates()).thenReturn(layoutCoordinates0)
         val containerLayoutCoordinates = mock<LayoutCoordinates> {
-            on { childToLocal(layoutCoordinates0, Offset.Zero) } doAnswer Offset.Zero
+            on { localPositionOf(layoutCoordinates0, Offset.Zero) } doAnswer Offset.Zero
         }
 
         val selectionRegistrar = SelectionRegistrarImpl()
@@ -181,7 +181,7 @@
         val layoutCoordinates0 = mock<LayoutCoordinates>()
         whenever(handler0.getLayoutCoordinates()).thenReturn(layoutCoordinates0)
         val containerLayoutCoordinates = mock<LayoutCoordinates> {
-            on { childToLocal(layoutCoordinates0, Offset.Zero) } doAnswer Offset.Zero
+            on { localPositionOf(layoutCoordinates0, Offset.Zero) } doAnswer Offset.Zero
         }
 
         val selectionRegistrar = SelectionRegistrarImpl()
diff --git a/paging/common/api/current.txt b/paging/common/api/current.txt
index 3208d3e..9624ee9 100644
--- a/paging/common/api/current.txt
+++ b/paging/common/api/current.txt
@@ -303,8 +303,11 @@
   public final class PagingDataTransforms {
     method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends java.lang.Iterable<? extends R>> transform);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
   }
@@ -475,6 +478,11 @@
   public final class SeparatorsKt {
   }
 
+  public enum TerminalSeparatorType {
+    enum_constant public static final androidx.paging.TerminalSeparatorType FULLY_COMPLETE;
+    enum_constant public static final androidx.paging.TerminalSeparatorType SOURCE_COMPLETE;
+  }
+
 }
 
 package androidx.paging.multicast {
diff --git a/paging/common/api/public_plus_experimental_current.txt b/paging/common/api/public_plus_experimental_current.txt
index cecc76f..6792721 100644
--- a/paging/common/api/public_plus_experimental_current.txt
+++ b/paging/common/api/public_plus_experimental_current.txt
@@ -304,8 +304,11 @@
   public final class PagingDataTransforms {
     method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends java.lang.Iterable<? extends R>> transform);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
   }
@@ -476,6 +479,11 @@
   public final class SeparatorsKt {
   }
 
+  public enum TerminalSeparatorType {
+    enum_constant public static final androidx.paging.TerminalSeparatorType FULLY_COMPLETE;
+    enum_constant public static final androidx.paging.TerminalSeparatorType SOURCE_COMPLETE;
+  }
+
 }
 
 package androidx.paging.multicast {
diff --git a/paging/common/api/restricted_current.txt b/paging/common/api/restricted_current.txt
index 3208d3e..9624ee9 100644
--- a/paging/common/api/restricted_current.txt
+++ b/paging/common/api/restricted_current.txt
@@ -303,8 +303,11 @@
   public final class PagingDataTransforms {
     method @CheckResult public static <T> androidx.paging.PagingData<T> filter(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,java.lang.Boolean> predicate);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> flatMap(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends java.lang.Iterable<? extends R>> transform);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertFooterItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, T item);
     method @CheckResult public static <T> androidx.paging.PagingData<T> insertHeaderItem(androidx.paging.PagingData<T>, T item);
+    method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, optional androidx.paging.TerminalSeparatorType terminalSeparatorType, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <R, T extends R> androidx.paging.PagingData<R> insertSeparators(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function2<? super T,? super T,? extends R> generator);
     method @CheckResult public static <T, R> androidx.paging.PagingData<R> map(androidx.paging.PagingData<T>, java.util.concurrent.Executor executor, kotlin.jvm.functions.Function1<? super T,? extends R> transform);
   }
@@ -475,6 +478,11 @@
   public final class SeparatorsKt {
   }
 
+  public enum TerminalSeparatorType {
+    enum_constant public static final androidx.paging.TerminalSeparatorType FULLY_COMPLETE;
+    enum_constant public static final androidx.paging.TerminalSeparatorType SOURCE_COMPLETE;
+  }
+
 }
 
 package androidx.paging.multicast {
diff --git a/paging/common/src/main/kotlin/androidx/paging/PagingDataTransforms.kt b/paging/common/src/main/kotlin/androidx/paging/PagingDataTransforms.kt
index 5f42dc1..836b86a 100644
--- a/paging/common/src/main/kotlin/androidx/paging/PagingDataTransforms.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/PagingDataTransforms.kt
@@ -19,6 +19,7 @@
 package androidx.paging
 
 import androidx.annotation.CheckResult
+import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
 import kotlinx.coroutines.asCoroutineDispatcher
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.withContext
@@ -116,13 +117,22 @@
  * Note that this transform is applied asynchronously, as pages are loaded. Potential
  * separators between pages are only computed once both pages are loaded.
  *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param generator Generator function used to construct a separator item given the item before
+ * and the item after. For terminal separators (header and footer), the arguments passed to the
+ * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
+ * list is empty, a single separator will be added where both `before` and `after` items are `null`.
+ *
  * @sample androidx.paging.samples.insertSeparatorsSample
  * @sample androidx.paging.samples.insertSeparatorsUiModelSample
  */
 @CheckResult
 @JvmSynthetic
 fun <T : R, R : Any> PagingData<T>.insertSeparators(
-    generator: suspend (T?, T?) -> R?
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    generator: suspend (T?, T?) -> R?,
 ): PagingData<R> {
     // This function must be an extension method, as it indirectly imposes a constraint on
     // the type of T (because T extends R). Ideally it would be declared not be an
@@ -132,7 +142,7 @@
     //     class ItemModel: UiModel
     //     class SeparatorModel: UiModel
     return PagingData(
-        flow = flow.insertEventSeparators(generator),
+        flow = flow.insertEventSeparators(terminalSeparatorType, generator),
         receiver = receiver
     )
 }
@@ -227,16 +237,27 @@
  *         }
  *     }
  * }
- *
- *
  * ```
+ *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param executor [Executor] to run the [generator] function in.
+ *
+ * @param generator Generator function used to construct a separator item given the item before
+ * and the item after. For terminal separators (header and footer), the arguments passed to the
+ * generator, `before` and `after`, will be `null` respectively. In cases where the fully paginated
+ * list is empty, a single separator will be added where both `before` and `after` items are `null`.
+ *
  */
 @CheckResult
+@JvmOverloads
 fun <R : Any, T : R> PagingData<T>.insertSeparators(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
     executor: Executor,
-    generator: (T?, T?) -> R?
+    generator: (T?, T?) -> R?,
 ): PagingData<R> {
-    return insertSeparators { before, after ->
+    return insertSeparators(terminalSeparatorType) { before, after ->
         withContext(executor.asCoroutineDispatcher()) {
             generator(before, after)
         }
@@ -248,18 +269,28 @@
  * to the start of the list.
  *
  * The header [item] is added to a loaded page which marks the end of the data stream in the
- * prepend direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It will be
- * removed if the first page in the list is dropped, which can happen in the case of loaded
+ * [LoadType.PREPEND] direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It
+ * will be removed if the first page in the list is dropped, which can happen in the case of loaded
  * pages exceeding [PagedList.Config.maxSize].
  *
  * Note: This operation is not idempotent, calling it multiple times will continually add
  * more headers to the start of the list, which can be useful if multiple header items are
  * required.
  *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param item The header to add to the front of the list once it is fully loaded in the
+ * [LoadType.PREPEND] direction.
+ *
  * @see [insertFooterItem]
  */
 @CheckResult
-fun <T : Any> PagingData<T>.insertHeaderItem(item: T) = insertSeparators { before, _ ->
+@JvmOverloads
+fun <T : Any> PagingData<T>.insertHeaderItem(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    item: T,
+) = insertSeparators(terminalSeparatorType) { before, _ ->
     if (before == null) item else null
 }
 
@@ -268,17 +299,27 @@
  * to the end of the list.
  *
  * The footer [item] is added to a loaded page which marks the end of the data stream in the
- * append direction, either by returning null in [PagingSource.LoadResult.Page.nextKey]. It
- * will be removed if the first page in the list is dropped, which can happen in the case of
- * loaded* pages exceeding [PagedList.Config.maxSize].
+ * [LoadType.APPEND] direction, either by returning null in [PagingSource.LoadResult.Page.nextKey].
+ * It will be removed if the last page in the list is dropped, which can happen in the case of
+ * loaded pages exceeding [PagedList.Config.maxSize].
  *
  * Note: This operation is not idempotent, calling it multiple times will continually add
  * more footers to the end of the list, which can be useful if multiple footer items are
  * required.
  *
+ * @param terminalSeparatorType [TerminalSeparatorType] used to configure when the header and
+ * footer are added.
+ *
+ * @param item The footer to add to the end of the list once it is fully loaded in the
+ * [LoadType.APPEND] direction.
+ *
  * @see [insertHeaderItem]
  */
 @CheckResult
-fun <T : Any> PagingData<T>.insertFooterItem(item: T) = insertSeparators { _, after ->
+@JvmOverloads
+fun <T : Any> PagingData<T>.insertFooterItem(
+    terminalSeparatorType: TerminalSeparatorType = FULLY_COMPLETE,
+    item: T,
+) = insertSeparators(terminalSeparatorType) { _, after ->
     if (after == null) item else null
 }
diff --git a/paging/common/src/main/kotlin/androidx/paging/Separators.kt b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
index 632f888..87440c4 100644
--- a/paging/common/src/main/kotlin/androidx/paging/Separators.kt
+++ b/paging/common/src/main/kotlin/androidx/paging/Separators.kt
@@ -23,10 +23,42 @@
 import androidx.paging.PageEvent.Drop
 import androidx.paging.PageEvent.Insert
 import androidx.paging.PageEvent.LoadStateUpdate
+import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
+import androidx.paging.TerminalSeparatorType.SOURCE_COMPLETE
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.map
 
 /**
+ * Mode for configuring when terminal separators (header and footer) would be displayed by the
+ * [insertSeparators], [insertHeaderItem] or [insertFooterItem] operators on [PagingData].
+ */
+enum class TerminalSeparatorType {
+    /**
+     * Show terminal separators (header and footer) when both [PagingSource] and [RemoteMediator]
+     * reaches the end of pagination.
+     *
+     * End of paginations occurs when [CombinedLoadStates] has set
+     * [LoadState.endOfPaginationReached] to `true` for both [CombinedLoadStates.source] and
+     * [CombinedLoadStates.mediator] in the [PREPEND] direction for the header and in the
+     * [APPEND] direction for the footer.
+     *
+     * In cases where [RemoteMediator] isn't used, only [CombinedLoadStates.source] will be
+     * considered.
+     */
+    FULLY_COMPLETE,
+
+    /**
+     * Show terminal separators (header and footer) as soon as [PagingSource] reaches the end of
+     * pagination, regardless of [RemoteMediator]'s state.
+     *
+     * End of paginations occurs when [CombinedLoadStates] has set
+     * [LoadState.endOfPaginationReached] to `true` for [CombinedLoadStates.source] in the [PREPEND]
+     * direction for the header and in the [APPEND] direction for the footer.
+     */
+    SOURCE_COMPLETE,
+}
+
+/**
  * Create a TransformablePage with separators inside (ignoring edges)
  *
  * Separators between pages are handled outside of the page, see `Flow<PageEvent>.insertSeparators`.
@@ -146,6 +178,7 @@
 }
 
 private class SeparatorState<R : Any, T : R>(
+    val terminalSeparatorType: TerminalSeparatorType,
     val generator: suspend (before: T?, after: T?) -> R?
 ) {
     /**
@@ -195,31 +228,37 @@
         return this as Insert<R>
     }
 
-    fun CombinedLoadStates.terminatesStart(): Boolean {
-        return source.prepend.endOfPaginationReached &&
-            mediator?.prepend?.endOfPaginationReached != false
+    fun <T : Any> Insert<T>.terminatesStart(terminalSeparatorType: TerminalSeparatorType): Boolean {
+        if (loadType == APPEND) {
+            return startTerminalSeparatorDeferred
+        }
+
+        return when (terminalSeparatorType) {
+            FULLY_COMPLETE -> {
+                combinedLoadStates.source.prepend.endOfPaginationReached &&
+                    combinedLoadStates.mediator?.prepend?.endOfPaginationReached != false
+            }
+            SOURCE_COMPLETE -> combinedLoadStates.source.prepend.endOfPaginationReached
+        }
     }
 
-    fun CombinedLoadStates.terminatesEnd(): Boolean {
-        return source.append.endOfPaginationReached &&
-            mediator?.append?.endOfPaginationReached != false
-    }
+    fun <T : Any> Insert<T>.terminatesEnd(terminalSeparatorType: TerminalSeparatorType): Boolean {
+        if (loadType == PREPEND) {
+            return endTerminalSeparatorDeferred
+        }
 
-    fun <T : Any> Insert<T>.terminatesStart(): Boolean = if (loadType == APPEND) {
-        startTerminalSeparatorDeferred
-    } else {
-        combinedLoadStates.terminatesStart()
-    }
-
-    fun <T : Any> Insert<T>.terminatesEnd(): Boolean = if (loadType == PREPEND) {
-        endTerminalSeparatorDeferred
-    } else {
-        combinedLoadStates.terminatesEnd()
+        return when (terminalSeparatorType) {
+            FULLY_COMPLETE -> {
+                combinedLoadStates.source.append.endOfPaginationReached &&
+                    combinedLoadStates.mediator?.append?.endOfPaginationReached != false
+            }
+            SOURCE_COMPLETE -> combinedLoadStates.source.append.endOfPaginationReached
+        }
     }
 
     suspend fun onInsert(event: Insert<T>): Insert<R> {
-        val eventTerminatesStart = event.terminatesStart()
-        val eventTerminatesEnd = event.terminatesEnd()
+        val eventTerminatesStart = event.terminatesStart(terminalSeparatorType)
+        val eventTerminatesEnd = event.terminatesEnd(terminalSeparatorType)
         val eventEmpty = event.pages.all { it.data.isEmpty() }
 
         require(!headerAdded || event.loadType != PREPEND || eventEmpty) {
@@ -240,31 +279,49 @@
             placeholdersAfter = event.placeholdersAfter
         }
 
+        // Special-case handling for empty events when the page stash is empty as the logic after
+        // this assumes we'll have some loaded items to use when generating separators, especially
+        // in the header / footer case.
         if (eventEmpty) {
-            if (eventTerminatesStart && eventTerminatesEnd) {
-                // if event is empty, and fully terminal, resolve single separator, and that's it
-                val separator = generator(null, null)
-                endTerminalSeparatorDeferred = false
-                startTerminalSeparatorDeferred = false
-                return if (separator == null) {
-                    event.asRType()
-                } else {
-                    event.transformPages {
-                        listOf(separatorPage(separator, intArrayOf(0), 0, 0))
+            // If event is non terminal no transformation necessary, just return it directly.
+            if (!eventTerminatesStart && !eventTerminatesEnd) {
+                return event.asRType()
+            }
+
+            // We only need to transform empty insert events if they would cause terminal
+            // separators to get added. If both terminal separators are already added we can just
+            // skip this and return the event directly.
+            if (headerAdded && footerAdded) {
+                return event.asRType()
+            }
+
+            // Only resolve separators for empty events if page stash is also empty, otherwise we
+            // can use the regular flow since we have loaded items to depend on.
+            if (pageStash.isEmpty()) {
+                if (eventTerminatesStart && eventTerminatesEnd && !headerAdded && !footerAdded) {
+                    // If event is empty and fully terminal, resolve a single separator.
+                    val separator = generator(null, null)
+                    endTerminalSeparatorDeferred = false
+                    startTerminalSeparatorDeferred = false
+                    headerAdded = true
+                    footerAdded = true
+                    return if (separator == null) {
+                        event.asRType()
+                    } else {
+                        event.transformPages {
+                            listOf(separatorPage(separator, intArrayOf(0), 0, 0))
+                        }
                     }
+                } else {
+                    // can't insert the appropriate separator yet - defer!
+                    if (eventTerminatesEnd && !footerAdded) {
+                        endTerminalSeparatorDeferred = true
+                    }
+                    if (eventTerminatesStart && !headerAdded) {
+                        startTerminalSeparatorDeferred = true
+                    }
+                    return event.asRType()
                 }
-            } else if (!eventTerminatesStart && !eventTerminatesEnd) {
-                // If event is non terminal simply ignore it.
-                return event.asRType()
-            } else if (pageStash.isEmpty()) {
-                // can't insert the appropriate separator yet - defer!
-                if (eventTerminatesEnd) {
-                    endTerminalSeparatorDeferred = true
-                }
-                if (eventTerminatesStart) {
-                    startTerminalSeparatorDeferred = true
-                }
-                return event.asRType()
             }
         }
 
@@ -288,8 +345,8 @@
             firstNonEmptyPageIndex = pageIndex
             firstNonEmptyPage = event.pages[pageIndex]
 
-            // Compute the last non-empty page index to be used as adjacent pages for creating separator
-            // pages.
+            // Compute the last non-empty page index to be used as adjacent pages for creating
+            // separator pages.
             // Note: We're guaranteed to have at least one non-empty page at this point.
             pageIndex = event.pages.lastIndex
             while (pageIndex > 0 && event.pages[pageIndex].data.isEmpty()) {
@@ -300,7 +357,7 @@
         }
 
         // Header separator
-        if (eventTerminatesStart) {
+        if (eventTerminatesStart && !headerAdded) {
             headerAdded = true
 
             // Using data from previous generation if event is empty, adjacent page otherwise.
@@ -394,7 +451,7 @@
         }
 
         // Footer separator
-        if (eventTerminatesEnd) {
+        if (eventTerminatesEnd && !footerAdded) {
             footerAdded = true
 
             // Using data from previous generation if event is empty, adjacent page otherwise.
@@ -511,8 +568,11 @@
  * with PagingData.insertSeparators, which is public
  */
 internal fun <T : R, R : Any> Flow<PageEvent<T>>.insertEventSeparators(
+    terminalSeparatorType: TerminalSeparatorType,
     generator: suspend (T?, T?) -> R?
 ): Flow<PageEvent<R>> {
-    val separatorState = SeparatorState { before: T?, after: T? -> generator(before, after) }
+    val separatorState = SeparatorState(terminalSeparatorType) { before: T?, after: T? ->
+        generator(before, after)
+    }
     return map { separatorState.onEvent(it) }
 }
\ No newline at end of file
diff --git a/paging/common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt b/paging/common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
index 85bac00..0c7ed53 100644
--- a/paging/common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/HeaderFooterTest.kt
@@ -48,12 +48,12 @@
     )
 
     private suspend fun <T : Any> PageEvent<T>.insertHeaderItem(item: T) = toPagingData()
-        .insertHeaderItem(item)
+        .insertHeaderItem(item = item)
         .flow
         .single()
 
     private suspend fun <T : Any> PageEvent<T>.insertFooterItem(item: T) = toPagingData()
-        .insertFooterItem(item)
+        .insertFooterItem(item = item)
         .flow
         .single()
 
diff --git a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
index 276285b..1e2c44c 100644
--- a/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/SeparatorsTest.kt
@@ -25,6 +25,8 @@
 import androidx.paging.PageEvent.Insert.Companion.Prepend
 import androidx.paging.PageEvent.Insert.Companion.Refresh
 import androidx.paging.PageEvent.LoadStateUpdate
+import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
+import androidx.paging.TerminalSeparatorType.SOURCE_COMPLETE
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -72,7 +74,10 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -124,7 +129,10 @@
                     placeholdersBefore = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -175,7 +183,10 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -218,8 +229,11 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 ),
-                Drop<String>(APPEND, 1, 1, 4)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+                Drop(APPEND, 1, 1, 4)
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -251,7 +265,7 @@
             pages.isEmpty() -> listOf(TransformablePage.empty())
             else -> pages.map {
                 if (it != null) listOf(it)
-                else emptyList()
+                else listOf()
             }.toTransformablePages()
         },
         placeholdersBefore = 0,
@@ -264,7 +278,7 @@
         prepend: LoadState = NotLoading.Incomplete
     ) = Prepend(
         pages = pages.map {
-            if (it != null) listOf(it) else emptyList()
+            if (it != null) listOf(it) else listOf()
         }.toTransformablePages(),
         placeholdersBefore = 0,
         combinedLoadStates = localLoadStatesOf(prependLocal = prepend)
@@ -275,7 +289,7 @@
         append: LoadState = NotLoading.Incomplete
     ) = Append(
         pages = pages.map {
-            if (it != null) listOf(it) else emptyList()
+            if (it != null) listOf(it) else listOf()
         }.toTransformablePages(),
         placeholdersAfter = 0,
         combinedLoadStates = localLoadStatesOf(appendLocal = append)
@@ -296,7 +310,10 @@
             ),
             flowOf(
                 refresh(pages = listOf("a1"))
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -308,7 +325,10 @@
             ),
             flowOf(
                 refresh(pages = listOf("a1"), prepend = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -320,7 +340,10 @@
             ),
             flowOf(
                 refresh(pages = listOf("a1"), append = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -336,7 +359,10 @@
                     prepend = NotLoading.Complete,
                     append = NotLoading.Complete
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -350,7 +376,10 @@
             ),
             flowOf(
                 refresh(pages = listOf())
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -372,7 +401,10 @@
                 append(pages = listOf("a1")),
                 append(pages = listOf()),
                 append(pages = listOf(), append = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -394,7 +426,10 @@
                 prepend(pages = listOf("a1")),
                 prepend(pages = listOf()),
                 prepend(pages = listOf(), prepend = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -414,7 +449,10 @@
                 drop(loadType = PREPEND, minPageOffset = 0, maxPageOffset = 0),
                 append(pages = listOf("a1")),
                 prepend(pages = listOf(), prepend = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -434,7 +472,10 @@
                 drop(loadType = APPEND, minPageOffset = 0, maxPageOffset = 0),
                 prepend(pages = listOf("a1")),
                 append(pages = listOf(), append = NotLoading.Complete)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -449,7 +490,10 @@
             flowOf(
                 refresh(pages = listOf()),
                 prepend(pages = listOf("a1"))
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -464,7 +508,10 @@
             flowOf(
                 refresh(pages = listOf()),
                 append(pages = listOf("a1"))
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         )
     }
 
@@ -478,7 +525,10 @@
                     prepend = NotLoading.Complete
                 ),
                 drop(loadType = PREPEND, minPageOffset = 0, maxPageOffset = 0)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -523,7 +573,10 @@
                     append = NotLoading.Complete
                 ),
                 drop(loadType = APPEND, minPageOffset = 0, maxPageOffset = 0)
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -587,7 +640,7 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators<PrimaryType, Base> { before, after ->
+            ).insertEventSeparators(terminalSeparatorType = FULLY_COMPLETE) { before, after ->
                 return@insertEventSeparators (
                     if (before != null && after != null) {
                         SeparatorType("B")
@@ -613,7 +666,10 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -665,7 +721,10 @@
                     placeholdersAfter = 1,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -732,7 +791,10 @@
                     placeholdersBefore = 0,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -799,7 +861,10 @@
                     placeholdersBefore = 0,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -872,7 +937,10 @@
                     placeholdersAfter = 0,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -939,7 +1007,10 @@
                     placeholdersAfter = 0,
                     combinedLoadStates = localLoadStatesOf()
                 )
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 refresh,
@@ -988,7 +1059,7 @@
     }
 
     @Test
-    fun remoteRefreshEndOfPaginationReached() = runBlockingTest {
+    fun remoteRefreshEndOfPaginationReached_fullyComplete() = runBlockingTest {
         assertThat(
             flowOf(
                 LoadStateUpdate(
@@ -1025,7 +1096,10 @@
                     fromMediator = true,
                     loadState = NotLoading(endOfPaginationReached = true)
                 ),
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 LoadStateUpdate(
@@ -1097,7 +1171,122 @@
     }
 
     @Test
-    fun remotePrependEndOfPaginationReached() = runBlockingTest {
+    fun remoteRefreshEndOfPaginationReached_sourceComplete() = runBlockingTest {
+        assertThat(
+            flowOf(
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = LoadState.Loading
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = false,
+                    loadState = LoadState.Loading
+                ),
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = PREPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                LoadStateUpdate(
+                    loadType = APPEND,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+            ).insertEventSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
+        ).isEqualTo(
+            listOf(
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = LoadState.Loading
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = false,
+                    loadState = LoadState.Loading
+                ),
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                LoadStateUpdate(
+                    loadType = REFRESH,
+                    fromMediator = true,
+                    loadState = NotLoading(endOfPaginationReached = true)
+                ),
+                Prepend(
+                    pages = listOf(),
+                    placeholdersBefore = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = NotLoading.Complete,
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        refreshRemote = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+                Append(
+                    pages = listOf(),
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        refresh = NotLoading.Complete,
+                        prepend = NotLoading.Complete,
+                        append = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        refreshRemote = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remotePrependEndOfPaginationReached_fullyComplete() = runBlockingTest {
         assertThat(
             flowOf(
                 Refresh(
@@ -1111,7 +1300,10 @@
                 ),
                 // Signalling that remote prepend is done triggers the header to resolve.
                 LoadStateUpdate(PREPEND, true, NotLoading.Complete),
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -1145,7 +1337,68 @@
     }
 
     @Test
-    fun remotePrependEndOfPaginationReachedWithDrops() = runBlockingTest {
+    fun remotePrependEndOfPaginationReached_sourceComplete() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote prepend is done triggers the header to resolve.
+                LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+            ).insertEventSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(),
+                    placeholdersBefore = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remotePrependEndOfPaginationReachedWithDrops_fullyComplete() = runBlockingTest {
         assertThat(
             flowOf(
                 Refresh(
@@ -1194,7 +1447,10 @@
                         prependRemote = NotLoading.Complete,
                     )
                 ),
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -1280,7 +1536,155 @@
     }
 
     @Test
-    fun remoteAppendEndOfPaginationReached() = runBlockingTest {
+    fun remotePrependEndOfPaginationReachedWithDrops_sourceComplete() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        )
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote prepend is done triggers the header to resolve.
+                LoadStateUpdate(PREPEND, true, NotLoading.Complete),
+                // Drop the first page, header and separator between "b1" and "a1"
+                Drop(
+                    loadType = PREPEND,
+                    minPageOffset = -1,
+                    maxPageOffset = -1,
+                    placeholdersRemaining = 1
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        )
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    )
+                ),
+            ).insertEventSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf("b1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1, 0),
+                            data = listOf("B"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1
+                        ),
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Prepend(
+                    pages = listOf(),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    ),
+                ),
+                Drop(
+                    loadType = PREPEND,
+                    minPageOffset = -1,
+                    maxPageOffset = -1,
+                    placeholdersRemaining = 1
+                ),
+                Prepend(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = -1,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(-1, 0),
+                            data = listOf("B"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = -1
+                        ),
+                    ),
+                    placeholdersBefore = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prepend = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        prependRemote = NotLoading.Complete,
+                    )
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remoteAppendEndOfPaginationReached_fullyComplete() = runBlockingTest {
         assertThat(
             flowOf(
                 Refresh(
@@ -1294,7 +1698,10 @@
                 ),
                 // Signalling that remote append is done triggers the footer to resolve.
                 LoadStateUpdate(APPEND, true, NotLoading.Complete),
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -1328,7 +1735,68 @@
     }
 
     @Test
-    fun remoteAppendEndOfPaginationReachedWithDrops() = runBlockingTest {
+    fun remoteAppendEndOfPaginationReached_sourceComplete() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("a1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote append is done triggers the footer to resolve.
+                LoadStateUpdate(APPEND, true, NotLoading.Complete),
+            ).insertEventSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("A"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf("a1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0,
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                    )
+                ),
+                Append(
+                    pages = listOf(),
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+            )
+        )
+    }
+
+    @Test
+    fun remoteAppendEndOfPaginationReachedWithDrops_fullyComplete() = runBlockingTest {
         assertThat(
             flowOf(
                 Refresh(
@@ -1377,7 +1845,10 @@
                         appendRemote = NotLoading.Complete,
                     )
                 ),
-            ).insertEventSeparators(LETTER_SEPARATOR_GENERATOR).toList()
+            ).insertEventSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
         ).isEqualTo(
             listOf(
                 Refresh(
@@ -1462,6 +1933,154 @@
         )
     }
 
+    @Test
+    fun remoteAppendEndOfPaginationReachedWithDrops_sourceComplete() = runBlockingTest {
+        assertThat(
+            flowOf(
+                Refresh(
+                    pages = listOf(listOf("b1")).toTransformablePages(),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Incomplete,
+                    )
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        )
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                    )
+                ),
+                // Signalling that remote append is done triggers the footer to resolve.
+                LoadStateUpdate(APPEND, true, NotLoading.Complete),
+                // Drop the last page, footer and separator between "b1" and "c1"
+                Drop(
+                    loadType = APPEND,
+                    minPageOffset = 1,
+                    maxPageOffset = 1,
+                    placeholdersRemaining = 1
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        )
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    )
+                ),
+            ).insertEventSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            ).toList()
+        ).isEqualTo(
+            listOf(
+                Refresh(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0),
+                            data = listOf("B"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 0
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 0,
+                            data = listOf("b1"),
+                        ),
+                    ),
+                    placeholdersBefore = 1,
+                    placeholdersAfter = 1,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Incomplete,
+                    ),
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0, 1),
+                            data = listOf("C"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(1),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1,
+                        ),
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                    ),
+                ),
+                Append(
+                    pages = listOf(),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    ),
+                ),
+                Drop(
+                    loadType = APPEND,
+                    minPageOffset = 1,
+                    maxPageOffset = 1,
+                    placeholdersRemaining = 1
+                ),
+                Append(
+                    pages = listOf(
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(0, 1),
+                            data = listOf("C"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                        TransformablePage(
+                            originalPageOffset = 1,
+                            data = listOf("c1"),
+                        ),
+                        TransformablePage(
+                            originalPageOffsets = intArrayOf(1),
+                            data = listOf("END"),
+                            hintOriginalIndices = listOf(0),
+                            hintOriginalPageOffset = 1
+                        ),
+                    ),
+                    placeholdersAfter = 0,
+                    combinedLoadStates = remoteLoadStatesOf(
+                        append = NotLoading.Complete,
+                        prependLocal = NotLoading.Complete,
+                        appendLocal = NotLoading.Complete,
+                        appendRemote = NotLoading.Complete,
+                    )
+                ),
+            )
+        )
+    }
+
     companion object {
         /**
          * Creates an upper-case letter at the beginning of each section of strings that start
diff --git a/paging/common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt b/paging/common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
index 2de817b..24cdf6e 100644
--- a/paging/common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
+++ b/paging/common/src/test/kotlin/androidx/paging/SeparatorsWithRemoteMediatorTest.kt
@@ -20,6 +20,9 @@
 import androidx.paging.PageEvent.Insert.Companion.Append
 import androidx.paging.PageEvent.Insert.Companion.Prepend
 import androidx.paging.PageEvent.Insert.Companion.Refresh
+import androidx.paging.SeparatorsTest.Companion.LETTER_SEPARATOR_GENERATOR
+import androidx.paging.TerminalSeparatorType.FULLY_COMPLETE
+import androidx.paging.TerminalSeparatorType.SOURCE_COMPLETE
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.flowOf
@@ -57,7 +60,10 @@
             "Prepend after endOfPaginationReached already true is invalid"
         ) {
             PagingData(pageEventFlow, dummyReceiver)
-                .insertSeparators(SeparatorsTest.LETTER_SEPARATOR_GENERATOR)
+                .insertSeparators(
+                    terminalSeparatorType = FULLY_COMPLETE,
+                    generator = LETTER_SEPARATOR_GENERATOR
+                )
                 .flow.toList()
         }
     }
@@ -86,7 +92,10 @@
             "Append after endOfPaginationReached already true is invalid"
         ) {
             PagingData(pageEventFlow, dummyReceiver)
-                .insertSeparators(SeparatorsTest.LETTER_SEPARATOR_GENERATOR)
+                .insertSeparators(
+                    terminalSeparatorType = FULLY_COMPLETE,
+                    generator = LETTER_SEPARATOR_GENERATOR
+                )
                 .flow.toList()
         }
     }
@@ -144,7 +153,7 @@
     }
 
     @Test
-    fun emptyPrependThenEmptyRemote() = runBlockingTest {
+    fun emptyPrependThenEmptyRemote_fullyComplete() = runBlockingTest {
         val pageEventFlow = flowOf(
             generateRefresh(listOf("a1"), remoteLoadStatesOf()),
             generatePrepend(
@@ -170,9 +179,6 @@
             ),
             generatePrepend(
                 // page offset becomes 0 here, as it's adjacent to page 0, the only page with data.
-                // Ideally it would be 2, since that's the index of the page last added,
-                // but empty pages are filtered out, so originalPageOffset = 1 and 2 don't make
-                // it to separators logic.
                 originalPageOffset = 0,
                 pages = listOf(listOf("A")),
                 loadStates = remoteLoadStatesOf(
@@ -183,14 +189,63 @@
         )
 
         val actual = PagingData(pageEventFlow, dummyReceiver)
-            .insertSeparators(SeparatorsTest.LETTER_SEPARATOR_GENERATOR)
+            .insertSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            )
             .flow.toList()
 
         assertThat(actual).isEqualTo(expected)
     }
 
     @Test
-    fun emptyAppendThenEmptyRemote() = runBlockingTest {
+    fun emptyPrependThenEmptyRemote_sourceComplete() = runBlockingTest {
+        val pageEventFlow = flowOf(
+            generateRefresh(listOf("a1"), remoteLoadStatesOf()),
+            generatePrepend(
+                originalPageOffset = 1,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(prependLocal = NotLoading.Complete)
+            ),
+            generatePrepend(
+                originalPageOffset = 2,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(
+                    prependLocal = NotLoading.Complete,
+                    prependRemote = NotLoading.Complete
+                )
+            )
+        )
+        val expected = listOf(
+            generateRefresh(listOf("a1"), remoteLoadStatesOf()),
+            generatePrepend(
+                // page offset becomes 0 here, as it's adjacent to page 0, the only page with data.
+                originalPageOffset = 0,
+                pages = listOf(listOf("A")),
+                loadStates = remoteLoadStatesOf(prependLocal = NotLoading.Complete)
+            ),
+            generatePrepend(
+                originalPageOffset = 2,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(
+                    prependLocal = NotLoading.Complete,
+                    prependRemote = NotLoading.Complete
+                )
+            )
+        )
+
+        val actual = PagingData(pageEventFlow, dummyReceiver)
+            .insertSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            )
+            .flow.toList()
+
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun emptyAppendThenEmptyRemote_fullyComplete() = runBlockingTest {
         val pageEventFlow = flowOf(
             generateRefresh(listOf("a1"), remoteLoadStatesOf()),
             generateAppend(
@@ -216,9 +271,6 @@
             ),
             generateAppend(
                 // page offset becomes 0 here, as it's adjacent to page 0, the only page with data.
-                // Ideally it would be 2, since that's the index of the page last added,
-                // but empty pages are filtered out, so originalPageOffset = 1 and 2 don't make
-                // it to separators logic.
                 originalPageOffset = 0,
                 pages = listOf(listOf("END")),
                 loadStates = remoteLoadStatesOf(
@@ -229,7 +281,56 @@
         )
 
         val actual = PagingData(pageEventFlow, dummyReceiver)
-            .insertSeparators(SeparatorsTest.LETTER_SEPARATOR_GENERATOR)
+            .insertSeparators(
+                terminalSeparatorType = FULLY_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            )
+            .flow.toList()
+
+        assertThat(actual).isEqualTo(expected)
+    }
+
+    @Test
+    fun emptyAppendThenEmptyRemote_sourceComplete() = runBlockingTest {
+        val pageEventFlow = flowOf(
+            generateRefresh(listOf("a1"), remoteLoadStatesOf()),
+            generateAppend(
+                originalPageOffset = 1,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(appendLocal = NotLoading.Complete)
+            ),
+            generateAppend(
+                originalPageOffset = 2,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(
+                    appendLocal = NotLoading.Complete,
+                    appendRemote = NotLoading.Complete
+                )
+            )
+        )
+        val expected = listOf(
+            generateRefresh(listOf("a1"), remoteLoadStatesOf()),
+            generateAppend(
+                // page offset becomes 0 here, as it's adjacent to page 0, the only page with data.
+                originalPageOffset = 0,
+                pages = listOf(listOf("END")),
+                loadStates = remoteLoadStatesOf(appendLocal = NotLoading.Complete)
+            ),
+            generateAppend(
+                originalPageOffset = 2,
+                pages = listOf(),
+                loadStates = remoteLoadStatesOf(
+                    appendLocal = NotLoading.Complete,
+                    appendRemote = NotLoading.Complete
+                )
+            )
+        )
+
+        val actual = PagingData(pageEventFlow, dummyReceiver)
+            .insertSeparators(
+                terminalSeparatorType = SOURCE_COMPLETE,
+                generator = LETTER_SEPARATOR_GENERATOR
+            )
             .flow.toList()
 
         assertThat(actual).isEqualTo(expected)
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3ViewModel.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3ViewModel.kt
index 2cd36a9..943e2d0 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3ViewModel.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3/V3ViewModel.kt
@@ -53,8 +53,8 @@
                         )
                     } else null
                 }
-                .insertHeaderItem(Item(Int.MIN_VALUE, "HEADER", Color.MAGENTA))
-                .insertFooterItem(Item(Int.MAX_VALUE, "FOOTER", Color.MAGENTA))
+                .insertHeaderItem(item = Item(Int.MIN_VALUE, "HEADER", Color.MAGENTA))
+                .insertFooterItem(item = Item(Int.MAX_VALUE, "FOOTER", Color.MAGENTA))
         }
         .cachedIn(viewModelScope)
 }
diff --git a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
index c336fdb..8f927e0 100644
--- a/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
+++ b/paging/integration-tests/testapp/src/main/java/androidx/paging/integration/testapp/v3room/V3RoomViewModel.kt
@@ -94,14 +94,14 @@
                     } else null
                 }
                 .insertHeaderItem(
-                    Customer().apply {
+                    item = Customer().apply {
                         id = Int.MIN_VALUE
                         name = "HEADER"
                         lastName = "HEADER"
                     }
                 )
                 .insertFooterItem(
-                    Customer().apply {
+                    item = Customer().apply {
                         id = Int.MAX_VALUE
                         name = "FOOTER"
                         lastName = "FOOTER"
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
new file mode 100644
index 0000000..5d35646
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XEnumTypeElement.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.compiler.processing
+
+import kotlin.contracts.contract
+
+/**
+ * Type elements that represent Enum declarations.
+ */
+interface XEnumTypeElement : XTypeElement {
+    val enumConstantNames: Set<String>
+}
+
+fun XTypeElement.isEnum(): Boolean {
+    contract {
+        returns(true) implies (this@isEnum is XEnumTypeElement)
+    }
+    return this is XEnumTypeElement
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
index 13d02ef..52388ed 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/XType.kt
@@ -107,11 +107,6 @@
     fun isNone(): Boolean
 
     /**
-     * Returns true if this represented by an [Enum].
-     */
-    fun isEnum(): Boolean
-
-    /**
      * Returns `true` if this is the same raw type as [other]
      */
     fun isTypeOf(other: KClass<*>): Boolean
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
index 8959ca0..d9bee51 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/ElementExt.kt
@@ -20,6 +20,7 @@
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
 import javax.lang.model.element.Element
+import javax.lang.model.element.ElementKind
 import javax.lang.model.element.TypeElement
 import javax.lang.model.element.VariableElement
 import javax.lang.model.type.TypeKind
@@ -39,6 +40,8 @@
 /**
  * Returns all fields including private fields (including private fields in super). Removes
  * duplicate fields if class has a field with the same name as the parent.
+ * Note that enum constants are not included in the list even thought they are fields in java.
+ * To access enum constants, use [JavacTypeElement.JavacEnumTypeElement].
  */
 internal fun TypeElement.getAllFieldsIncludingPrivateSupers(
     elementUtils: Elements
@@ -46,6 +49,7 @@
     val selection = ElementFilter
         .fieldsIn(elementUtils.getAllMembers(this))
         .filterIsInstance<VariableElement>()
+        .filterNot { it.kind == ElementKind.ENUM_CONSTANT }
         .toMutableSet()
     val selectionNames = selection.mapTo(mutableSetOf()) {
         it.simpleName
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
index 8e6e544..2266ced 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacProcessingEnv.kt
@@ -46,8 +46,8 @@
             findElement = { qName ->
                 delegate.elementUtils.getTypeElement(qName)
             },
-            wrap = {
-                JavacTypeElement(this, it)
+            wrap = { typeElement ->
+                JavacTypeElement.create(this, typeElement)
             },
             getQName = {
                 it.qualifiedName.toString()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
index 13889f5..e220f6b 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacType.kt
@@ -24,7 +24,6 @@
 import androidx.room.compiler.processing.ksp.ERROR_TYPE_NAME
 import androidx.room.compiler.processing.safeTypeName
 import com.google.auto.common.MoreTypes
-import javax.lang.model.element.ElementKind
 import javax.lang.model.type.TypeKind
 import javax.lang.model.type.TypeMirror
 import kotlin.reflect.KClass
@@ -167,7 +166,4 @@
         // a boxed primitive to be marked as non-null.
         return copyWithNullability(XNullability.NONNULL)
     }
-
-    override fun isEnum() = typeMirror.kind == TypeKind.DECLARED &&
-        MoreTypes.asElement(typeMirror).kind == ElementKind.ENUM
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
index 2395061..acaffb7 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacTypeElement.kt
@@ -16,10 +16,12 @@
 
 package androidx.room.compiler.processing.javac
 
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.XTypeElement
+import androidx.room.compiler.processing.javac.JavacTypeElement.JavacEnumTypeElement
 import androidx.room.compiler.processing.javac.kotlin.KotlinMetadataElement
 import com.google.auto.common.MoreElements
 import com.google.auto.common.MoreTypes
@@ -29,7 +31,7 @@
 import javax.lang.model.type.TypeKind
 import javax.lang.model.util.ElementFilter
 
-internal class JavacTypeElement(
+internal sealed class JavacTypeElement(
     env: JavacProcessingEnv,
     override val element: TypeElement
 ) : JavacElement(env, element), XTypeElement, XHasModifiers by JavacHasModifiers(element) {
@@ -142,4 +144,38 @@
     override val equalityItems: Array<out Any?> by lazy {
         arrayOf(element)
     }
+
+    class DefaultJavacTypeElement(
+        env: JavacProcessingEnv,
+        element: TypeElement
+    ) : JavacTypeElement(env, element)
+
+    class JavacEnumTypeElement(
+        env: JavacProcessingEnv,
+        element: TypeElement
+    ) : JavacTypeElement(env, element), XEnumTypeElement {
+        init {
+            check(element.kind == ElementKind.ENUM)
+        }
+
+        override val enumConstantNames: Set<String> by lazy {
+            element.enclosedElements.filter {
+                it.kind == ElementKind.ENUM_CONSTANT
+            }.mapTo(mutableSetOf()) {
+                it.simpleName.toString()
+            }
+        }
+    }
+
+    companion object {
+        fun create(
+            env: JavacProcessingEnv,
+            typeElement: TypeElement
+        ): JavacTypeElement {
+            return when (typeElement.kind) {
+                ElementKind.ENUM -> JavacEnumTypeElement(env, typeElement)
+                else -> DefaultJavacTypeElement(env, typeElement)
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
index fee52da..ba8ae4e 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspProcessingEnv.kt
@@ -57,8 +57,8 @@
                 // it is best to just not cache them
                 it.qualifiedName?.asString()
             },
-            wrap = {
-                KspTypeElement(this, it)
+            wrap = { classDeclaration ->
+                KspTypeElement.create(this, classDeclaration)
             }
         )
 
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
index dcdfca1..4284a63 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspType.kt
@@ -21,7 +21,6 @@
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.tryBox
 import androidx.room.compiler.processing.tryUnbox
-import com.google.devtools.ksp.symbol.ClassKind
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeReference
@@ -139,10 +138,6 @@
         return ksType.toString()
     }
 
-    override fun isEnum(): Boolean {
-        return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
-    }
-
     abstract override fun boxed(): KspType
 
     /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
index 6712e14..437d9a3 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeElement.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XAnnotated
 import androidx.room.compiler.processing.XConstructorElement
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.compiler.processing.XFieldElement
 import androidx.room.compiler.processing.XHasModifiers
 import androidx.room.compiler.processing.XMethodElement
@@ -40,7 +41,7 @@
 import com.google.devtools.ksp.symbol.Origin
 import com.squareup.javapoet.ClassName
 
-internal class KspTypeElement(
+internal sealed class KspTypeElement(
     env: KspProcessingEnv,
     override val declaration: KSClassDeclaration
 ) : KspElement(env, declaration),
@@ -342,4 +343,36 @@
     override fun toString(): String {
         return declaration.toString()
     }
+
+    private class DefaultKspTypeElement(
+        env: KspProcessingEnv,
+        declaration: KSClassDeclaration
+    ) : KspTypeElement(env, declaration)
+
+    private class KspEnumTypeElement(
+        env: KspProcessingEnv,
+        declaration: KSClassDeclaration
+    ) : KspTypeElement(env, declaration), XEnumTypeElement {
+        override val enumConstantNames: Set<String> by lazy {
+            // TODO this does not work for java sources
+            // https://github.com/google/ksp/issues/234
+            declaration.declarations.filter {
+                it is KSClassDeclaration && it.classKind == ClassKind.ENUM_ENTRY
+            }.mapTo(mutableSetOf()) {
+                it.simpleName.asString()
+            }
+        }
+    }
+
+    companion object {
+        fun create(
+            env: KspProcessingEnv,
+            ksClassDeclaration: KSClassDeclaration
+        ): KspTypeElement {
+            return when (ksClassDeclaration.classKind) {
+                ClassKind.ENUM_CLASS -> KspEnumTypeElement(env, ksClassDeclaration)
+                else -> DefaultKspTypeElement(env, ksClassDeclaration)
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
index 6e7b5bc..21ab8ac 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/ResolverExt.kt
@@ -144,12 +144,8 @@
         return declaration.simpleName.asString()
     } catch (cannotFindDeclaration: IllegalStateException) {
         // workaround for https://github.com/google/ksp/issues/200
-        val name = declaration.simpleName.asString()
-        if (name.startsWith("get") or name.startsWith("set")) {
-            return name
-        }
-        // we don't know why it happened so we better throw
-        throw cannotFindDeclaration
+        // happens for setters getters as well as `values` method of Enum descriptor
+        return declaration.simpleName.asString()
     }
 }
 
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
index ddd18f3..7e46099 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XTypeElementTest.kt
@@ -18,13 +18,14 @@
 
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.compileFiles
 import androidx.room.compiler.processing.util.getAllFieldNames
 import androidx.room.compiler.processing.util.getField
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.runKspTest
 import androidx.room.compiler.processing.util.runProcessorTest
-import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
@@ -764,13 +765,84 @@
                 )
 
             subjects.forEach {
-                Truth.assertWithMessage(it)
+                assertWithMessage(it)
                     .that(invocation.processingEnv.requireTypeElement(it).findPrimaryConstructor())
                     .isNull()
             }
         }
     }
 
+    @Test
+    fun enumTypeElement() {
+        fun createSources(packageName: String) = listOf(
+            Source.kotlin(
+                "$packageName/KotlinEnum.kt",
+                """
+                package $packageName
+                enum class KotlinEnum(private val x:Int) {
+                    VAL1(1),
+                    VAL2(2);
+
+                    fun enumMethod():Unit {}
+                }
+                """.trimIndent()
+            ),
+            Source.java(
+                "$packageName.JavaEnum",
+                """
+                package $packageName;
+                public enum JavaEnum {
+                    VAL1(1),
+                    VAL2(2);
+
+                    private int x;
+
+                    JavaEnum(int x) {
+                        this.x = x;
+                    }
+                    void enumMethod() {}
+                }
+                """.trimIndent()
+            )
+        )
+        val classpath = compileFiles(
+            createSources("lib")
+        )
+        runProcessorTest(
+            sources = createSources("app"),
+            classpath = listOf(classpath)
+        ) { invocation ->
+            listOf(
+                "lib.KotlinEnum", "lib.JavaEnum",
+                "app.KotlinEnum", "app.JavaEnum"
+            ).forEach { qName ->
+                val typeElement = invocation.processingEnv.requireTypeElement(qName)
+                assertWithMessage("$qName is enum")
+                    .that(typeElement.isEnum())
+                    .isTrue()
+                assertWithMessage("$qName does not report enum constants in methods")
+                    .that(typeElement.getDeclaredMethods().map { it.name })
+                    .run {
+                        contains("enumMethod")
+                        containsNoneOf("VAL1", "VAL2")
+                    }
+                if (qName != "app.JavaEnum" || !invocation.isKsp) {
+                    // KSP does not properly return enum constants for java sources yet
+                    // https://github.com/google/ksp/issues/234
+                    assertWithMessage("$qName can return enum constants")
+                        .that((typeElement as XEnumTypeElement).enumConstantNames)
+                        .containsExactly("VAL1", "VAL2")
+                    assertWithMessage("$qName  does not report enum constants in fields")
+                        .that(typeElement.getAllFieldNames())
+                        .run {
+                            contains("x")
+                            containsNoneOf("VAL1", "VAL2")
+                        }
+                }
+            }
+        }
+    }
+
     /**
      * it is good to exclude methods coming from Object when testing as they differ between KSP
      * and KAPT but irrelevant for Room.
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index f12c2b1..0a3c658 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -18,6 +18,7 @@
 
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.isArray
+import androidx.room.compiler.processing.isEnum
 import androidx.room.ext.CommonTypeNames
 import androidx.room.ext.GuavaBaseTypeNames
 import androidx.room.ext.isEntityElement
@@ -346,8 +347,9 @@
     }
 
     private fun createEnumTypeAdapter(type: XType): ColumnTypeAdapter? {
-        if (type.isEnum()) {
-            return EnumColumnTypeAdapter(type)
+        val typeElement = type.typeElement ?: return null
+        if (typeElement.isEnum()) {
+            return EnumColumnTypeAdapter(typeElement)
         }
         return null
     }
diff --git a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
index 2f01519..879019b 100644
--- a/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/androidx/room/solver/types/EnumColumnTypeAdapter.kt
@@ -16,8 +16,7 @@
 
 package androidx.room.solver.types
 
-import androidx.room.compiler.processing.XFieldElement
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XEnumTypeElement
 import androidx.room.ext.CommonTypeNames.ILLEGAL_ARG_EXCEPTION
 import androidx.room.ext.L
 import androidx.room.ext.N
@@ -28,15 +27,14 @@
 import androidx.room.writer.ClassWriter
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
-import java.util.Locale
-import javax.lang.model.element.ElementKind
 import javax.lang.model.element.Modifier
 
 /**
  * Uses enum string representation.
  */
-class EnumColumnTypeAdapter(out: XType) :
-    ColumnTypeAdapter(out, TEXT) {
+class EnumColumnTypeAdapter(
+    private val enumTypeElement: XEnumTypeElement
+) : ColumnTypeAdapter(enumTypeElement.type, TEXT) {
     override fun readFromCursor(
         outVarName: String,
         cursorVarName: String,
@@ -92,8 +90,8 @@
                         beginControlFlow("if ($N == null)", param)
                         addStatement("return null")
                         nextControlFlow("switch ($N)", param)
-                        getEnumConstantElements().forEach { enumConstant ->
-                            addStatement("case $L: return $S", enumConstant.name, enumConstant.name)
+                        enumTypeElement.enumConstantNames.forEach { enumConstantName ->
+                            addStatement("case $L: return $S", enumConstantName, enumConstantName)
                         }
                         addStatement(
                             "default: throw new $T($S + $N)",
@@ -129,11 +127,10 @@
                         beginControlFlow("if ($N == null)", param)
                         addStatement("return null")
                         nextControlFlow("switch ($N)", param)
-                        getEnumConstantElements().forEach {
-                            enumConstant ->
+                        enumTypeElement.enumConstantNames.forEach { enumConstantName ->
                             addStatement(
                                 "case $S: return $T.$L",
-                                enumConstant.name, out.typeName, enumConstant.name
+                                enumConstantName, out.typeName, enumConstantName
                             )
                         }
                         addStatement(
@@ -147,14 +144,4 @@
                 }
             })
     }
-
-    private fun getEnumConstantElements(): List<XFieldElement> {
-        // TODO: Switch below logic to use`getDeclaredFields` when the
-        //  functionality is available in the XTypeElement API
-        val typeElementFields = out.typeElement!!.getAllFieldsIncludingPrivateSupers()
-        return typeElementFields.filter {
-            // TODO: (b/173236324) Add kind to the X abstraction API to avoid using kindName()
-            ElementKind.ENUM_CONSTANT.toString().toLowerCase(Locale.US) == it.kindName()
-        }
-    }
 }
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt
new file mode 100644
index 0000000..2911d71
--- /dev/null
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/EnumColumnTypeAdapterTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.integration.kotlintestapp.test
+
+import android.content.Context
+import androidx.room.Dao
+import androidx.room.Database
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import androidx.room.Query
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverter
+import androidx.room.TypeConverters
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class EnumColumnTypeAdapterTest {
+    private lateinit var db: EnumColumnTypeAdapterDatabase
+
+    @Entity
+    data class EntityWithEnum(
+        @PrimaryKey
+        val id: Long,
+        val fruit: Fruit
+    )
+
+    @Entity
+    data class EntityWithOneWayEnum(
+        @PrimaryKey
+        val id: Long,
+        val color: Color
+    )
+
+    @Entity
+    data class ComplexEntityWithEnum(
+        @PrimaryKey
+        val id: Long,
+        val season: Season
+    )
+
+    enum class Color {
+        RED, GREEN
+    }
+
+    enum class Fruit {
+        BANANA, STRAWBERRY, WILDBERRY
+    }
+
+    enum class Season(private val text: String) {
+        SUMMER("Sunny"), SPRING("Warm"), WINTER("Cold"), AUTUMN("Rainy");
+    }
+
+    @Dao
+    interface SampleDao {
+        @Query("INSERT INTO EntityWithEnum (id, fruit) VALUES (:id, :fruit)")
+        fun insert(id: Long, fruit: Fruit?): Long
+
+        @Query("SELECT * FROM EntityWithEnum WHERE id = :id")
+        fun getValueWithId(id: Long): EntityWithEnum
+    }
+
+    @Dao
+    interface SampleDaoWithOneWayConverter {
+        @Query("INSERT INTO EntityWithOneWayEnum (id, color) VALUES (:id, :colorInt)")
+        fun insert(id: Long, colorInt: Int): Long
+
+        @Query("SELECT * FROM EntityWithOneWayEnum WHERE id = :id")
+        fun getValueWithId(id: Long): EntityWithOneWayEnum
+    }
+
+    class ColorTypeConverter {
+        @TypeConverter
+        fun fromIntToColorEnum(colorInt: Int): Color {
+            return if (colorInt == 1) {
+                Color.RED
+            } else {
+                Color.GREEN
+            }
+        }
+    }
+
+    @Dao
+    interface SampleDaoWithComplexEnum {
+        @Query("INSERT INTO ComplexEntityWithEnum (id, season) VALUES (:id, :season)")
+        fun insertComplex(id: Long, season: Season?): Long
+
+        @Query("SELECT * FROM ComplexEntityWithEnum WHERE id = :id")
+        fun getComplexValueWithId(id: Long): ComplexEntityWithEnum
+    }
+
+    @Database(
+        entities = [
+            EntityWithEnum::class,
+            ComplexEntityWithEnum::class,
+            EntityWithOneWayEnum::class
+        ],
+        version = 1,
+        exportSchema = false
+    )
+    @TypeConverters(
+        ColorTypeConverter::class
+    )
+    abstract class EnumColumnTypeAdapterDatabase : RoomDatabase() {
+        abstract fun dao(): SampleDao
+        abstract fun oneWayDao(): SampleDaoWithOneWayConverter
+        abstract fun complexDao(): SampleDaoWithComplexEnum
+    }
+
+    @Before
+    fun initDb() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        db = Room.inMemoryDatabaseBuilder(
+            context,
+            EnumColumnTypeAdapterDatabase::class.java
+        ).build()
+    }
+
+    @After
+    fun teardown() {
+        db.close()
+    }
+
+    @Test
+    fun readAndWriteEnumToDatabase() {
+        db.dao().insert(1, Fruit.BANANA)
+        db.dao().insert(2, Fruit.STRAWBERRY)
+        assertThat(
+            db.dao().getValueWithId(1).fruit
+        ).isEqualTo(Fruit.BANANA)
+        assertThat(
+            db.dao().getValueWithId(2).fruit
+        ).isEqualTo(Fruit.STRAWBERRY)
+    }
+
+    @Test
+    fun writeOneWayEnumToDatabase() {
+        db.oneWayDao().insert(1, 1)
+        assertThat(
+            db.oneWayDao().getValueWithId(1).color
+        ).isEqualTo(
+            Color.RED
+        )
+    }
+
+    @Test
+    fun filterOutComplexEnumTest() {
+        db.complexDao().insertComplex(1, Season.AUTUMN)
+        assertThat(
+            db.complexDao().getComplexValueWithId(1).season
+        ).isEqualTo(
+            Season.AUTUMN
+        )
+    }
+}