Merge "Adding functionality to handle proguard when storing enums in databases." into androidx-master-dev
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 7fe96b8..4175155 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -30,16 +30,16 @@
     api("androidx.core:core-ktx:1.1.0") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
-    api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+    api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
     api(KOTLIN_STDLIB)
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(JUNIT)
     androidTestImplementation(TRUTH)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b0d8662..e683bb6 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,12 +23,12 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.core:core:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
-    api("androidx.savedstate:savedstate:1.1.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+    api(projectOrArtifact(":savedstate:savedstate"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.0-beta01")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(KOTLIN_STDLIB)
     androidTestImplementation(LEAKCANARY)
     androidTestImplementation(LEAKCANARY_INSTRUMENTATION)
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/public_plus_experimental_current.txt b/appsearch/appsearch/api/public_plus_experimental_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/public_plus_experimental_current.txt
+++ b/appsearch/appsearch/api/public_plus_experimental_current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index e0fa7a7..32a70b4 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -98,6 +98,7 @@
 
   public interface AppSearchSession {
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,androidx.appsearch.app.GenericDocument!>!> getByUri(androidx.appsearch.app.GetByUriRequest);
+    method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.util.Set<androidx.appsearch.app.AppSearchSchema!>!>!> getSchema();
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchBatchResult<java.lang.String!,java.lang.Void!>!> putDocuments(androidx.appsearch.app.PutDocumentsRequest);
     method public androidx.appsearch.app.SearchResults query(String, androidx.appsearch.app.SearchSpec);
     method public com.google.common.util.concurrent.ListenableFuture<androidx.appsearch.app.AppSearchResult<java.lang.Void!>!> removeByQuery(String, androidx.appsearch.app.SearchSpec);
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
new file mode 100644
index 0000000..99ac18e
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/SetSchemaRequestTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.appsearch.app;
+
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+import static androidx.appsearch.app.AppSearchSchema.PropertyConfig.TOKENIZER_TYPE_PLAIN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import androidx.appsearch.annotation.AppSearchDocument;
+
+import org.junit.Test;
+
+public class SetSchemaRequestTest {
+
+    @AppSearchDocument
+    static class Card {
+        @AppSearchDocument.Uri
+        String mUri;
+        @AppSearchDocument.Property
+                (indexingType = INDEXING_TYPE_PREFIXES, tokenizerType = TOKENIZER_TYPE_PLAIN)
+        String mString;
+
+        @Override
+        public boolean equals(Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof AnnotationProcessorTest.Card)) {
+                return false;
+            }
+            AnnotationProcessorTest.Card otherCard = (AnnotationProcessorTest.Card) other;
+            assertThat(otherCard.mUri).isEqualTo(this.mUri);
+            return true;
+        }
+    }
+
+    @Test
+    public void testInvalidSchemaReferences() {
+        IllegalArgumentException expected = assertThrows(IllegalArgumentException.class,
+                () -> new SetSchemaRequest.Builder().setSchemaTypeVisibilityForSystemUi(false,
+                        "InvalidSchema").build());
+        assertThat(expected).hasMessageThat().contains("referenced, but were not added");
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForSystemUi_Visible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+
+        // By default, the schema is visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchema(schema).build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+
+        request =
+                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
+                        true,
+                        "Schema").build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+    }
+
+    @Test
+    public void testSchemaTypeVisibilityForSystemUi_NotVisible() {
+        AppSearchSchema schema = new AppSearchSchema.Builder("Schema").build();
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addSchema(schema).setSchemaTypeVisibilityForSystemUi(
+                        false,
+                        "Schema").build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Schema");
+    }
+
+    @Test
+    public void testDataClassVisibilityForSystemUi_Visible() throws Exception {
+        // By default, the schema is visible.
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(Card.class).build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+
+        request =
+                new SetSchemaRequest.Builder().addDataClass(
+                        Card.class).setDataClassVisibilityForSystemUi(
+                        true,
+                        Card.class).build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).isEmpty();
+    }
+
+    @Test
+    public void testDataClassVisibilityForSystemUi_NotVisible() throws Exception {
+        SetSchemaRequest request =
+                new SetSchemaRequest.Builder().addDataClass(
+                        Card.class).setDataClassVisibilityForSystemUi(
+                        false,
+                        Card.class).build();
+        assertThat(request.getSchemasNotPlatformSurfaceable()).containsExactly("Card");
+    }
+}
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
index ba04b58..7a646b0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSchemaCtsTest.java
@@ -72,4 +72,105 @@
                         .build()));
         assertThat(e).hasMessageThat().contains("Property defined more than once: subject");
     }
+
+    @Test
+    public void testEquals_identical() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .build()
+                ).build();
+        assertThat(schema1).isEqualTo(schema2);
+        assertThat(schema1.hashCode()).isEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
+
+    @Test
+    public void testEquals_failure_differentOrder() {
+        AppSearchSchema schema1 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        // Order of 'body' and 'subject' has been switched
+        AppSearchSchema schema2 = new AppSearchSchema.Builder("Email")
+                .addProperty(new PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        assertThat(schema1).isNotEqualTo(schema2);
+        assertThat(schema1.hashCode()).isNotEqualTo(schema2.hashCode());
+    }
 }
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
index 9586204..91af24c 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/cts/AppSearchSessionCtsTest.java
@@ -99,6 +99,52 @@
     }
 
     @Test
+    public void testGetSchema() throws Exception {
+        AppSearchSchema emailSchema1 = new AppSearchSchema.Builder("Email1")
+                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES)
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+        AppSearchSchema emailSchema2 = new AppSearchSchema.Builder("Email2")
+                .addProperty(new AppSearchSchema.PropertyConfig.Builder("subject")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).addProperty(new AppSearchSchema.PropertyConfig.Builder("body")
+                        .setDataType(PropertyConfig.DATA_TYPE_STRING)
+                        .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL)
+                        .setIndexingType(PropertyConfig.INDEXING_TYPE_EXACT_TERMS)  // Different
+                        .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN)
+                        .build()
+                ).build();
+
+        SetSchemaRequest request1 = new SetSchemaRequest.Builder()
+                .addSchema(emailSchema1).addDataClass(EmailDataClass.class).build();
+        SetSchemaRequest request2 = new SetSchemaRequest.Builder()
+                .addSchema(emailSchema2).addDataClass(EmailDataClass.class).build();
+
+        checkIsResultSuccess(mDb1.setSchema(request1));
+        checkIsResultSuccess(mDb2.setSchema(request2));
+
+        Set<AppSearchSchema> actual1 = checkIsResultSuccess(mDb1.getSchema());
+        Set<AppSearchSchema> actual2 = checkIsResultSuccess(mDb2.getSchema());
+
+        assertThat(actual1).isEqualTo(request1.getSchemas());
+        assertThat(actual2).isEqualTo(request2.getSchemas());
+    }
+
+    @Test
     public void testPutDocuments() throws Exception {
         // Schema registration
         checkIsResultSuccess(mDb1.setSchema(
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
new file mode 100644
index 0000000..3830b9d
--- /dev/null
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/util/BundleUtilTest.java
@@ -0,0 +1,255 @@
+/*
+ * 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.appsearch.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.util.Size;
+import android.util.SizeF;
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.UUID;
+
+public class BundleUtilTest {
+    @Test
+    public void testDeepEquals_self() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+        assertThat(one).isEqualTo(one);
+        assertThat(BundleUtil.deepEquals(one, one)).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_simple() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+
+        Bundle two = new Bundle();
+        two.putString("a", "a");
+
+        assertThat(one).isNotEqualTo(two);
+        assertThat(BundleUtil.deepEquals(one, two)).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_keyMismatch() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+
+        Bundle two = new Bundle();
+        two.putString("a", "a");
+        two.putString("b", "b");
+        assertThat(BundleUtil.deepEquals(one, two)).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_thorough_equal() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            inputs[i] = createThoroughBundle();
+        }
+        assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+        assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isTrue();
+    }
+
+    @Test
+    public void testDeepEquals_thorough_notEqual() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = createThoroughBundle();
+            // Create a difference
+            assertThat(b.containsKey("doubleArray")).isTrue();
+            b.putDoubleArray("doubleArray", new double[]{18., i});
+            inputs[i] = b;
+        }
+        assertThat(inputs[0]).isNotEqualTo(inputs[1]);
+        assertThat(BundleUtil.deepEquals(inputs[0], inputs[1])).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_nestedNotEquals() {
+        Bundle one = new Bundle();
+        one.putString("a", "a");
+        Bundle two = new Bundle();
+        two.putBundle("b", one);
+        Bundle twoClone = new Bundle();
+        twoClone.putBundle("b", one);
+        Bundle three = new Bundle();
+        three.putBundle("b", two);
+
+        ArrayList<Bundle> listOne = new ArrayList<>(ImmutableList.of(one, two, three));
+        ArrayList<Bundle> listOneClone = new ArrayList<>(ImmutableList.of(one, twoClone, three));
+        ArrayList<Bundle> listTwo = new ArrayList<>(ImmutableList.of(one, three, two));
+        Bundle b1 = new Bundle();
+        b1.putParcelableArrayList("key", listOne);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putParcelableArrayList("key", listOneClone);
+        Bundle b2 = new Bundle();
+        b2.putParcelableArrayList("key", listTwo);
+
+        assertThat(b1).isNotEqualTo(b1Clone);
+        assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+        assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+        assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+    }
+
+    @Test
+    public void testDeepEquals_sparseArray() {
+        Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+        SparseArray<Parcelable> array1 = new SparseArray<>();
+        array1.put(1, parcelable1);
+        array1.put(10, parcelable2);
+
+        SparseArray<Parcelable> array1Clone = new SparseArray<>();
+        array1Clone.put(1, parcelable1);
+        array1Clone.put(10, parcelable2);
+
+        SparseArray<Parcelable> array2 = new SparseArray<>();
+        array2.put(1, parcelable1);
+        array2.put(10, parcelable3);  // Different
+
+        Bundle b1 = new Bundle();
+        b1.putSparseParcelableArray("array1", array1);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putSparseParcelableArray("array1", array1Clone);
+        Bundle b2 = new Bundle();
+        b2.putSparseParcelableArray("array1", array2);
+
+        assertThat(b1).isNotEqualTo(b1Clone);
+        assertThat(BundleUtil.deepEquals(b1, b1Clone)).isTrue();
+        assertThat(BundleUtil.deepEquals(b1, b2)).isFalse();
+        assertThat(BundleUtil.deepEquals(b1Clone, b2)).isFalse();
+    }
+
+    @Test
+    public void testDeepHashCode_same() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            inputs[i] = createThoroughBundle();
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
+    @Test
+    public void testDeepHashCode_different() {
+        Bundle[] inputs = new Bundle[2];
+        for (int i = 0; i < 2; i++) {
+            Bundle b = createThoroughBundle();
+            // Create a difference
+            assertThat(b.containsKey("doubleArray")).isTrue();
+            b.putDoubleArray("doubleArray", new double[]{18., i});
+            inputs[i] = b;
+        }
+        assertThat(BundleUtil.deepHashCode(inputs[0]))
+                .isNotEqualTo(BundleUtil.deepHashCode(inputs[1]));
+    }
+
+    @Test
+    public void testHashCode_sparseArray() {
+        Parcelable parcelable1 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable2 = new ParcelUuid(UUID.randomUUID());
+        Parcelable parcelable3 = new ParcelUuid(UUID.randomUUID());
+
+        SparseArray<Parcelable> array1 = new SparseArray<>();
+        array1.put(1, parcelable1);
+        array1.put(10, parcelable2);
+
+        SparseArray<Parcelable> array1Clone = new SparseArray<>();
+        array1Clone.put(1, parcelable1);
+        array1Clone.put(10, parcelable2);
+
+        SparseArray<Parcelable> array2 = new SparseArray<>();
+        array2.put(1, parcelable1);
+        array2.put(10, parcelable3);  // Different
+
+        Bundle b1 = new Bundle();
+        b1.putSparseParcelableArray("array1", array1);
+        Bundle b1Clone = new Bundle();
+        b1Clone.putSparseParcelableArray("array1", array1Clone);
+        Bundle b2 = new Bundle();
+        b2.putSparseParcelableArray("array1", array2);
+
+        assertThat(b1.hashCode()).isNotEqualTo(b1Clone.hashCode());
+        assertThat(BundleUtil.deepHashCode(b1)).isEqualTo(BundleUtil.deepHashCode(b1Clone));
+        assertThat(BundleUtil.deepHashCode(b1)).isNotEqualTo(BundleUtil.deepHashCode(b2));
+    }
+
+    private static Bundle createThoroughBundle() {
+        Bundle toy1 = new Bundle();
+        toy1.putString("a", "a");
+        Bundle toy2 = new Bundle();
+        toy2.putInt("b", 2);
+
+        Bundle b = new Bundle();
+        // BaseBundle stuff
+        b.putBoolean("boolean", true);
+        b.putByte("byte", (byte) 1);
+        b.putChar("char", 'a');
+        b.putShort("short", (short) 2);
+        b.putInt("int", 3);
+        b.putLong("long", 4L);
+        b.putFloat("float", 5f);
+        b.putDouble("double", 6f);
+        b.putString("string", "b");
+        b.putCharSequence("charSequence", "c");
+        b.putIntegerArrayList("integerArrayList", new ArrayList<>(ImmutableList.of(7, 8)));
+        b.putStringArrayList("stringArrayList", new ArrayList<>(ImmutableList.of("d", "e")));
+        b.putCharSequenceArrayList(
+                "charSequenceArrayList", new ArrayList<>(ImmutableList.of("f", "g")));
+        b.putSerializable("serializable", new BigDecimal(9));
+        b.putBooleanArray("booleanArray", new boolean[]{true, false, true});
+        b.putByteArray("byteArray", new byte[]{(byte) 10, (byte) 11});
+        b.putShortArray("shortArray", new short[]{(short) 12, (short) 13});
+        b.putCharArray("charArray", new char[]{'h', 'i'});
+        b.putLongArray("longArray", new long[]{14L, 15L});
+        b.putFloatArray("floatArray", new float[]{16f, 17f});
+        b.putDoubleArray("doubleArray", new double[]{18., 19.});
+        b.putStringArray("stringArray", new String[]{"j", "k"});
+        b.putCharSequenceArray("charSequenceArrayList", new CharSequence[]{"l", "m"});
+
+        // Bundle stuff
+        b.putParcelable("parcelable", toy1);
+        if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+            b.putSize("size", new Size(20, 21));
+            b.putSizeF("sizeF", new SizeF(22f, 23f));
+        }
+        b.putParcelableArray("parcelableArray", new Parcelable[]{toy1, toy2});
+        b.putParcelableArrayList(
+                "parcelableArrayList", new ArrayList<>(ImmutableList.of(toy1, toy2)));
+        SparseArray<Parcelable> sparseArray = new SparseArray<>();
+        sparseArray.put(24, toy1);
+        sparseArray.put(1025, toy2);
+        b.putSparseParcelableArray("sparceParcelableArray", sparseArray);
+        b.putBundle("bundle", toy1);
+
+        return b;
+    }
+}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
index 9d0460a..11b587b 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSchema.java
@@ -24,7 +24,9 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.IllegalSchemaException;
+import androidx.appsearch.util.BundleUtil;
 import androidx.collection.ArraySet;
+import androidx.core.util.ObjectsCompat;
 import androidx.core.util.Preconditions;
 
 import java.lang.annotation.Retention;
@@ -96,17 +98,37 @@
         return ret;
     }
 
+    @Override
+    public boolean equals(@Nullable Object other) {
+        if (this == other) {
+            return true;
+        }
+        if (!(other instanceof AppSearchSchema)) {
+            return false;
+        }
+        AppSearchSchema otherSchema = (AppSearchSchema) other;
+        if (!getSchemaType().equals(otherSchema.getSchemaType())) {
+            return false;
+        }
+        return getProperties().equals(otherSchema.getProperties());
+    }
+
+    @Override
+    public int hashCode() {
+        return ObjectsCompat.hash(getSchemaType(), getProperties());
+    }
+
     /** Builder for {@link AppSearchSchema objects}. */
     public static final class Builder {
-        private final String mTypeName;
+        private final String mSchemaType;
         private final ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
         private final Set<String> mPropertyNames = new ArraySet<>();
         private boolean mBuilt = false;
 
         /** Creates a new {@link AppSearchSchema.Builder}. */
-        public Builder(@NonNull String typeName) {
-            Preconditions.checkNotNull(typeName);
-            mTypeName = typeName;
+        public Builder(@NonNull String schemaType) {
+            Preconditions.checkNotNull(schemaType);
+            mSchemaType = schemaType;
         }
 
         /** Adds a property to the given type. */
@@ -135,7 +157,7 @@
         public AppSearchSchema build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             Bundle bundle = new Bundle();
-            bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mTypeName);
+            bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
             bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
             mBuilt = true;
             return new AppSearchSchema(bundle);
@@ -273,6 +295,9 @@
 
         final Bundle mBundle;
 
+        @Nullable
+        private Integer mHashCode;
+
         PropertyConfig(@NonNull Bundle bundle) {
             mBundle = Preconditions.checkNotNull(bundle);
         }
@@ -321,6 +346,26 @@
             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
         }
 
+        @Override
+        public boolean equals(@Nullable Object other) {
+            if (this == other) {
+                return true;
+            }
+            if (!(other instanceof PropertyConfig)) {
+                return false;
+            }
+            PropertyConfig otherProperty = (PropertyConfig) other;
+            return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
+        }
+
+        @Override
+        public int hashCode() {
+            if (mHashCode == null) {
+                mHashCode = BundleUtil.deepHashCode(mBundle);
+            }
+            return mHashCode;
+        }
+
         /**
          * Builder for {@link PropertyConfig}.
          *
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
index 671aa1d..311b515 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchSession.java
@@ -16,11 +16,14 @@
 // @exportToFramework:skipFile()
 package androidx.appsearch.app;
 
+import android.annotation.SuppressLint;
+
 import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.Set;
+
 /**
  * Represents a connection to an AppSearch storage system where {@link GenericDocument}s can be
  * placed and queried.
@@ -74,37 +77,29 @@
      * <p>It is a no-op to set the same schema as has been previously set; this is handled
      * efficiently.
      *
+     * <p>By default, documents are visible on platform surfaces. To opt out, call {@code
+     * SetSchemaRequest.Builder#setPlatformSurfaceable} with {@code surfaceable} as false. Any
+     * visibility settings apply only to the schemas that are included in the {@code request}.
+     * Visibility settings for a schema type do not apply or persist across
+     * {@link SetSchemaRequest}s.
+     *
      * @param request The schema update request.
      * @return The pending result of performing this operation.
      */
+    // TODO(b/169883602): Change @code references to @link when setPlatformSurfaceable APIs are
+    //  exposed.
     @NonNull
     ListenableFuture<AppSearchResult<Void>> setSchema(@NonNull SetSchemaRequest request);
 
     /**
-     * Sets visibility settings for documents in AppSearch.
+     * Retrieves the schema most recently successfully provided to {@link #setSchema}.
      *
-     * <p>Visibility settings are not carried over from previous {@code SetVisibilityRequest}s.
-     * The entire set of visibility settings must be specified on each {@code SetVisibilityRequest}.
-     *
-     * <p>The visibility settings apply to the schema instance that currently exists. If a schema
-     * is deleted and then re-added, the visibility setting will no longer apply to the new
-     * instance of the schema.
-     *
-     * <p>An {@link AppSearchResult#RESULT_NOT_FOUND} will be returned if a specified schema
-     * doesn't exist.
-     *
-     * <p>The default visibility settings are that all documents can be shown on platform
-     * surfaces. Documents can be opted out of being shown on platform surfaces by specifying
-     * their schema type in {@link SetVisibilityRequest.Builder#addHiddenFromPlatformSurfaces}.
-     *
-     * @param request The visibility settings request
      * @return The pending result of performing this operation.
-     * @hide
      */
-    // TODO(b/169883602): Add integration tests for this API in the platform.
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    // This call hits disk; async API prevents us from treating these calls as properties.
+    @SuppressLint("KotlinPropertyAccess")
     @NonNull
-    ListenableFuture<AppSearchResult<Void>> setVisibility(@NonNull SetVisibilityRequest request);
+    ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema();
 
     /**
      * Indexes documents into AppSearch.
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
index ca34d32..28357af 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/GenericDocument.java
@@ -25,6 +25,7 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.util.BundleUtil;
 import androidx.core.util.Preconditions;
 
 import java.lang.reflect.Array;
@@ -43,7 +44,7 @@
  * @see AppSearchSession#query
  */
 public class GenericDocument {
-    private static final String TAG = "GenericDocument";
+    private static final String TAG = "AppSearchGenericDocumen";
 
     /** The default empty namespace.*/
     public static final String DEFAULT_NAMESPACE = "";
@@ -480,142 +481,17 @@
             return false;
         }
         GenericDocument otherDocument = (GenericDocument) other;
-        return bundleEquals(this.mBundle, otherDocument.mBundle);
-    }
-
-    /**
-     * Deeply checks whether two bundles are equal.
-     * <p>Two bundles will be considered equal if they contain the same content.
-     */
-    @SuppressWarnings("unchecked")
-    private static boolean bundleEquals(Bundle one, Bundle two) {
-        if (one.size() != two.size()) {
-            return false;
-        }
-        Set<String> keySetOne = one.keySet();
-        Object valueOne;
-        Object valueTwo;
-        // Bundle inherit its equals() from Object.java, which only compare their memory address.
-        // We should iterate all keys and check their presents and values in both bundle.
-        for (String key : keySetOne) {
-            valueOne = one.get(key);
-            valueTwo = two.get(key);
-            if (valueOne instanceof Bundle
-                    && valueTwo instanceof Bundle
-                    && !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) {
-                return false;
-            } else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) {
-                // If we call bundle.get(key) when the 'key' doesn't actually exist in the
-                // bundle, we'll get back a null. So make sure that both values are null and
-                // both keys exist in the bundle.
-                return false;
-            } else if (valueOne instanceof boolean[]) {
-                if (!(valueTwo instanceof boolean[])
-                        || !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof long[]) {
-                if (!(valueTwo instanceof long[])
-                        || !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof double[]) {
-                if (!(valueTwo instanceof double[])
-                        || !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) {
-                    return false;
-                }
-            } else if (valueOne instanceof Bundle[]) {
-                if (!(valueTwo instanceof Bundle[])) {
-                    return false;
-                }
-                Bundle[] bundlesOne = (Bundle[]) valueOne;
-                Bundle[] bundlesTwo = (Bundle[]) valueTwo;
-                if (bundlesOne.length != bundlesTwo.length) {
-                    return false;
-                }
-                for (int i = 0; i < bundlesOne.length; i++) {
-                    if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) {
-                        return false;
-                    }
-                }
-            } else if (valueOne instanceof ArrayList) {
-                if (!(valueTwo instanceof ArrayList)) {
-                    return false;
-                }
-                ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne;
-                ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo;
-                if (bundlesOne.size() != bundlesTwo.size()) {
-                    return false;
-                }
-                for (int i = 0; i < bundlesOne.size(); i++) {
-                    if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) {
-                        return false;
-                    }
-                }
-            } else if (valueOne instanceof Object[]) {
-                if (!(valueTwo instanceof Object[])
-                        || !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) {
-                    return false;
-                }
-            }
-        }
-        return true;
+        return BundleUtil.deepEquals(this.mBundle, otherDocument.mBundle);
     }
 
     @Override
     public int hashCode() {
         if (mHashCode == null) {
-            mHashCode = bundleHashCode(mBundle);
+            mHashCode = BundleUtil.deepHashCode(mBundle);
         }
         return mHashCode;
     }
 
-    /**
-     * Calculates the hash code for a bundle.
-     * <p> The hash code is only effected by the contents in the bundle. Bundles will get
-     * consistent hash code if they have same contents.
-     */
-    @SuppressWarnings("unchecked")
-    private static int bundleHashCode(Bundle bundle) {
-        int[] hashCodes = new int[bundle.size()];
-        int i = 0;
-        // Bundle inherit its hashCode() from Object.java, which only relative to their memory
-        // address. Bundle doesn't have an order, so we should iterate all keys and combine
-        // their value's hashcode into an array. And use the hashcode of the array to be
-        // the hashcode of the bundle.
-        for (String key : bundle.keySet()) {
-            Object value = bundle.get(key);
-            if (value instanceof boolean[]) {
-                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
-            } else if (value instanceof long[]) {
-                hashCodes[i++] = Arrays.hashCode((long[]) value);
-            } else if (value instanceof double[]) {
-                hashCodes[i++] = Arrays.hashCode((double[]) value);
-            } else if (value instanceof String[]) {
-                hashCodes[i++] = Arrays.hashCode((Object[]) value);
-            } else if (value instanceof Bundle) {
-                hashCodes[i++] = bundleHashCode((Bundle) value);
-            } else if (value instanceof Bundle[]) {
-                Bundle[] bundles = (Bundle[]) value;
-                int[] innerHashCodes = new int[bundles.length];
-                for (int j = 0; j < innerHashCodes.length; j++) {
-                    innerHashCodes[j] = bundleHashCode(bundles[j]);
-                }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
-            } else if (value instanceof ArrayList) {
-                ArrayList<Bundle> bundles = (ArrayList<Bundle>) value;
-                int[] innerHashCodes = new int[bundles.size()];
-                for (int j = 0; j < innerHashCodes.length; j++) {
-                    innerHashCodes[j] = bundleHashCode(bundles.get(j));
-                }
-                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
-            } else {
-                hashCodes[i++] = value.hashCode();
-            }
-        }
-        return Arrays.hashCode(hashCodes);
-    }
-
     @Override
     @NonNull
     public String toString() {
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
index 2f8337c..a918106 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetSchemaRequest.java
@@ -19,6 +19,7 @@
 import android.annotation.SuppressLint;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
 import androidx.appsearch.exceptions.AppSearchException;
 import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
@@ -26,6 +27,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -36,17 +38,31 @@
  */
 public final class SetSchemaRequest {
     private final Set<AppSearchSchema> mSchemas;
+    private final Set<String> mSchemasNotPlatformSurfaceable;
     private final boolean mForceOverride;
 
-    SetSchemaRequest(Set<AppSearchSchema> schemas, boolean forceOverride) {
-        mSchemas = schemas;
+    SetSchemaRequest(@NonNull Set<AppSearchSchema> schemas,
+            @NonNull Set<String> schemasNotPlatformSurfaceable, boolean forceOverride) {
+        mSchemas = Preconditions.checkNotNull(schemas);
+        mSchemasNotPlatformSurfaceable = Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
         mForceOverride = forceOverride;
     }
 
     /** Returns the schemas that are part of this request. */
     @NonNull
     public Set<AppSearchSchema> getSchemas() {
-        return mSchemas;
+        return Collections.unmodifiableSet(mSchemas);
+    }
+
+    /**
+     * Returns the set of schema types that have opted out of being visible on system UI surfaces.
+     *
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    @NonNull
+    public Set<String> getSchemasNotPlatformSurfaceable() {
+        return Collections.unmodifiableSet(mSchemasNotPlatformSurfaceable);
     }
 
     /** Returns whether this request will force the schema to be overridden. */
@@ -57,17 +73,26 @@
     /** Builder for {@link SetSchemaRequest} objects. */
     public static final class Builder {
         private final Set<AppSearchSchema> mSchemas = new ArraySet<>();
+        private final Set<String> mSchemasNotPlatformSurfaceable = new ArraySet<>();
         private boolean mForceOverride = false;
         private boolean mBuilt = false;
 
-        /** Adds one or more types to the schema. */
+        /**
+         * Adds one or more types to the schema.
+         *
+         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         */
         @NonNull
         public Builder addSchema(@NonNull AppSearchSchema... schemas) {
             Preconditions.checkNotNull(schemas);
             return addSchema(Arrays.asList(schemas));
         }
 
-        /** Adds one or more types to the schema. */
+        /**
+         * Adds one or more types to the schema.
+         *
+         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         */
         @NonNull
         public Builder addSchema(@NonNull Collection<AppSearchSchema> schemas) {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
@@ -79,6 +104,8 @@
         /**
          * Adds one or more types to the schema.
          *
+         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         *
          * @param dataClasses classes annotated with
          *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
@@ -95,6 +122,8 @@
         /**
          * Adds one or more types to the schema.
          *
+         * <p>Any documents of these types will be visible on system UI surfaces by default.
+         *
          * @param dataClasses classes annotated with
          *                    {@link androidx.appsearch.annotation.AppSearchDocument}.
          * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
@@ -116,6 +145,78 @@
         }
 
         /**
+         * Sets visibility on system UI surfaces for schema types.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
+                @NonNull String... schemaTypes) {
+            Preconditions.checkNotNull(schemaTypes);
+            return this.setSchemaTypeVisibilityForSystemUi(visible, Arrays.asList(schemaTypes));
+        }
+
+        /**
+         * Sets visibility on system UI surfaces for schema types.
+         *
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setSchemaTypeVisibilityForSystemUi(boolean visible,
+                @NonNull Collection<String> schemaTypes) {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(schemaTypes);
+            if (visible) {
+                mSchemasNotPlatformSurfaceable.removeAll(schemaTypes);
+            } else {
+                mSchemasNotPlatformSurfaceable.addAll(schemaTypes);
+            }
+            return this;
+        }
+
+        /**
+         * Sets visibility on system UI surfaces for schema types.
+         *
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given data classes.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setDataClassVisibilityForSystemUi(boolean visible,
+                @NonNull Class<?>... dataClasses) throws AppSearchException {
+            Preconditions.checkNotNull(dataClasses);
+            return setDataClassVisibilityForSystemUi(visible, Arrays.asList(dataClasses));
+        }
+
+        /**
+         * Sets visibility on system UI surfaces for schema types.
+         *
+         * @throws AppSearchException if {@code androidx.appsearch.compiler.AppSearchCompiler}
+         *                            has not generated a schema for the given data classes.
+         * @hide
+         */
+        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+        @NonNull
+        public Builder setDataClassVisibilityForSystemUi(boolean visible,
+                @NonNull Collection<Class<?>> dataClasses) throws AppSearchException {
+            Preconditions.checkState(!mBuilt, "Builder has already been used");
+            Preconditions.checkNotNull(dataClasses);
+            DataClassFactoryRegistry registry = DataClassFactoryRegistry.getInstance();
+            for (Class<?> dataClass : dataClasses) {
+                DataClassFactory<?> factory = registry.getOrCreateFactory(dataClass);
+                if (visible) {
+                    mSchemasNotPlatformSurfaceable.remove(factory.getSchemaType());
+                } else {
+                    mSchemasNotPlatformSurfaceable.add(factory.getSchemaType());
+                }
+            }
+            return this;
+        }
+
+        /**
          * Configures the {@link SetSchemaRequest} to delete any existing documents that don't
          * follow the new schema.
          *
@@ -130,12 +231,34 @@
             return this;
         }
 
-        /** Builds a new {@link SetSchemaRequest}. */
+        /**
+         * Builds a new {@link SetSchemaRequest}.
+         *
+         * @throws IllegalArgumentException If schema types were referenced, but the
+         *                                  corresponding {@link AppSearchSchema} was never added.
+         */
         @NonNull
         public SetSchemaRequest build() {
             Preconditions.checkState(!mBuilt, "Builder has already been used");
             mBuilt = true;
-            return new SetSchemaRequest(mSchemas, mForceOverride);
+
+            // Verify that any schema types with visibility settings refer to a real schema.
+            // Create a copy because we're going to remove from the set for verification purposes.
+            Set<String> schemasNotPlatformSurfaceableCopy = new ArraySet<>(
+                    mSchemasNotPlatformSurfaceable);
+            for (AppSearchSchema schema : mSchemas) {
+                schemasNotPlatformSurfaceableCopy.remove(schema.getSchemaType());
+            }
+            if (!schemasNotPlatformSurfaceableCopy.isEmpty()) {
+                // We still have schema types that weren't seen in our mSchemas set. This means
+                // there wasn't a corresponding AppSearchSchema.
+                throw new IllegalArgumentException(
+                        "Schema types " + schemasNotPlatformSurfaceableCopy
+                                + " referenced, but were not added.");
+            }
+
+            return new SetSchemaRequest(mSchemas, mSchemasNotPlatformSurfaceable,
+                    mForceOverride);
         }
     }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java
deleted file mode 100644
index 291a735..0000000
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/app/SetVisibilityRequest.java
+++ /dev/null
@@ -1,78 +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.appsearch.app;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.collection.ArraySet;
-import androidx.core.util.Preconditions;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Set;
-
-/**
- * Encapsulates a request to update the visibility settings of an {@link AppSearchSession} database.
- *
- * @see AppSearchSession#setVisibility
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public final class SetVisibilityRequest {
-    private final Set<AppSearchSchema> mSchemasHiddenFromPlatformSurfaces;
-
-    SetVisibilityRequest(Set<AppSearchSchema> schemasHiddenFromPlatformSurfaces) {
-        mSchemasHiddenFromPlatformSurfaces = schemasHiddenFromPlatformSurfaces;
-    }
-
-    /** @hide */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-    @NonNull
-    public Set<AppSearchSchema> getSchemasHiddenFromPlatformSurfaces() {
-        return mSchemasHiddenFromPlatformSurfaces;
-    }
-
-    /** Builder for {@link SetVisibilityRequest} objects. */
-    public static final class Builder {
-        private final Set<AppSearchSchema> mSchemasHiddenFromPlatformSurfaces = new ArraySet<>();
-        private boolean mBuilt = false;
-
-        /** Set documents of type {@code schemas} to be hidden from platform surfaces. */
-        @NonNull
-        public Builder addHiddenFromPlatformSurfaces(@NonNull AppSearchSchema... schemas) {
-            Preconditions.checkNotNull(schemas);
-            return addHiddenFromPlatformSurfaces(Arrays.asList(schemas));
-        }
-
-        /** Set documents of type {@code schemas} to be hidden from platform surfaces. */
-        @NonNull
-        public Builder addHiddenFromPlatformSurfaces(@NonNull Collection<AppSearchSchema> schemas) {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            Preconditions.checkNotNull(schemas);
-            mSchemasHiddenFromPlatformSurfaces.addAll(schemas);
-            return this;
-        }
-
-        /** Builds a new {@link SetVisibilityRequest}. */
-        @NonNull
-        public SetVisibilityRequest build() {
-            Preconditions.checkState(!mBuilt, "Builder has already been used");
-            mBuilt = true;
-            return new SetVisibilityRequest(mSchemasHiddenFromPlatformSurfaces);
-        }
-    }
-}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
new file mode 100644
index 0000000..86de233
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/util/BundleUtil.java
@@ -0,0 +1,222 @@
+/*
+ * 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.appsearch.util;
+
+import android.os.Bundle;
+import android.util.SparseArray;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Utilities for working with {@link android.os.Bundle}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public final class BundleUtil {
+    private BundleUtil() {}
+
+    /**
+     * Deeply checks two bundles are equal or not.
+     *
+     * <p>Two bundles will be considered equal if they contain the same keys, and each value is also
+     * equal. Bundle values are compared using deepEquals.
+     */
+    public static boolean deepEquals(@Nullable Bundle one, @Nullable Bundle two) {
+        if (one == null && two == null) {
+            return true;
+        }
+        if (one == null || two == null) {
+            return false;
+        }
+        if (one.size() != two.size()) {
+            return false;
+        }
+        if (!one.keySet().equals(two.keySet())) {
+            return false;
+        }
+        // Bundle inherit its equals() from Object.java, which only compare their memory address.
+        // We should iterate all keys and check their presents and values in both bundle.
+        for (String key : one.keySet()) {
+            if (!bundleValueEquals(one.get(key), two.get(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Deeply checks whether two values in a Bundle are equal or not.
+     *
+     * <p>Values of type Bundle are compared using {@link #deepEquals}.
+     */
+    private static boolean bundleValueEquals(@Nullable Object one, @Nullable Object two) {
+        if (one == null && two == null) {
+            return true;
+        }
+        if (one == null || two == null) {
+            return false;
+        }
+        if (one.equals(two)) {
+            return true;
+        }
+        if (one instanceof Bundle && two instanceof Bundle) {
+            return deepEquals((Bundle) one, (Bundle) two);
+        } else if (one instanceof int[] && two instanceof int[]) {
+            return Arrays.equals((int[]) one, (int[]) two);
+        } else if (one instanceof byte[] && two instanceof byte[]) {
+            return Arrays.equals((byte[]) one, (byte[]) two);
+        } else if (one instanceof char[] && two instanceof char[]) {
+            return Arrays.equals((char[]) one, (char[]) two);
+        } else if (one instanceof long[] && two instanceof long[]) {
+            return Arrays.equals((long[]) one, (long[]) two);
+        } else if (one instanceof float[] && two instanceof float[]) {
+            return Arrays.equals((float[]) one, (float[]) two);
+        } else if (one instanceof short[] && two instanceof short[]) {
+            return Arrays.equals((short[]) one, (short[]) two);
+        } else if (one instanceof double[] && two instanceof double[]) {
+            return Arrays.equals((double[]) one, (double[]) two);
+        } else if (one instanceof boolean[] && two instanceof boolean[]) {
+            return Arrays.equals((boolean[]) one, (boolean[]) two);
+        } else if (one instanceof Object[] && two instanceof Object[]) {
+            Object[] arrayOne = (Object[]) one;
+            Object[] arrayTwo = (Object[]) two;
+            if (arrayOne.length != arrayTwo.length) {
+                return false;
+            }
+            if (Arrays.equals(arrayOne, arrayTwo)) {
+                return true;
+            }
+            for (int i = 0; i < arrayOne.length; i++) {
+                if (!bundleValueEquals(arrayOne[i], arrayTwo[i])) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (one instanceof ArrayList && two instanceof ArrayList) {
+            ArrayList<?> listOne = (ArrayList<?>) one;
+            ArrayList<?> listTwo = (ArrayList<?>) two;
+            if (listOne.size() != listTwo.size()) {
+                return false;
+            }
+            for (int i = 0; i < listOne.size(); i++) {
+                if (!bundleValueEquals(listOne.get(i), listTwo.get(i))) {
+                    return false;
+                }
+            }
+            return true;
+        } else if (one instanceof SparseArray && two instanceof SparseArray) {
+            SparseArray<?> arrayOne = (SparseArray<?>) one;
+            SparseArray<?> arrayTwo = (SparseArray<?>) two;
+            if (arrayOne.size() != arrayTwo.size()) {
+                return false;
+            }
+            for (int i = 0; i < arrayOne.size(); i++) {
+                if (arrayOne.keyAt(i) != arrayTwo.keyAt(i)
+                        || !bundleValueEquals(arrayOne.valueAt(i), arrayTwo.valueAt(i))) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Calculates the hash code for a bundle.
+     * <p> The hash code is only effected by the contents in the bundle. Bundles will get
+     * consistent hash code if they have same contents.
+     */
+    public static int deepHashCode(@Nullable Bundle bundle) {
+        if (bundle == null) {
+            return 0;
+        }
+        int[] hashCodes = new int[bundle.size()];
+        int i = 0;
+        // Bundle inherit its hashCode() from Object.java, which only relative to their memory
+        // address. Bundle doesn't have an order, so we should iterate all keys and combine
+        // their value's hashcode into an array. And use the hashcode of the array to be
+        // the hashcode of the bundle.
+        for (String key : bundle.keySet()) {
+            Object value = bundle.get(key);
+            if (value instanceof Bundle) {
+                hashCodes[i++] = deepHashCode((Bundle) value);
+            } else if (value instanceof int[]) {
+                hashCodes[i++] = Arrays.hashCode((int[]) value);
+            } else if (value instanceof byte[]) {
+                hashCodes[i++] = Arrays.hashCode((byte[]) value);
+            } else if (value instanceof char[]) {
+                hashCodes[i++] = Arrays.hashCode((char[]) value);
+            } else if (value instanceof long[]) {
+                hashCodes[i++] = Arrays.hashCode((long[]) value);
+            } else if (value instanceof float[]) {
+                hashCodes[i++] = Arrays.hashCode((float[]) value);
+            } else if (value instanceof short[]) {
+                hashCodes[i++] = Arrays.hashCode((short[]) value);
+            } else if (value instanceof double[]) {
+                hashCodes[i++] = Arrays.hashCode((double[]) value);
+            } else if (value instanceof boolean[]) {
+                hashCodes[i++] = Arrays.hashCode((boolean[]) value);
+            } else if (value instanceof String[]) {
+                // Optimization to avoid Object[] handler creating an inner array for common cases
+                hashCodes[i++] = Arrays.hashCode((String[]) value);
+            } else if (value instanceof Object[]) {
+                Object[] array = (Object[]) value;
+                int[] innerHashCodes = new int[array.length];
+                for (int j = 0; j < array.length; j++) {
+                    if (array[j] instanceof Bundle) {
+                        innerHashCodes[j] = deepHashCode((Bundle) array[j]);
+                    } else if (array[j] != null) {
+                        innerHashCodes[j] = array[j].hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else if (value instanceof ArrayList) {
+                ArrayList<?> list = (ArrayList<?>) value;
+                int[] innerHashCodes = new int[list.size()];
+                for (int j = 0; j < innerHashCodes.length; j++) {
+                    Object item = list.get(j);
+                    if (item instanceof Bundle) {
+                        innerHashCodes[j] = deepHashCode((Bundle) item);
+                    } else if (item != null) {
+                        innerHashCodes[j] = item.hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else if (value instanceof SparseArray) {
+                SparseArray<?> array = (SparseArray<?>) value;
+                int[] innerHashCodes = new int[array.size() * 2];
+                for (int j = 0; j < array.size(); j++) {
+                    innerHashCodes[j * 2] = array.keyAt(j);
+                    Object item = array.valueAt(j);
+                    if (item instanceof Bundle) {
+                        innerHashCodes[j * 2 + 1] = deepHashCode((Bundle) item);
+                    } else if (item != null) {
+                        innerHashCodes[j * 2 + 1] = item.hashCode();
+                    }
+                }
+                hashCodes[i++] = Arrays.hashCode(innerHashCodes);
+            } else {
+                hashCodes[i++] = value.hashCode();
+            }
+        }
+        return Arrays.hashCode(hashCodes);
+    }
+}
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index 3f4c746..cad539b 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -64,12 +64,13 @@
                         VisibilityStore.DATABASE_NAME + AppSearchImpl.DATABASE_DELIMITER
                                 + VisibilityStore.SCHEMA_TYPE)
                         .addProperty(new AppSearchSchema.PropertyConfig.Builder(
-                                VisibilityStore.PLATFORM_HIDDEN_PROPERTY)
+                                VisibilityStore.NOT_PLATFORM_SURFACEABLE_PROPERTY)
                                 .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
                                 .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                                 .build())
                         .build();
-        mVisibilitySchemaProto = SchemaToProtoConverter.convert(visibilityAppSearchSchema);
+        mVisibilitySchemaProto =
+                SchemaToProtoConverter.toSchemaTypeConfigProto(visibilityAppSearchSchema);
     }
 
     /**
@@ -280,7 +281,8 @@
         // Insert schema
         Set<AppSearchSchema> schemas =
                 Collections.singleton(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Insert enough documents.
         for (int i = 0; i < AppSearchImpl.OPTIMIZE_THRESHOLD_DOC_COUNT
@@ -327,7 +329,8 @@
         // Insert schema
         Set<AppSearchSchema> schemas =
                 Collections.singleton(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema("database", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Insert document
         GenericDocument document = new GenericDocument.Builder("uri", "type").setNamespace(
@@ -351,8 +354,10 @@
         Set<AppSearchSchema> schemas = Set.of(
                 new AppSearchSchema.Builder("typeA").build(),
                 new AppSearchSchema.Builder("typeB").build());
-        mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
-        mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Insert documents
         GenericDocument document1 = new GenericDocument.Builder("uri", "typeA").setNamespace(
@@ -415,7 +420,8 @@
         Set<AppSearchSchema> schemas =
                 Collections.singleton(new AppSearchSchema.Builder("Email").build());
         // Set schema Email to AppSearch database1
-        mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -432,21 +438,22 @@
     @Test
     public void testSetSchema_existingSchemaRetainsVisibilitySetting() throws Exception {
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "schema1").build()), /*forceOverride=*/false);
-        mAppSearchImpl.setVisibility("database", Set.of("schema1"));
+                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.singleton("schema1"), /*forceOverride=*/ false);
 
         // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/schema1");
 
         // Add a new schema, and include the already-existing "schema1"
         mAppSearchImpl.setSchema("database", Set.of(new AppSearchSchema.Builder(
-                "schema1").build(), new AppSearchSchema.Builder(
-                "schema2").build()), /*forceOverride=*/false);
+                        "schema1").build(), new AppSearchSchema.Builder(
+                        "schema2").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.singleton("schema1"), /*forceOverride=*/ false);
 
         // Check that "schema1" is still platform hidden, but "schema2" is the default platform
         // visible.
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/schema1");
     }
 
@@ -456,7 +463,8 @@
         schemas.add(new AppSearchSchema.Builder("Email").build());
         schemas.add(new AppSearchSchema.Builder("Document").build());
         // Set schema Email and Document to AppSearch database1
-        mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -475,12 +483,15 @@
                 "Email").build());
         // Check the incompatible error has been thrown.
         AppSearchException e = assertThrows(AppSearchException.class, () ->
-                mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/false));
+                mAppSearchImpl.setSchema("database1",
+                        finalSchemas, /*schemasNotPlatformSurfaceable=*/
+                        Collections.emptySet(), /*forceOverride=*/ false));
         assertThat(e).hasMessageThat().contains("Schema is incompatible");
         assertThat(e).hasMessageThat().contains("Deleted types: [database1/Document]");
 
         // ForceOverride to delete.
-        mAppSearchImpl.setSchema("database1", finalSchemas, /*forceOverride=*/true);
+        mAppSearchImpl.setSchema("database1", finalSchemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ true);
 
         // Check Document schema is removed.
         expectedProto = SchemaProto.newBuilder()
@@ -502,8 +513,10 @@
         schemas.add(new AppSearchSchema.Builder("Document").build());
 
         // Set schema Email and Document to AppSearch database1 and 2
-        mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/false);
-        mAppSearchImpl.setSchema("database2", schemas, /*forceOverride=*/false);
+        mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
+        mAppSearchImpl.setSchema("database2", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
 
         // Create expected schemaType proto.
         SchemaProto expectedProto = SchemaProto.newBuilder()
@@ -522,7 +535,8 @@
 
         // Save only Email to database1 this time.
         schemas = Collections.singleton(new AppSearchSchema.Builder("Email").build());
-        mAppSearchImpl.setSchema("database1", schemas, /*forceOverride=*/true);
+        mAppSearchImpl.setSchema("database1", schemas, /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ true);
 
         // Create expected schemaType list, database 1 should only contain Email but database 2
         // remains in same.
@@ -544,67 +558,60 @@
     @Test
     public void testRemoveSchema_removedFromVisibilityStore() throws Exception {
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "schema1").build()), /*forceOverride=*/false);
-        mAppSearchImpl.setVisibility("database", Set.of("schema1"));
+                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.singleton("schema1"), /*forceOverride=*/ false);
 
         // "schema1" is platform hidden now
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/schema1");
 
         // Remove "schema1" by force overriding
-        mAppSearchImpl.setSchema("database", Collections.emptySet(), /*forceOverride=*/true);
+        mAppSearchImpl.setSchema("database",
+                Collections.emptySet(), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ true);
 
         // Check that "schema1" is no longer considered platform hidden
         assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+                mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                         "database")).isEmpty();
 
         // Add "schema1" back, it gets default visibility settings which means it's not platform
         // hidden.
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "schema1").build()), /*forceOverride=*/false);
+                        "schema1").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
         assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+                mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                         "database")).isEmpty();
     }
 
     @Test
-    public void testSetVisibility_defaultPlatformVisible() throws Exception {
+    public void testSetSchema_defaultPlatformVisible() throws Exception {
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "Schema").build()), /*forceOverride=*/false);
+                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
         assertThat(
-                mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+                mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                         "database")).isEmpty();
     }
 
     @Test
-    public void testSetVisibility_platformHidden() throws Exception {
+    public void testSetSchema_platformHidden() throws Exception {
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "Schema").build()), /*forceOverride=*/false);
-        mAppSearchImpl.setVisibility("database", Set.of("Schema"));
-        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getPlatformHiddenSchemas(
+                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.singleton("Schema"), /*forceOverride=*/ false);
+        assertThat(mAppSearchImpl.getVisibilityStoreLocked().getSchemasNotPlatformSurfaceable(
                 "database")).containsExactly("database/Schema");
     }
 
     @Test
-    public void testSetVisibility_unknownSchema() throws Exception {
-        mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "Schema").build()), /*forceOverride=*/false);
-
-        // We'll throw an exception if a client tries to set visibility on a schema we don't know
-        // about.
-        AppSearchException e = assertThrows(AppSearchException.class,
-                () -> mAppSearchImpl.setVisibility("database", Set.of("UnknownSchema")));
-        assertThat(e).hasMessageThat().contains("Unknown schema(s)");
-    }
-
-    @Test
     public void testHasSchemaType() throws Exception {
         // Nothing exists yet
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isFalse();
 
         mAppSearchImpl.setSchema("database", Collections.singleton(new AppSearchSchema.Builder(
-                "Schema").build()), /*forceOverride=*/false);
+                        "Schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "Schema")).isTrue();
 
         assertThat(mAppSearchImpl.hasSchemaTypeLocked("database", "UnknownSchema")).isFalse();
@@ -618,13 +625,15 @@
 
         // Has database1
         mAppSearchImpl.setSchema("database1", Collections.singleton(new AppSearchSchema.Builder(
-                "schema").build()), /*forceOverride=*/false);
+                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
                 VisibilityStore.DATABASE_NAME, "database1");
 
         // Has both databases
         mAppSearchImpl.setSchema("database2", Collections.singleton(new AppSearchSchema.Builder(
-                "schema").build()), /*forceOverride=*/false);
+                        "schema").build()), /*schemasNotPlatformSurfaceable=*/
+                Collections.emptySet(), /*forceOverride=*/ false);
         assertThat(mAppSearchImpl.getDatabasesLocked()).containsExactly(
                 VisibilityStore.DATABASE_NAME, "database1", "database2");
     }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
index 2113859..d4a30f4 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/VisibilityStoreTest.java
@@ -42,35 +42,19 @@
     @Test
     public void testSetVisibility() throws Exception {
         mVisibilityStore.setVisibility(
-                "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2"));
-        assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
+                "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema2"));
+        assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
                 .containsExactly("schema1", "schema2");
 
         // New .setVisibility() call completely overrides previous visibility settings. So
         // "schema1" isn't preserved.
         mVisibilityStore.setVisibility(
-                "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema3"));
-        assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
+                "database", /*schemasNotPlatformSurfaceable=*/ Set.of("schema1", "schema3"));
+        assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database"))
                 .containsExactly("schema1", "schema3");
 
         mVisibilityStore.setVisibility(
-                "database", /*platformHiddenSchemas=*/ Collections.emptySet());
-        assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty();
+                "database", /*schemasNotPlatformSurfaceable=*/ Collections.emptySet());
+        assertThat(mVisibilityStore.getSchemasNotPlatformSurfaceable("database")).isEmpty();
     }
-
-    @Test
-    public void testRemoveSchemas() throws Exception {
-        mVisibilityStore.setVisibility(
-                "database", /*platformHiddenSchemas=*/ Set.of("schema1", "schema2"));
-
-        // Removed just schema1
-        mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema1"));
-        assertThat(mVisibilityStore.getPlatformHiddenSchemas("database"))
-                .containsExactly("schema2");
-
-        // Removed everything now
-        mVisibilityStore.updateSchemas("database", /*schemasToRemove=*/ Set.of("schema2"));
-        assertThat(mVisibilityStore.getPlatformHiddenSchemas("database")).isEmpty();
-    }
-
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
index 4fa010f..1245262 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverterTest.java
@@ -84,21 +84,20 @@
                         .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_1))
                         .addBytesValues(ByteString.copyFrom(BYTE_ARRAY_2)));
         propertyProtoMap.put("documentKey1",
-                PropertyProto.newBuilder().setName("documentKey1")
-                        .addDocumentValues(
-                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_1)));
+                PropertyProto.newBuilder().setName("documentKey1").addDocumentValues(
+                        GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_1)));
         propertyProtoMap.put("documentKey2",
-                PropertyProto.newBuilder().setName("documentKey2")
-                        .addDocumentValues(
-                                GenericDocumentToProtoConverter.convert(DOCUMENT_PROPERTIES_2)));
+                PropertyProto.newBuilder().setName("documentKey2").addDocumentValues(
+                        GenericDocumentToProtoConverter.toDocumentProto(DOCUMENT_PROPERTIES_2)));
         List<String> sortedKey = new ArrayList<>(propertyProtoMap.keySet());
         Collections.sort(sortedKey);
         for (String key : sortedKey) {
             documentProtoBuilder.addProperties(propertyProtoMap.get(key));
         }
         DocumentProto documentProto = documentProtoBuilder.build();
-        assertThat(GenericDocumentToProtoConverter.convert(document))
+        assertThat(GenericDocumentToProtoConverter.toDocumentProto(document))
                 .isEqualTo(documentProto);
-        assertThat(document).isEqualTo(GenericDocumentToProtoConverter.convert(documentProto));
+        assertThat(document)
+                .isEqualTo(GenericDocumentToProtoConverter.toGenericDocument(documentProto));
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
index 0fb39db..889b0c7 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverterTest.java
@@ -69,7 +69,10 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.convert(emailSchema)).isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(emailSchema))
+                .isEqualTo(expectedEmailProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedEmailProto))
+                .isEqualTo(emailSchema);
     }
 
     @Test
@@ -113,7 +116,9 @@
                         )
                 ).build();
 
-        assertThat(SchemaToProtoConverter.convert(musicRecordingSchema))
+        assertThat(SchemaToProtoConverter.toSchemaTypeConfigProto(musicRecordingSchema))
                 .isEqualTo(expectedMusicRecordingProto);
+        assertThat(SchemaToProtoConverter.toAppSearchSchema(expectedMusicRecordingProto))
+                .isEqualTo(musicRecordingSchema);
     }
 }
diff --git a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index d7ecf7b..9ffdeb9 100644
--- a/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -77,7 +77,7 @@
 
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
             SearchResult.MatchInfo match = result.getMatches().get(0);
             assertThat(match.getPropertyPath()).isEqualTo(propertyKeyString);
@@ -124,7 +124,7 @@
                 .build();
 
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
             assertThat(result.getMatches()).isEmpty();
         }
@@ -186,7 +186,7 @@
 
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage =
-                SearchResultToProtoConverter.convertToSearchResultPage(searchResultProto);
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto);
         for (SearchResult result : searchResultPage.getResults()) {
 
             SearchResult.MatchInfo match1 = result.getMatches().get(0);
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 545b0d0..2ec2d82 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
@@ -223,13 +223,17 @@
      *
      * <p>This method belongs to mutate group.
      *
-     * @param databaseName  The name of the database where this schema lives.
-     * @param schemas       Schemas to set for this app.
-     * @param forceOverride Whether to force-apply the schema even if it is incompatible. Documents
-     *                      which do not comply with the new schema will be deleted.
+     * @param databaseName                  The name of the database where this schema lives.
+     * @param schemas                       Schemas to set for this app.
+     * @param schemasNotPlatformSurfaceable Schema types that should not be surfaced on platform
+     *                                      surfaces.
+     * @param forceOverride                 Whether to force-apply the schema even if it is
+     *                                      incompatible. Documents
+     *                                      which do not comply with the new schema will be deleted.
      * @throws AppSearchException on IcingSearchEngine error.
      */
     public void setSchema(@NonNull String databaseName, @NonNull Set<AppSearchSchema> schemas,
+            @NonNull Set<String> schemasNotPlatformSurfaceable,
             boolean forceOverride) throws AppSearchException {
         mReadWriteLock.writeLock().lock();
         try {
@@ -237,7 +241,8 @@
 
             SchemaProto.Builder newSchemaBuilder = SchemaProto.newBuilder();
             for (AppSearchSchema schema : schemas) {
-                SchemaTypeConfigProto schemaTypeProto = SchemaToProtoConverter.convert(schema);
+                SchemaTypeConfigProto schemaTypeProto =
+                        SchemaToProtoConverter.toSchemaTypeConfigProto(schema);
                 newSchemaBuilder.addTypes(schemaTypeProto);
             }
 
@@ -272,8 +277,15 @@
 
             // Update derived data structures.
             mSchemaMapLocked.put(databaseName, rewrittenSchemaResults.mRewrittenQualifiedTypes);
-            mVisibilityStoreLocked.updateSchemas(databaseName,
-                    rewrittenSchemaResults.mDeletedQualifiedTypes);
+
+            String databasePrefix = getDatabasePrefix(databaseName);
+            Set<String> qualifiedSchemasNotPlatformSurfaceable =
+                    new ArraySet<>(schemasNotPlatformSurfaceable.size());
+            for (String schema : schemasNotPlatformSurfaceable) {
+                qualifiedSchemasNotPlatformSurfaceable.add(databasePrefix + schema);
+            }
+            mVisibilityStoreLocked.setVisibility(databaseName,
+                    qualifiedSchemasNotPlatformSurfaceable);
 
             // Determine whether to schedule an immediate optimize.
             if (setSchemaResultProto.getDeletedSchemaTypesCount() > 0
@@ -290,38 +302,53 @@
     }
 
     /**
-     * Update the visibility settings for this app.
+     * Retrieves the AppSearch schema for this database.
      *
-     * <p>This method belongs to the mutate group
+     * <p>This method belongs to query group.
      *
-     * @param databaseName                      The name of the database where the
-     *                                          visibility settings will apply.
-     * @param schemasHiddenFromPlatformSurfaces Schemas that should be hidden from platform
-     *                                          surfaces
-     * @throws AppSearchException on IcingSearchEngine error
+     * @param databaseName  The name of the database where this schema lives.
+     * @throws AppSearchException on IcingSearchEngine error.
      */
-    public void setVisibility(@NonNull String databaseName,
-            @NonNull Set<String> schemasHiddenFromPlatformSurfaces)
-            throws AppSearchException {
-        mReadWriteLock.writeLock().lock();
+    @NonNull
+    public Set<AppSearchSchema> getSchema(@NonNull String databaseName) throws AppSearchException {
+        SchemaProto fullSchema;
+        mReadWriteLock.readLock().lock();
         try {
-            String databasePrefix = getDatabasePrefix(databaseName);
-            Set<String> qualifiedSchemasHiddenFromPlatformSurface =
-                    new ArraySet<>(schemasHiddenFromPlatformSurfaces.size());
-            for (String schema : schemasHiddenFromPlatformSurfaces) {
-                Set<String> existingSchemas = mSchemaMapLocked.get(databaseName);
-                if (existingSchemas == null || !existingSchemas.contains(databasePrefix + schema)) {
-                    throw new AppSearchException(AppSearchResult.RESULT_NOT_FOUND,
-                            "Unknown schema(s): " + schemasHiddenFromPlatformSurfaces
-                                    + " provided during setVisibility.");
-                }
-                qualifiedSchemasHiddenFromPlatformSurface.add(databasePrefix + schema);
-            }
-            mVisibilityStoreLocked.setVisibility(databaseName,
-                    qualifiedSchemasHiddenFromPlatformSurface);
+            fullSchema = getSchemaProtoLocked();
         } finally {
-            mReadWriteLock.writeLock().lock();
+            mReadWriteLock.readLock().unlock();
         }
+
+        Set<AppSearchSchema> result = new ArraySet<>();
+        for (int i = 0; i < fullSchema.getTypesCount(); i++) {
+            String typeDatabase = getDatabaseName(fullSchema.getTypes(i).getSchemaType());
+            if (!databaseName.equals(typeDatabase)) {
+                continue;
+            }
+            // Rewrite SchemaProto.types.schema_type
+            SchemaTypeConfigProto.Builder typeConfigBuilder = fullSchema.getTypes(i).toBuilder();
+            String newSchemaType =
+                    typeConfigBuilder.getSchemaType().substring(databaseName.length() + 1);
+            typeConfigBuilder.setSchemaType(newSchemaType);
+
+            // Rewrite SchemaProto.types.properties.schema_type
+            for (int propertyIdx = 0;
+                    propertyIdx < typeConfigBuilder.getPropertiesCount();
+                    propertyIdx++) {
+                PropertyConfigProto.Builder propertyConfigBuilder =
+                        typeConfigBuilder.getProperties(propertyIdx).toBuilder();
+                if (!propertyConfigBuilder.getSchemaType().isEmpty()) {
+                    String newPropertySchemaType = propertyConfigBuilder.getSchemaType()
+                            .substring(databaseName.length() + 1);
+                    propertyConfigBuilder.setSchemaType(newPropertySchemaType);
+                    typeConfigBuilder.setProperties(propertyIdx, propertyConfigBuilder);
+                }
+            }
+
+            AppSearchSchema schema = SchemaToProtoConverter.toAppSearchSchema(typeConfigBuilder);
+            result.add(schema);
+        }
+        return result;
     }
 
     /**
@@ -335,7 +362,7 @@
      */
     public void putDocument(@NonNull String databaseName, @NonNull GenericDocument document)
             throws AppSearchException {
-        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.convert(
+        DocumentProto.Builder documentBuilder = GenericDocumentToProtoConverter.toDocumentProto(
                 document).toBuilder();
         addPrefixToDocument(documentBuilder, getDatabasePrefix(databaseName));
 
@@ -379,7 +406,7 @@
 
         DocumentProto.Builder documentBuilder = getResultProto.getDocument().toBuilder();
         removeDatabasesFromDocument(documentBuilder);
-        return GenericDocumentToProtoConverter.convert(documentBuilder.build());
+        return GenericDocumentToProtoConverter.toGenericDocument(documentBuilder.build());
     }
 
     /**
@@ -961,7 +988,7 @@
                 resultsBuilder.setResults(i, resultBuilder);
             }
         }
-        return SearchResultToProtoConverter.convertToSearchResultPage(resultsBuilder);
+        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder);
     }
 
     @GuardedBy("mReadWriteLock")
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
index df21825..12b25fa 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/SearchSessionImpl.java
@@ -30,9 +30,7 @@
 import androidx.appsearch.app.SearchResults;
 import androidx.appsearch.app.SearchSpec;
 import androidx.appsearch.app.SetSchemaRequest;
-import androidx.appsearch.app.SetVisibilityRequest;
 import androidx.appsearch.localstorage.util.FutureUtil;
-import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import com.google.common.util.concurrent.ListenableFuture;
@@ -69,7 +67,8 @@
         return execute(() -> {
             try {
                 mAppSearchImpl.setSchema(
-                        mDatabaseName, request.getSchemas(), request.isForceOverride());
+                        mDatabaseName, request.getSchemas(),
+                        request.getSchemasNotPlatformSurfaceable(), request.isForceOverride());
                 return AppSearchResult.newSuccessfulResult(/*value=*/ null);
             } catch (Throwable t) {
                 return throwableToFailedResult(t);
@@ -79,21 +78,10 @@
 
     @Override
     @NonNull
-    public ListenableFuture<AppSearchResult<Void>> setVisibility(
-            @NonNull SetVisibilityRequest request) {
-        Preconditions.checkNotNull(request);
+    public ListenableFuture<AppSearchResult<Set<AppSearchSchema>>> getSchema() {
         return execute(() -> {
             try {
-                Set<AppSearchSchema> appSearchSchemasHiddenFromPlatformSurfaces =
-                        request.getSchemasHiddenFromPlatformSurfaces();
-                Set<String> schemasHiddenFromPlatformSurfaces =
-                        new ArraySet<>(appSearchSchemasHiddenFromPlatformSurfaces.size());
-                for (AppSearchSchema schema : appSearchSchemasHiddenFromPlatformSurfaces) {
-                    schemasHiddenFromPlatformSurfaces.add(schema.getSchemaType());
-                }
-
-                mAppSearchImpl.setVisibility(mDatabaseName, schemasHiddenFromPlatformSurfaces);
-                return AppSearchResult.newSuccessfulResult(/*value=*/ null);
+                return AppSearchResult.newSuccessfulResult(mAppSearchImpl.getSchema(mDatabaseName));
             } catch (Throwable t) {
                 return throwableToFailedResult(t);
             }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
index feb32db..a95231f 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/VisibilityStore.java
@@ -53,21 +53,24 @@
     // Schema type for documents that hold AppSearch's metadata, e.g. visibility settings
     @VisibleForTesting
     static final String SCHEMA_TYPE = "Visibility";
+
     // Property that holds the list of platform-hidden schemas, as part of the visibility
     // settings.
     @VisibleForTesting
-    static final String PLATFORM_HIDDEN_PROPERTY = "platformHidden";
+    static final String NOT_PLATFORM_SURFACEABLE_PROPERTY = "notPlatformSurfaceable";
     // Database name to prefix all visibility schemas and documents with. Special-cased to
     // minimize the chance of collision with a client-supplied database.
+
     @VisibleForTesting
     static final String DATABASE_NAME = "$$__AppSearch__Database";
+
     // Namespace of documents that contain visibility settings
     private static final String NAMESPACE = "namespace";
     private final AppSearchImpl mAppSearchImpl;
 
     // The map contains schemas that are platform-hidden for each database. All schemas in the map
     // have a database name prefix.
-    private final Map<String, Set<String>> mPlatformHiddenMap = new ArrayMap<>();
+    private final Map<String, Set<String>> mNotPlatformSurfaceableMap = new ArrayMap<>();
 
     /**
      * Creates an uninitialized VisibilityStore object. Callers must also call {@link #initialize()}
@@ -84,7 +87,8 @@
      *
      * <p>This is kept separate from the constructor because this will call methods on
      * AppSearchImpl. Some may even then recursively call back into VisibilityStore (for example,
-     * {@link AppSearchImpl#setSchema} will call {@link #updateSchemas}. We need to have both
+     * {@link AppSearchImpl#setSchema} will call {@link #setVisibility(String, Set)}. We need to
+     * have both
      * AppSearchImpl and VisibilityStore fully initialized for this call flow to work.
      *
      * @throws AppSearchException AppSearchException on AppSearchImpl error.
@@ -95,12 +99,13 @@
             mAppSearchImpl.setSchema(DATABASE_NAME,
                     Collections.singleton(new AppSearchSchema.Builder(SCHEMA_TYPE)
                             .addProperty(new AppSearchSchema.PropertyConfig.Builder(
-                                    PLATFORM_HIDDEN_PROPERTY)
+                                    NOT_PLATFORM_SURFACEABLE_PROPERTY)
                                     .setDataType(AppSearchSchema.PropertyConfig.DATA_TYPE_STRING)
                                     .setCardinality(
                                             AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED)
                                     .build())
                             .build()),
+                    /*schemasNotPlatformSurfaceable=*/ Collections.emptySet(),
                     /*forceOverride=*/ false);
         }
 
@@ -116,8 +121,9 @@
                 GenericDocument document = mAppSearchImpl.getDocument(
                         DATABASE_NAME, NAMESPACE, /*uri=*/ database);
 
-                String[] schemas = document.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY);
-                mPlatformHiddenMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
+                String[] schemas = document.getPropertyStringArray(
+                        NOT_PLATFORM_SURFACEABLE_PROPERTY);
+                mNotPlatformSurfaceableMap.put(database, new ArraySet<>(Arrays.asList(schemas)));
             } catch (AppSearchException e) {
                 if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
                     // TODO(b/172068212): This indicates some desync error. We were expecting a
@@ -132,91 +138,33 @@
     }
 
     /**
-     * Update visibility settings for the {@code databaseName}.
-     *
-     * @param schemasToRemove Database-prefixed schemas that should be removed
-     */
-    public void updateSchemas(@NonNull String databaseName,
-            @NonNull Set<String> schemasToRemove) throws AppSearchException {
-        Preconditions.checkNotNull(databaseName);
-        Preconditions.checkNotNull(schemasToRemove);
-
-        GenericDocument visibilityDocument;
-        try {
-            visibilityDocument = mAppSearchImpl.getDocument(
-                    DATABASE_NAME, NAMESPACE, /*uri=*/ databaseName);
-        } catch (AppSearchException e) {
-            if (e.getResultCode() == AppSearchResult.RESULT_NOT_FOUND) {
-                // This might be the first time we're seeing visibility changes for a database.
-                // Create a new visibility document.
-                mAppSearchImpl.putDocument(DATABASE_NAME, new GenericDocument.Builder(
-                        /*uri=*/ databaseName, SCHEMA_TYPE)
-                        .setNamespace(NAMESPACE).build());
-
-                // Since we know there was nothing that existed before, we don't need to remove
-                // anything either. Return early.
-                return;
-            }
-            // Otherwise, this is some real error we should pass up.
-            throw e;
-        }
-
-        String[] hiddenSchemas =
-                visibilityDocument.getPropertyStringArray(PLATFORM_HIDDEN_PROPERTY);
-        if (hiddenSchemas == null) {
-            // Nothing to remove.
-            return;
-        }
-
-        // Create a new set so we can remove from it.
-        Set<String> remainingSchemas = new ArraySet<>(Arrays.asList(hiddenSchemas));
-        boolean changed = remainingSchemas.removeAll(schemasToRemove);
-        if (!changed) {
-            // Nothing was actually removed. Can return early.
-            return;
-        }
-
-        // Update our persisted document
-        // TODO(b/171882200): Switch to a .toBuilder API when it's available.
-        GenericDocument.Builder newVisibilityDocument = new GenericDocument.Builder(
-                /*uri=*/ databaseName, SCHEMA_TYPE)
-                .setNamespace(NAMESPACE);
-        if (!remainingSchemas.isEmpty()) {
-            newVisibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY,
-                    remainingSchemas.toArray(new String[0]));
-        }
-        mAppSearchImpl.putDocument(DATABASE_NAME, newVisibilityDocument.build());
-
-        // Update derived data structures
-        mPlatformHiddenMap.put(databaseName, remainingSchemas);
-    }
-
-    /**
      * Sets visibility settings for {@code databaseName}. Any previous visibility settings will be
      * overwritten.
      *
-     * @param databaseName          Database name that owns the {@code platformHiddenSchemas}.
-     * @param platformHiddenSchemas Set of database-qualified schemas that should be hidden from
-     *                              the platform.
+     * @param databaseName                  Database name that owns the {@code
+     *                                      schemasNotPlatformSurfaceable}.
+     * @param schemasNotPlatformSurfaceable Set of database-qualified schemas that should be
+     *                                      hidden from
+     *                                      the platform.
      * @throws AppSearchException on AppSearchImpl error.
      */
     public void setVisibility(@NonNull String databaseName,
-            @NonNull Set<String> platformHiddenSchemas) throws AppSearchException {
+            @NonNull Set<String> schemasNotPlatformSurfaceable) throws AppSearchException {
         Preconditions.checkNotNull(databaseName);
-        Preconditions.checkNotNull(platformHiddenSchemas);
+        Preconditions.checkNotNull(schemasNotPlatformSurfaceable);
 
         // Persist the document
         GenericDocument.Builder visibilityDocument = new GenericDocument.Builder(
                 /*uri=*/ databaseName, SCHEMA_TYPE)
                 .setNamespace(NAMESPACE);
-        if (!platformHiddenSchemas.isEmpty()) {
-            visibilityDocument.setPropertyString(PLATFORM_HIDDEN_PROPERTY,
-                    platformHiddenSchemas.toArray(new String[0]));
+        if (!schemasNotPlatformSurfaceable.isEmpty()) {
+            visibilityDocument.setPropertyString(NOT_PLATFORM_SURFACEABLE_PROPERTY,
+                    schemasNotPlatformSurfaceable.toArray(new String[0]));
         }
         mAppSearchImpl.putDocument(DATABASE_NAME, visibilityDocument.build());
 
         // Update derived data structures.
-        mPlatformHiddenMap.put(databaseName, platformHiddenSchemas);
+        mNotPlatformSurfaceableMap.put(databaseName, schemasNotPlatformSurfaceable);
     }
 
     /**
@@ -228,13 +176,13 @@
      * none exist.
      */
     @NonNull
-    public Set<String> getPlatformHiddenSchemas(@NonNull String databaseName) {
+    public Set<String> getSchemasNotPlatformSurfaceable(@NonNull String databaseName) {
         Preconditions.checkNotNull(databaseName);
-        Set<String> platformHiddenSchemas = mPlatformHiddenMap.get(databaseName);
-        if (platformHiddenSchemas == null) {
+        Set<String> schemasNotPlatformSurfaceable = mNotPlatformSurfaceableMap.get(databaseName);
+        if (schemasNotPlatformSurfaceable == null) {
             return Collections.emptySet();
         }
-        return platformHiddenSchemas;
+        return schemasNotPlatformSurfaceable;
     }
 
     /**
@@ -244,7 +192,7 @@
      * @throws AppSearchException on AppSearchImpl error.
      */
     public void handleReset() throws AppSearchException {
-        mPlatformHiddenMap.clear();
+        mNotPlatformSurfaceableMap.clear();
         initialize();
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index 2f4c23d..c27cb92 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -39,7 +39,7 @@
     /** Converts a {@link GenericDocument} into a {@link DocumentProto}. */
     @NonNull
     @SuppressWarnings("unchecked")
-    public static DocumentProto convert(@NonNull GenericDocument document) {
+    public static DocumentProto toDocumentProto(@NonNull GenericDocument document) {
         Preconditions.checkNotNull(document);
         DocumentProto.Builder mProtoBuilder = DocumentProto.newBuilder();
         mProtoBuilder.setUri(document.getUri())
@@ -81,7 +81,7 @@
                 }
             } else if (documentValues != null) {
                 for (int j = 0; j < documentValues.length; j++) {
-                    DocumentProto proto = convert(documentValues[j]);
+                    DocumentProto proto = toDocumentProto(documentValues[j]);
                     propertyProto.addDocumentValues(proto);
                 }
             } else {
@@ -95,7 +95,7 @@
 
     /** Converts a {@link DocumentProto} into a {@link GenericDocument}. */
     @NonNull
-    public static GenericDocument convert(@NonNull DocumentProto proto) {
+    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto) {
         Preconditions.checkNotNull(proto);
         GenericDocument.Builder<?> documentBuilder =
                 new GenericDocument.Builder<>(proto.getUri(), proto.getSchema())
@@ -140,7 +140,7 @@
             } else if (property.getDocumentValuesCount() > 0) {
                 GenericDocument[] values = new GenericDocument[property.getDocumentValuesCount()];
                 for (int j = 0; j < values.length; j++) {
-                    values[j] = convert(property.getDocumentValues(j));
+                    values[j] = toGenericDocument(property.getDocumentValues(j));
                 }
                 documentBuilder.setPropertyDocument(name, values);
             } else {
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
index 3d79150..f52c220 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SchemaToProtoConverter.java
@@ -16,6 +16,8 @@
 
 package androidx.appsearch.localstorage.converter;
 
+import android.util.Log;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.AppSearchSchema;
@@ -23,6 +25,7 @@
 
 import com.google.android.icing.proto.PropertyConfigProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.SchemaTypeConfigProtoOrBuilder;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
 
@@ -34,6 +37,8 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SchemaToProtoConverter {
+    private static final String TAG = "AppSearchSchemaToProtoC";
+
     private SchemaToProtoConverter() {}
 
     /**
@@ -41,23 +46,23 @@
      * {@link SchemaTypeConfigProto}.
      */
     @NonNull
-    public static SchemaTypeConfigProto convert(@NonNull AppSearchSchema schema) {
+    public static SchemaTypeConfigProto toSchemaTypeConfigProto(@NonNull AppSearchSchema schema) {
         Preconditions.checkNotNull(schema);
         SchemaTypeConfigProto.Builder protoBuilder =
                 SchemaTypeConfigProto.newBuilder().setSchemaType(schema.getSchemaType());
         List<AppSearchSchema.PropertyConfig> properties = schema.getProperties();
         for (int i = 0; i < properties.size(); i++) {
-            PropertyConfigProto propertyProto = convertProperty(properties.get(i));
+            PropertyConfigProto propertyProto = toPropertyConfigProto(properties.get(i));
             protoBuilder.addProperties(propertyProto);
         }
         return protoBuilder.build();
     }
 
     @NonNull
-    private static PropertyConfigProto convertProperty(
+    private static PropertyConfigProto toPropertyConfigProto(
             @NonNull AppSearchSchema.PropertyConfig property) {
         Preconditions.checkNotNull(property);
-        PropertyConfigProto.Builder propertyConfigProto = PropertyConfigProto.newBuilder()
+        PropertyConfigProto.Builder builder = PropertyConfigProto.newBuilder()
                 .setPropertyName(property.getName());
         StringIndexingConfig.Builder indexingConfig = StringIndexingConfig.newBuilder();
 
@@ -68,12 +73,12 @@
         if (dataTypeProto == null) {
             throw new IllegalArgumentException("Invalid dataType: " + dataType);
         }
-        propertyConfigProto.setDataType(dataTypeProto);
+        builder.setDataType(dataTypeProto);
 
         // Set schemaType
         String schemaType = property.getSchemaType();
         if (schemaType != null) {
-            propertyConfigProto.setSchemaType(schemaType);
+            builder.setSchemaType(schemaType);
         }
 
         // Set cardinality
@@ -83,7 +88,7 @@
         if (cardinalityProto == null) {
             throw new IllegalArgumentException("Invalid cardinality: " + dataType);
         }
-        propertyConfigProto.setCardinality(cardinalityProto);
+        builder.setCardinality(cardinalityProto);
 
         // Set indexingType
         @AppSearchSchema.PropertyConfig.IndexingType int indexingType = property.getIndexingType();
@@ -114,7 +119,63 @@
         indexingConfig.setTokenizerType(tokenizerTypeProto);
 
         // Build!
-        propertyConfigProto.setStringIndexingConfig(indexingConfig);
-        return propertyConfigProto.build();
+        builder.setStringIndexingConfig(indexingConfig);
+        return builder.build();
+    }
+
+    /**
+     * Converts a {@link SchemaTypeConfigProto} into an
+     * {@link androidx.appsearch.app.AppSearchSchema}.
+     */
+    @NonNull
+    public static AppSearchSchema toAppSearchSchema(@NonNull SchemaTypeConfigProtoOrBuilder proto) {
+        Preconditions.checkNotNull(proto);
+        AppSearchSchema.Builder builder = new AppSearchSchema.Builder(proto.getSchemaType());
+        List<PropertyConfigProto> properties = proto.getPropertiesList();
+        for (int i = 0; i < properties.size(); i++) {
+            AppSearchSchema.PropertyConfig propertyConfig = toPropertyConfig(properties.get(i));
+            builder.addProperty(propertyConfig);
+        }
+        return builder.build();
+    }
+
+    @NonNull
+    private static AppSearchSchema.PropertyConfig toPropertyConfig(
+            @NonNull PropertyConfigProto proto) {
+        Preconditions.checkNotNull(proto);
+        AppSearchSchema.PropertyConfig.Builder builder =
+                new AppSearchSchema.PropertyConfig.Builder(proto.getPropertyName())
+                        .setDataType(proto.getDataType().getNumber())
+                        .setCardinality(proto.getCardinality().getNumber())
+                        .setTokenizerType(
+                                proto.getStringIndexingConfig().getTokenizerType().getNumber());
+
+        // Set schema
+        if (!proto.getSchemaType().isEmpty()) {
+            builder.setSchemaType(proto.getSchemaType());
+        }
+
+        // Set indexingType
+        @AppSearchSchema.PropertyConfig.IndexingType int indexingType;
+        TermMatchType.Code termMatchTypeProto = proto.getStringIndexingConfig().getTermMatchType();
+        switch (termMatchTypeProto) {
+            case UNKNOWN:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+                break;
+            case EXACT_ONLY:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_EXACT_TERMS;
+                break;
+            case PREFIX:
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_PREFIXES;
+                break;
+            default:
+                // Avoid crashing in the 'read' path; we should try to interpret the document to the
+                // extent possible.
+                Log.w(TAG, "Invalid indexingType: " + termMatchTypeProto.getNumber());
+                indexingType = AppSearchSchema.PropertyConfig.INDEXING_TYPE_NONE;
+        }
+        builder.setIndexingType(indexingType);
+
+        return builder.build();
     }
 }
diff --git a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index 97c4adc..8d7257c 100644
--- a/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -42,13 +42,12 @@
 
     /** Translate a {@link SearchResultProto} into {@link SearchResultPage}. */
     @NonNull
-    public static SearchResultPage convertToSearchResultPage(
-            @NonNull SearchResultProtoOrBuilder proto) {
+    public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto) {
         Bundle bundle = new Bundle();
         bundle.putLong(SearchResultPage.NEXT_PAGE_TOKEN_FIELD, proto.getNextPageToken());
         ArrayList<Bundle> resultBundles = new ArrayList<>(proto.getResultsCount());
         for (int i = 0; i < proto.getResultsCount(); i++) {
-            resultBundles.add(convertToSearchResultBundle(proto.getResults(i)));
+            resultBundles.add(toSearchResultBundle(proto.getResults(i)));
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
         return new SearchResultPage(bundle);
@@ -56,10 +55,11 @@
 
     /** Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. */
     @NonNull
-    private static Bundle convertToSearchResultBundle(
+    private static Bundle toSearchResultBundle(
             @NonNull SearchResultProto.ResultProtoOrBuilder proto) {
         Bundle bundle = new Bundle();
-        GenericDocument document = GenericDocumentToProtoConverter.convert(proto.getDocument());
+        GenericDocument document =
+                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument());
         bundle.putBundle(SearchResult.DOCUMENT_FIELD, document.getBundle());
 
         ArrayList<Bundle> matchList = new ArrayList<>();
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
index 44a22a8..f103e683 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/AndroidManifest.xml
@@ -26,12 +26,16 @@
         under the new package visibility changes for Android 11.
          -->
         <activity
-            android:name=".MainActivity"
+            android:name=".TrivialStartupActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
+            <intent-filter>
+                <action android:name="androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
         </activity>
 
         <activity
@@ -42,6 +46,5 @@
                 <category android:name="android.intent.category.DEFAULT" />
             </intent-filter>
         </activity>
-
     </application>
 </manifest>
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
index efddddb..bb07d9f 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/RecyclerViewActivity.kt
@@ -24,9 +24,11 @@
 class RecyclerViewActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
+        title = "RecyclerView Sample"
         setContentView(R.layout.activity_recycler_view)
         val recycler = findViewById<RecyclerView>(R.id.recycler)
-        val adapter = EntryAdapter(entries(1000))
+        val itemCount = intent.getIntExtra(EXTRA_ITEM_COUNT, 1000)
+        val adapter = EntryAdapter(entries(itemCount))
         recycler.layoutManager = LinearLayoutManager(this)
         recycler.adapter = adapter
     }
@@ -36,4 +38,8 @@
             Entry("Item $it")
         }
     }
+
+    companion object {
+        const val EXTRA_ITEM_COUNT = "ITEM_COUNT"
+    }
 }
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
similarity index 96%
rename from benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
rename to benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
index 05177a9..5cf2081 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/MainActivity.kt
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/java/androidx/benchmark/integration/macrobenchmark/target/TrivialStartupActivity.kt
@@ -23,7 +23,7 @@
 import androidx.annotation.RequiresApi
 import androidx.appcompat.app.AppCompatActivity
 
-class MainActivity : AppCompatActivity() {
+class TrivialStartupActivity : AppCompatActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
diff --git a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
index d8d6754..fa8493f 100644
--- a/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
+++ b/benchmark/integration-tests/macrobenchmark-target/src/main/res/layout/recycler_row.xml
@@ -15,23 +15,44 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
+    android:layout_height="wrap_content"
+    android:minHeight="64dp"
+    android:padding="8dp">
 
     <androidx.cardview.widget.CardView
         android:id="@+id/card"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="match_parent"
         android:padding="8dp">
 
-        <TextView
-            android:id="@+id/content"
-            android:padding="8dp"
+        <androidx.constraintlayout.widget.ConstraintLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            tools:text="Sample text" />
+            android:layout_height="wrap_content">
 
+            <androidx.appcompat.widget.AppCompatCheckBox
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:layout_marginEnd="16dp"
+                android:layout_marginRight="16dp"
+                android:layout_marginTop="8dp"
+                app:layout_constraintEnd_toEndOf="parent"
+                app:layout_constraintTop_toTopOf="parent" />
+
+            <androidx.appcompat.widget.AppCompatTextView
+                android:id="@+id/content"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="16dp"
+                android:layout_marginStart="16dp"
+                android:layout_marginTop="8dp"
+                android:padding="8dp"
+                app:layout_constraintStart_toStartOf="parent"
+                app:layout_constraintTop_toTopOf="parent"
+                tools:text="Sample text" />
+        </androidx.constraintlayout.widget.ConstraintLayout>
     </androidx.cardview.widget.CardView>
-
 </LinearLayout>
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/build.gradle b/benchmark/integration-tests/macrobenchmark/build.gradle
index 398842e..3384fe8 100644
--- a/benchmark/integration-tests/macrobenchmark/build.gradle
+++ b/benchmark/integration-tests/macrobenchmark/build.gradle
@@ -34,6 +34,7 @@
 }
 
 dependencies {
+    androidTestImplementation(project(":benchmark:benchmark-junit4"))
     androidTestImplementation(project(":benchmark:benchmark-macro"))
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
index 3285605..89e3b20 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/JankMetricValidation.kt
@@ -22,24 +22,23 @@
 import androidx.benchmark.macro.MacrobenchmarkConfig
 import androidx.benchmark.macro.MacrobenchmarkRule
 import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.Direction
 import androidx.test.uiautomator.UiDevice
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
+@SdkSuppress(minSdkVersion = 29)
 @RunWith(Parameterized::class)
 class JankMetricValidation(
-    private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val compilationMode: CompilationMode
 ) {
-
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
@@ -52,13 +51,11 @@
     }
 
     @Test
-    @Ignore("Not running the test in CI")
     fun start() {
         val config = MacrobenchmarkConfig(
             packageName = PACKAGE_NAME,
             metrics = listOf(JankMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
             iterations = 10
         )
 
@@ -67,7 +64,6 @@
             setupBlock = {
                 val intent = Intent()
                 intent.action = ACTION
-                intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                 launchIntentAndWait(intent)
             }
         ) {
@@ -85,17 +81,13 @@
             "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
         private const val RESOURCE_ID = "recycler"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}")
         @JvmStatic
         fun jankParameters(): List<Array<Any>> {
-            val compilationModes = listOf(
+            return listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
-            )
-            val processKillOptions = listOf(false, false)
-            return compilationModes.zip(processKillOptions).map {
-                arrayOf(it.first, it.second)
-            }
+            ).map { arrayOf(it) }
         }
     }
 }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
deleted file mode 100644
index 1b7b5a6..0000000
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/MacrobenchmarkRuleTest.kt
+++ /dev/null
@@ -1,50 +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.benchmark.integration.macrobenchmark
-
-import androidx.benchmark.macro.CompilationMode
-import androidx.benchmark.macro.MacrobenchmarkConfig
-import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.StartupTimingMetric
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import org.junit.Ignore
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-class MacrobenchmarkRuleTest {
-    @get:Rule
-    val benchmarkRule = MacrobenchmarkRule()
-
-    @Test
-    @Ignore("Not running the test in CI")
-    fun basicTest() = benchmarkRule.measureRepeated(
-        MacrobenchmarkConfig(
-            packageName = "androidx.benchmark.integration.macrobenchmark.target",
-            listOf(StartupTimingMetric()),
-            CompilationMode.Speed,
-            killProcessEachIteration = true,
-            iterations = 4
-        )
-    ) {
-        pressHome()
-        launchPackageAndWait()
-    }
-}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index 34ab2b4..bd1951a 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -16,58 +16,53 @@
 
 package androidx.benchmark.integration.macrobenchmark
 
-import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
-import androidx.benchmark.macro.macrobenchmark
 import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.Parameterized
 
 @LargeTest
+@SdkSuppress(minSdkVersion = 29)
 @RunWith(Parameterized::class)
 class ProcessSpeedProfileValidation(
     private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val startupMode: StartupMode
 ) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
     @Test
-    @Ignore("Not running the test in CI")
-    fun start() {
-        val benchmarkName = "speed_profile_process_validation"
-        val config = MacrobenchmarkConfig(
+    fun start() = benchmarkRule.measureStartupRepeated(
+        MacrobenchmarkConfig(
             packageName = PACKAGE_NAME,
             metrics = listOf(StartupTimingMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
-            iterations = 10
-        )
-        macrobenchmark(
-            benchmarkName = benchmarkName,
-            config = config
-        ) {
-            pressHome()
-            launchPackageAndWait { launchIntent ->
-                // Clear out any previous instances
-                launchIntent.flags =
-                    Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-            }
-        }
+            iterations = 3
+        ),
+        startupMode
+    ) {
+        pressHome()
+        launchPackageAndWait()
     }
 
     companion object {
         private const val PACKAGE_NAME = "androidx.benchmark.integration.macrobenchmark.target"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
         @JvmStatic
         fun kilProcessParameters(): List<Array<Any>> {
             val compilationModes = listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
             )
-            val processKillOptions = listOf(true, false)
+            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
             return compilationModes.zip(processKillOptions).map {
                 arrayOf(it.first, it.second)
             }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
new file mode 100644
index 0000000..33778b4
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/SmallListStartupBenchmark.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.benchmark.integration.macrobenchmark
+
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class SmallListStartupBenchmark(private val startupMode: StartupMode) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun startup() = benchmarkRule.measureStartup(
+        profileCompiled = true,
+        startupMode = startupMode
+    ) {
+        action = "androidx.benchmark.integration.macrobenchmark.target.RECYCLER_VIEW"
+        putExtra("ITEM_COUNT", 5)
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "mode={0}")
+        @JvmStatic
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM)
+                .map { arrayOf(it) }
+        }
+    }
+}
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
new file mode 100644
index 0000000..e8c152e
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/StartupUtils.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.benchmark.integration.macrobenchmark
+
+import android.content.Intent
+import androidx.benchmark.macro.CompilationMode
+import androidx.benchmark.macro.MacrobenchmarkConfig
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+
+const val TARGET_PACKAGE = "androidx.benchmark.integration.macrobenchmark.target"
+
+fun MacrobenchmarkRule.measureStartup(
+    profileCompiled: Boolean,
+    startupMode: StartupMode,
+    iterations: Int = 5,
+    setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
+    MacrobenchmarkConfig(
+        packageName = "androidx.benchmark.integration.macrobenchmark.target",
+        metrics = listOf(StartupTimingMetric()),
+        compilationMode = if (profileCompiled) {
+            CompilationMode.SpeedProfile(warmupIterations = 3)
+        } else {
+            CompilationMode.None
+        },
+        iterations = iterations
+    ),
+    startupMode = startupMode
+) {
+    pressHome()
+    val intent = Intent()
+    intent.setPackage(TARGET_PACKAGE)
+    setupIntent(intent)
+    launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
new file mode 100644
index 0000000..af3e850
--- /dev/null
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupBenchmark.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.benchmark.integration.macrobenchmark
+
+import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
+import androidx.test.filters.LargeTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+@LargeTest
+@RunWith(Parameterized::class)
+class TrivialStartupBenchmark(private val startupMode: StartupMode) {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun startup() = benchmarkRule.measureStartup(
+        profileCompiled = true,
+        startupMode = startupMode
+    ) {
+        action = "androidx.benchmark.integration.macrobenchmark.target.TRIVIAL_STARTUP_ACTIVITY"
+    }
+
+    companion object {
+        @Parameterized.Parameters(name = "mode={0}")
+        @JvmStatic
+        fun parameters(): List<Array<Any>> {
+            return listOf(StartupMode.COLD, StartupMode.WARM, StartupMode.HOT)
+                .map { arrayOf(it) }
+        }
+    }
+}
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
index d614eb1..d5e016d 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/ActionsTest.kt
@@ -37,7 +37,7 @@
     @Test
     @Ignore("Figure out why we can't launch the default activity.")
     fun killTest() {
-        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
         scope.pressHome()
         scope.launchPackageAndWait()
         assertTrue(isProcessAlive(PACKAGE_NAME))
@@ -48,7 +48,7 @@
     @Test
     @Ignore("Compilation modes are a bit flaky")
     fun compile_speedProfile() {
-        val scope = MacrobenchmarkScope(PACKAGE_NAME)
+        val scope = MacrobenchmarkScope(PACKAGE_NAME, launchWithClearTask = true)
         val iterations = 1
         var executions = 0
         val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
diff --git a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
index fa8afde..a2ece0e 100644
--- a/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
+++ b/benchmark/macro/src/androidTest/java/androidx/benchmark/macro/test/AppStartupHelperTest.kt
@@ -56,7 +56,7 @@
         val packageName = "androidx.benchmark.macro.test"
         val helper = AppStartupHelper()
         helper.startCollecting()
-        val scope = MacrobenchmarkScope(packageName)
+        val scope = MacrobenchmarkScope(packageName, launchWithClearTask = false)
         // note: don't killProcess first, that's our process too!
         // additionally, skip pressHome, since it's not needed, and skipping saves significant time
         scope.launchPackageAndWait {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
index 3a0d28d..be1fffd 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Actions.kt
@@ -21,9 +21,10 @@
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.UiDevice
 import java.io.File
-import java.lang.IllegalStateException
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
 
-private const val TAG = "MacroBenchmarks"
+const val TAG = "MacroBenchmarks"
 
 // SELinux enforcement
 private const val PERMISSIVE = "Permissive"
@@ -38,6 +39,9 @@
 // All modes
 private val COMPILE_MODES = listOf(SPEED, SPEED_PROFILE, QUICKEN, VERIFY)
 
+// SELinux Permission Lock
+private val permissiveLock = ReentrantReadWriteLock()
+
 /**
  * Drops the kernel page cache
  *
@@ -81,6 +85,7 @@
         }
         Thread.sleep(profileSaveTimeout)
     }
+    Log.d(TAG, "Compiling $packageName ($mode)")
     val response = device.executeShellCommand("cmd package compile -f -m $mode $packageName")
     if (!response.contains("Success")) {
         Log.d(TAG, "Received compile cmd response: $response")
@@ -95,6 +100,7 @@
     instrumentation: Instrumentation,
     packageName: String,
 ) {
+    Log.d(TAG, "Clearing profiles for $packageName")
     instrumentation.device().executeShellCommand("cmd package compile --reset $packageName")
 }
 
@@ -113,14 +119,16 @@
 fun withPermissiveSeLinuxPolicy(block: () -> Unit) {
     val instrumentation = InstrumentationRegistry.getInstrumentation()
     val previousPolicy = getSeLinuxPolicyEnforced(instrumentation)
-    try {
-        if (previousPolicy == 1) {
-            setSeLinuxPolicyEnforced(instrumentation, 0)
-        }
-        block()
-    } finally {
-        if (previousPolicy == 1) {
-            setSeLinuxPolicyEnforced(instrumentation, previousPolicy)
+    permissiveLock.read {
+        try {
+            if (previousPolicy == 1) {
+                setSeLinuxPolicyEnforced(instrumentation, 0)
+            }
+            block()
+        } finally {
+            if (previousPolicy == 1 && permissiveLock.readHoldCount == 1) {
+                setSeLinuxPolicyEnforced(instrumentation, previousPolicy)
+            }
         }
     }
 }
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
index 795f693..c0c35c3 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/AppStartupHelper.kt
@@ -89,7 +89,7 @@
         return List(expectedEventCount) { index ->
             val appStart = appStartOccurredList.getOrNull(index)
             AppStartupMetrics(
-                transitionType = appStart?.type.toString(),
+                transitionType = appStart?.type?.toStartupMode(),
                 windowDrawnDelayMs = appStart?.windowsDrawnDelayMillis?.toLong(),
                 transitionDelayMs = appStart?.transitionDelayMillis?.toLong(),
                 appStartupTimeMs = appStartFullyDrawnList.getOrNull(index)?.appStartupTimeMillis,
@@ -113,14 +113,20 @@
             )
         }
         val result = results.first()
-        // TODO: potentially filter these further for app vs platform usage
+
+        Log.d(
+            "AppStartupHelper",
+            "saw startup of package $packageName, type ${result.transitionType}"
+        )
         return mapOf(
             // AppStartupHelper originally reports this as simply startup time, so we do the same
             "startupMs" to result.windowDrawnDelayMs,
-            "transitionDelayMs" to result.transitionDelayMs,
             // Though the proto calls this appStartupTime, we clarify this is "fully drawn" startup
             "startupFullyDrawnMs" to result.appStartupTimeMs,
-            "processStartDelayMs" to result.processStartDelayMs,
+
+            // The following metrics are useful for platform devs, but disabled for now for brevity
+            // "transitionDelayMs" to result.transitionDelayMs,
+            // "processStartDelayMs" to result.processStartDelayMs,
         )
             .filterValues { it != null } // doesn't drop nullability for values...
             .mapValues { it.value!! } // ...so we do that explicitly here
@@ -140,8 +146,17 @@
         isProcStartDetailsDisabled = true
     }
 
+    private fun Int.toStartupMode(): StartupMode? {
+        return when (this) {
+            AtomsProto.AppStartOccurred.COLD -> StartupMode.COLD
+            AtomsProto.AppStartOccurred.WARM -> StartupMode.WARM
+            AtomsProto.AppStartOccurred.HOT -> StartupMode.HOT
+            else -> null
+        }
+    }
+
     data class AppStartupMetrics(
-        val transitionType: String?,
+        val transitionType: StartupMode?,
         val windowDrawnDelayMs: Long?,
         val transitionDelayMs: Long?,
         val appStartupTimeMs: Long?,
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 71ccadf..c386a1e 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -16,6 +16,8 @@
 
 package androidx.benchmark.macro
 
+import androidx.test.platform.app.InstrumentationRegistry
+
 sealed class CompilationMode(
     // for modes other than [None], is argument passed `cmd package compile`
     private val compileArgument: String?
@@ -39,3 +41,23 @@
         override fun toString() = "CompilationMode.Speed"
     }
 }
+
+internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
+    val instrumentation = InstrumentationRegistry.getInstrumentation()
+    // Clear profile between runs.
+    clearProfile(instrumentation, packageName)
+    if (this == CompilationMode.None) {
+        return // nothing to do
+    }
+    if (this is CompilationMode.SpeedProfile) {
+        repeat(this.warmupIterations) {
+            block()
+        }
+    }
+    // TODO: merge in below method
+    compilationFilter(
+        InstrumentationRegistry.getInstrumentation(),
+        packageName,
+        compileArgument()
+    )
+}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
new file mode 100644
index 0000000..24e628d
--- /dev/null
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/IdeSummaryString.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.benchmark.macro
+
+import androidx.benchmark.Stats
+import java.util.Collections
+import kotlin.math.max
+
+internal fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
+    val maxLabelLength = Collections.max(statsList.map { it.name.length })
+
+    // max string length of any printed min/median/max is the largest max value seen. used to pad.
+    val maxValueLength = statsList
+        .map { it.max }
+        .reduce { acc, maxValue -> max(acc, maxValue) }
+        .toString().length
+
+    return "$benchmarkName\n" + statsList.joinToString("\n") {
+        val displayName = it.name.padStart(maxLabelLength)
+        val displayMin = it.min.toString().padStart(maxValueLength)
+        val displayMedian = it.median.toString().padStart(maxValueLength)
+        val displayMax = it.max.toString().padStart(maxValueLength)
+        "  $displayName   min $displayMin,   median $displayMedian,   max $displayMax"
+    } + "\n"
+}
\ No newline at end of file
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index 1d51fa8..4a88391 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -17,21 +17,27 @@
 package androidx.benchmark.macro
 
 import android.content.Intent
+import android.util.Log
 import androidx.benchmark.InstrumentationResults
 import androidx.benchmark.Stats
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.uiautomator.By
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
-import java.util.Collections
-import kotlin.math.max
 
 /**
  * Provides access to common operations in app automation, such as killing the app,
  * or navigating home.
  */
 public class MacrobenchmarkScope(
-    private val packageName: String
+    private val packageName: String,
+    /**
+     * Controls whether launches will automatically set [Intent.FLAG_ACTIVITY_CLEAR_TASK].
+     *
+     * Default to true, so Activity launches go through full creation lifecycle stages, instead of
+     * just resume.
+     */
+    private val launchWithClearTask: Boolean
 ) {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val context = instrumentation.context
@@ -39,14 +45,9 @@
 
     /**
      * Launch the package, with a customizable intent.
-     *
-     * If [block] is not specified, launches with [Intent.FLAG_ACTIVITY_NEW_TASK] as well as
-     * [Intent.FLAG_ACTIVITY_CLEAR_TASK]
      */
     fun launchPackageAndWait(
-        block: (Intent) -> Unit = {
-            it.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
-        }
+        block: (Intent) -> Unit = {}
     ) {
         val intent = context.packageManager.getLaunchIntentForPackage(packageName)
             ?: throw IllegalStateException("Unable to acquire intent for package $packageName")
@@ -56,6 +57,11 @@
     }
 
     fun launchIntentAndWait(intent: Intent) {
+        // Must launch with new task, as we're not launching from an existing task
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        if (launchWithClearTask) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
+        }
         context.startActivity(intent)
         device.wait(
             Until.hasObject(By.pkg(packageName).depth(0)),
@@ -69,6 +75,7 @@
     }
 
     fun killProcess() {
+        Log.d(TAG, "Killing process $packageName")
         device.executeShellCommand("am force-stop $packageName")
     }
 }
@@ -77,7 +84,6 @@
     val packageName: String,
     val metrics: List<Metric>,
     val compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
-    val killProcessEachIteration: Boolean = false,
     val iterations: Int
 )
 
@@ -89,16 +95,17 @@
 fun macrobenchmark(
     benchmarkName: String,
     config: MacrobenchmarkConfig,
-    setupBlock: MacrobenchmarkScope.() -> Unit = {},
+    launchWithClearTask: Boolean,
+    setupBlock: MacrobenchmarkScope.(Boolean) -> Unit,
     measureBlock: MacrobenchmarkScope.() -> Unit
 ) = withPermissiveSeLinuxPolicy {
-    val scope = MacrobenchmarkScope(config.packageName)
+    val scope = MacrobenchmarkScope(config.packageName, launchWithClearTask)
 
     // always kill the process at beginning of test
     scope.killProcess()
 
     config.compilationMode.compile(config.packageName) {
-        setupBlock(scope)
+        setupBlock(scope, false)
         measureBlock(scope)
     }
 
@@ -109,24 +116,21 @@
         config.metrics.forEach {
             it.configure(config)
         }
+        var isFirstRun = true
         val results = List(config.iterations) { iteration ->
-            if (config.killProcessEachIteration) {
-                // TODO: remove this flag, make this part of setupBlock
-                scope.killProcess()
-            }
-            setupBlock(scope)
+            setupBlock(scope, isFirstRun)
+            isFirstRun = false
             try {
                 perfettoCollector.start()
                 config.metrics.forEach {
                     it.start()
                 }
                 measureBlock(scope)
+            } finally {
                 config.metrics.forEach {
                     it.stop()
                 }
-            } finally {
-                val iterString = iteration.toString().padStart(3, '0')
-                perfettoCollector.stop("${benchmarkName}_iter$iterString.trace")
+                perfettoCollector.stop(benchmarkName, iteration)
             }
 
             config.metrics
@@ -139,9 +143,14 @@
         // merge each independent Map<String,Long> to one Map<String,List<Long>>
         val setOfAllKeys = results.flatMap { it.keys }.toSet()
         val listResults = setOfAllKeys.map { key ->
-            key to results.map { it[key] ?: error("Value $key missing from one iteration") }
+            // b/174175947
+            key to results.mapNotNull {
+                if (key !in it) {
+                    Log.w(TAG, "Value $key missing from one iteration {$it}")
+                }
+                it[key]
+            }
         }.toMap()
-
         val statsList = listResults.map { (metricName, values) ->
             Stats(values.toLongArray(), metricName)
         }.sortedBy { it.name }
@@ -155,40 +164,53 @@
     }
 }
 
-fun ideSummaryString(benchmarkName: String, statsList: List<Stats>): String {
-    val maxLabelLength = Collections.max(statsList.map { it.name.length })
+enum class StartupMode {
+    /**
+     * Startup from scratch - app's process is not alive, and must be started in addition to
+     * Activity creation.
+     *
+     * See
+     * [Cold startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#cold)
+     */
+    COLD,
 
-    // max string length of any printed min/median/max is the largest max value seen. used to pad.
-    val maxValueLength = statsList
-        .map { it.max }
-        .reduce { acc, maxValue -> max(acc, maxValue) }
-        .toString().length
+    /**
+     * Create and display a new Activity in a currently running app process.
+     *
+     * See
+     * [Warm startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#warm)
+     */
+    WARM,
 
-    return "$benchmarkName\n" + statsList.joinToString("\n") {
-        val displayName = it.name.padStart(maxLabelLength)
-        val displayMin = it.min.toString().padStart(maxValueLength)
-        val displayMedian = it.median.toString().padStart(maxValueLength)
-        val displayMax = it.max.toString().padStart(maxValueLength)
-        "  $displayName   min $displayMin,   median $displayMedian,   max $displayMax"
-    } + "\n"
+    /**
+     * Bring existing activity to the foreground, process and Activity still exist from previous
+     * launch.
+     *
+     * See
+     * [Hot startup documentation](https://developer.android.com/topic/performance/vitals/launch-time#hot)
+     */
+    HOT
 }
 
-internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
-    val instrumentation = InstrumentationRegistry.getInstrumentation()
-    // Clear profile between runs.
-    clearProfile(instrumentation, packageName)
-    if (this == CompilationMode.None) {
-        return // nothing to do
-    }
-    if (this is CompilationMode.SpeedProfile) {
-        repeat(this.warmupIterations) {
-            block()
-        }
-    }
-    // TODO: merge in below method
-    compilationFilter(
-        InstrumentationRegistry.getInstrumentation(),
-        packageName,
-        compileArgument()
+fun startupMacrobenchmark(
+    benchmarkName: String,
+    config: MacrobenchmarkConfig,
+    startupMode: StartupMode,
+    performStartup: MacrobenchmarkScope.() -> Unit
+) {
+    macrobenchmark(
+        benchmarkName = benchmarkName,
+        config = config,
+        setupBlock = { firstIterAfterCompile ->
+            if (startupMode == StartupMode.COLD) {
+                killProcess()
+            } else if (firstIterAfterCompile) {
+                // warmup process by launching the activity, unmeasured
+                performStartup()
+            }
+        },
+        // only reuse existing activity if StartupMode == HOT
+        launchWithClearTask = startupMode != StartupMode.HOT,
+        measureBlock = performStartup
     )
 }
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
index 86e31bc..a5edac8 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/MacrobenchmarkRule.kt
@@ -28,10 +28,29 @@
 
     fun measureRepeated(
         config: MacrobenchmarkConfig,
-        setupBlock: MacrobenchmarkScope.() -> Unit = {},
+        setupBlock: MacrobenchmarkScope.(Boolean) -> Unit = {},
         measureBlock: MacrobenchmarkScope.() -> Unit
     ) {
-        macrobenchmark(benchmarkName, config, setupBlock, measureBlock)
+        macrobenchmark(
+            benchmarkName = benchmarkName,
+            config = config,
+            launchWithClearTask = true,
+            setupBlock = setupBlock,
+            measureBlock = measureBlock
+        )
+    }
+
+    fun measureStartupRepeated(
+        config: MacrobenchmarkConfig,
+        startupMode: StartupMode,
+        performStartup: MacrobenchmarkScope.() -> Unit
+    ) {
+        startupMacrobenchmark(
+            benchmarkName = benchmarkName,
+            config = config,
+            startupMode = startupMode,
+            performStartup = performStartup
+        )
     }
 
     override fun apply(base: Statement, description: Description) = object : Statement() {
diff --git a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
index 88014df..840dee5 100644
--- a/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
+++ b/benchmark/macro/src/main/java/androidx/benchmark/macro/PerfettoCaptureWrapper.kt
@@ -42,11 +42,14 @@
         return true
     }
 
-    fun stop(traceName: String): Boolean {
+    fun stop(benchmarkName: String, iteration: Int): Boolean {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            val iterString = iteration.toString().padStart(3, '0')
+            val traceName = "${benchmarkName}_iter$iterString.trace"
+
             val destination = destinationPath(traceName).absolutePath
             capture?.stop(destination)
-            reportAdditionalFileToCopy("perfetto_trace", destination)
+            reportAdditionalFileToCopy("perfetto_trace_$iterString", destination)
         }
         return true
     }
diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
index dc64b9a..f27e132 100644
--- a/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXUiPlugin.kt
@@ -35,6 +35,9 @@
 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
+const val composeSourceOption =
+    "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
+
 /**
  * Plugin to apply options across all of the androidx.ui projects
  */
@@ -81,6 +84,20 @@
                         }
                     }
 
+                    project.afterEvaluate {
+                        val androidXExtension =
+                            project.extensions.findByType(AndroidXExtension::class.java)
+                        if (androidXExtension != null) {
+                            if (!conf.isEmpty && androidXExtension.publish.shouldPublish()) {
+                                project.tasks.withType(KotlinCompile::class.java)
+                                    .configureEach { compile ->
+                                        compile.kotlinOptions.freeCompilerArgs +=
+                                            listOf("-P", composeSourceOption)
+                                    }
+                            }
+                        }
+                    }
+
                     if (plugin is KotlinMultiplatformPluginWrapper) {
                         project.configureForMultiplatform()
                     }
diff --git a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
index 5023caa..57e1f7d 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -35,16 +35,16 @@
     val BIOMETRIC = Version("1.2.0-alpha01")
     val BROWSER = Version("1.3.0-rc01")
     val BUILDSRC_TESTS = Version("1.0.0-alpha01")
-    val CAMERA = Version("1.0.0-beta12")
-    val CAMERA_EXTENSIONS = Version("1.0.0-alpha19")
+    val CAMERA = Version("1.0.0-rc01")
+    val CAMERA_EXTENSIONS = Version("1.0.0-alpha20")
     val CAMERA_PIPE = Version("1.0.0-alpha01")
     val CAMERA_VIDEO = Version("1.0.0-alpha01")
-    val CAMERA_VIEW = Version("1.0.0-alpha19")
+    val CAMERA_VIEW = Version("1.0.0-alpha20")
     val CARDVIEW = Version("1.1.0-alpha01")
     val CAR_APP = Version("1.0.0-alpha01")
     val COLLECTION = Version("1.2.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
-    val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha08")
+    val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha09")
     val COORDINATORLAYOUT = Version("1.2.0-alpha01")
     val CORE = Version("1.5.0-alpha06")
     val CORE_ANIMATION = Version("1.0.0-alpha03")
@@ -75,16 +75,16 @@
     val LEANBACK_PREFERENCE = Version("1.1.0-beta01")
     val LEGACY = Version("1.1.0-alpha01")
     val LOCALBROADCASTMANAGER = Version("1.1.0-alpha02")
-    val LIFECYCLE = Version("2.3.0-beta01")
+    val LIFECYCLE = Version("2.3.0-rc01")
     val LIFECYCLE_EXTENSIONS = Version("2.2.0")
     val LOADER = Version("1.2.0-alpha01")
     val MEDIA = Version("1.3.0-alpha02")
     val MEDIA2 = Version("1.2.0-alpha01")
     val MEDIAROUTER = Version("1.3.0-alpha01")
     val NAVIGATION = Version("2.4.0-alpha01")
-    val NAVIGATION_COMPOSE = Version("1.0.0-alpha03")
+    val NAVIGATION_COMPOSE = Version("1.0.0-alpha04")
     val PAGING = Version("3.0.0-alpha10")
-    val PAGING_COMPOSE = Version("1.0.0-alpha03")
+    val PAGING_COMPOSE = Version("1.0.0-alpha04")
     val PALETTE = Version("1.1.0-alpha01")
     val PRINT = Version("1.1.0-beta01")
     val PERCENTLAYOUT = Version("1.1.0-alpha01")
@@ -94,7 +94,7 @@
     val RECYCLERVIEW_SELECTION = Version("2.0.0-alpha01")
     val REMOTECALLBACK = Version("1.0.0-alpha02")
     val ROOM = Version("2.3.0-alpha04")
-    val SAVEDSTATE = Version("1.1.0-beta01")
+    val SAVEDSTATE = Version("1.1.0-rc01")
     val SECURITY = Version("1.1.0-alpha03")
     val SECURITY_BIOMETRIC = Version("1.0.0-alpha01")
     val SECURITY_IDENTITY_CREDENTIAL = Version("1.0.0-alpha01")
diff --git a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
index a29179c..e764c62 100644
--- a/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/LintConfiguration.kt
@@ -91,7 +91,6 @@
             isCheckReleaseBuilds = false
 
             // Write output directly to the console (and nowhere else).
-            textOutput("stderr")
             textReport = true
             htmlReport = false
 
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index 02db235..d8ba393 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -98,7 +98,7 @@
 const val REACTIVE_STREAMS = "org.reactivestreams:reactive-streams:1.0.0"
 const val RX_JAVA = "io.reactivex.rxjava2:rxjava:2.2.9"
 const val RX_JAVA3 = "io.reactivex.rxjava3:rxjava:3.0.0"
-val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.16"
+val SKIKO_VERSION = System.getenv("SKIKO_VERSION") ?: "0.1.18"
 val SKIKO = "org.jetbrains.skiko:skiko-jvm:$SKIKO_VERSION"
 val SKIKO_LINUX_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-linux-x64:$SKIKO_VERSION"
 val SKIKO_MACOS_X64 = "org.jetbrains.skiko:skiko-jvm-runtime-macos-x64:$SKIKO_VERSION"
diff --git a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
index 5bfdf18..c92d780 100644
--- a/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/uptodatedness/TaskUpToDateValidator.kt
@@ -47,8 +47,6 @@
     "createArchive",
     "createDiffArchiveForAll",
     "createProjectZip",
-    "desugarPublicDebugFileDependencies",
-    "desugarTipOfTreeDebugFileDependencies",
     "externalNativeBuildDebug",
     "externalNativeBuildRelease",
     "generateJsonModelDebug",
@@ -107,10 +105,7 @@
      */
     "relocateShadowJar",
     "stripArchiveForPartialDejetification",
-    "transformClassesWithDexBuilderForPublicDebug",
-    "transformClassesWithDexBuilderForTipOfTreeDebug",
     "verifyDependencyVersions",
-    "zipEcFiles",
     "zipTestConfigsWithApks",
 
     ":camera:integration-tests:camera-testapp-core:mergeLibDexDebug",
@@ -123,10 +118,6 @@
     ":camera:integration-tests:camera-testapp-view:GenerateTestConfigurationdebugAndroidTest",
     ":camera:integration-tests:camera-testapp-view:mergeLibDexDebug",
     ":camera:integration-tests:camera-testapp-view:packageDebug",
-
-    ":inspection:inspection-gradle-plugin:generatePomFileForInspectionPluginMarkerMavenPublication",
-    ":inspection:inspection-gradle-plugin:" +
-        "publishInspectionPluginMarkerMavenPublicationToMavenRepository"
 )
 
 // Additional tasks that are expected to be temporarily out-of-date after running once
diff --git a/busytown/androidx_max_dep_versions.sh b/busytown/androidx_max_dep_versions.sh
index 5dc8e64..aee0689 100755
--- a/busytown/androidx_max_dep_versions.sh
+++ b/busytown/androidx_max_dep_versions.sh
@@ -5,6 +5,8 @@
 
 cd "$(dirname $0)"
 
-impl/build.sh --no-daemon assembleDebug assembleAndroidTest -PuseMaxDepVersions --offline "$@"
+impl/build.sh --no-daemon --offline assembleDebug assembleAndroidTest \
+    -PuseMaxDepVersions \
+    -Pandroidx.validateNoUnrecognizedMessages "$@"
 
 echo "Completing $0 at $(date)"
diff --git a/camera/camera-camera2-pipe-integration/build.gradle b/camera/camera-camera2-pipe-integration/build.gradle
index fe1548c..30e18b6 100644
--- a/camera/camera-camera2-pipe-integration/build.gradle
+++ b/camera/camera-camera2-pipe-integration/build.gradle
@@ -18,6 +18,7 @@
 import androidx.build.LibraryGroups
 import androidx.build.LibraryVersions
 import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
 import static androidx.build.dependencies.DependenciesKt.*
 
@@ -25,6 +26,7 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("kotlin-android")
+    id("kotlin-kapt")
 }
 
 apply from: "../camera-camera2-pipe/dependencies.gradle"
@@ -39,14 +41,21 @@
     releaseBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportRelease"))
     debugBundleInside(project(path: ':camera:camera-camera2-pipe', configuration: "exportDebug"))
 
+    // Classes and types that are needed at compile & runtime
     api("androidx.annotation:annotation:1.1.0")
     api(project(":camera:camera-core"))
+
+    // Classes and types that are only needed at runtime
+    implementation(project(":lifecycle:lifecycle-livedata-ktx"))
+    implementation(KOTLIN_COROUTINES_GUAVA)
     implementation(KOTLIN_STDLIB)
 
     // Since we jarjar CameraPipe, include the transitive dependencies as implementation
     implementation CAMERA_PIPE_DEPS.API
     implementation CAMERA_PIPE_DEPS.IMPLEMENTATION
 
+    kapt(DAGGER_COMPILER)
+
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
     androidTestImplementation(ANDROIDX_TEST_RUNNER)
     androidTestImplementation(KOTLIN_COROUTINES_ANDROID)
@@ -63,6 +72,20 @@
     }
 }
 
+kapt {
+    javacOptions {
+        option("-Adagger.fastInit=enabled")
+        option("-Adagger.fullBindingGraphValidation=ERROR")
+    }
+}
+
+// Allow usage of Kotlin's @OptIn.
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
+    }
+}
+
 androidx {
     name = "Jetpack Camera Camera Pipe Integration Library"
     publish = Publish.NONE
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
index 473559c..bb925bd 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/CameraPipeConfig.kt
@@ -16,9 +16,9 @@
 package androidx.camera.camera2.pipe.integration
 
 import androidx.camera.core.CameraXConfig
-import androidx.camera.camera2.pipe.integration.impl.CameraPipeFactory
-import androidx.camera.camera2.pipe.integration.impl.StreamConfigurationMap
-import androidx.camera.camera2.pipe.integration.impl.UseCaseConfigurationMap
+import androidx.camera.camera2.pipe.integration.adapter.CameraFactoryAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraSurfaceAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraUseCaseAdapter
 
 /**
  * Convenience class for generating a pre-populated CameraPipe based [CameraXConfig].
@@ -29,9 +29,9 @@
      */
     fun defaultConfig(): CameraXConfig {
         return CameraXConfig.Builder()
-            .setCameraFactoryProvider(::CameraPipeFactory)
-            .setDeviceSurfaceManagerProvider(::StreamConfigurationMap)
-            .setUseCaseConfigFactoryProvider(::UseCaseConfigurationMap)
+            .setCameraFactoryProvider(::CameraFactoryAdapter)
+            .setDeviceSurfaceManagerProvider(::CameraSurfaceAdapter)
+            .setUseCaseConfigFactoryProvider(::CameraUseCaseAdapter)
             .build()
     }
 }
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
new file mode 100644
index 0000000..dcf14ed5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraControlAdapter.kt
@@ -0,0 +1,160 @@
+/*
+ * 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.integration.adapter
+
+import android.annotation.SuppressLint
+import android.graphics.Rect
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.FocusMeteringResult
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Adapt the [CameraControlInternal] interface to [CameraPipe].
+ *
+ * This controller class maintains state as use-cases are attached / detached from the camera as
+ * well as providing access to other utility methods. The primary purpose of this class it to
+ * forward these interactions to the currently configured [UseCaseCamera].
+ */
+@CameraScope
+@OptIn(ExperimentalCoroutinesApi::class)
+class CameraControlAdapter @Inject constructor(
+    lazyCameraMetadata: Provider<CameraMetadata>,
+    private val cameraScope: CoroutineScope,
+    private val cameraState: CameraState,
+    private val useCaseManager: UseCaseManager
+) : CameraControlInternal {
+    private val cameraMetadata by lazy { lazyCameraMetadata.get() }
+    private var interopConfig: Config = MutableOptionsBundle.create()
+    private var imageCaptureFlashMode: Int = ImageCapture.FLASH_MODE_OFF
+
+    override fun getSensorRect(): Rect {
+        return cameraMetadata[CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE]!!
+    }
+
+    override fun addInteropConfig(config: Config) {
+        interopConfig = Config.mergeConfigs(config, interopConfig)
+    }
+
+    override fun clearInteropConfig() {
+        interopConfig = MutableOptionsBundle.create()
+    }
+
+    override fun getInteropConfig(): Config {
+        return interopConfig
+    }
+
+    override fun enableTorch(torch: Boolean): ListenableFuture<Void> {
+        // Launch UNDISPATCHED to preserve interaction order with the camera.
+        return cameraScope.launchAsVoidFuture(start = CoroutineStart.UNDISPATCHED) {
+            useCaseManager.camera?.let {
+                // Tell the camera to turn the torch on / off.
+                val result = it.enableTorchAsync(torch)
+
+                // Update the torch state.
+                withContext(Dispatchers.Main) {
+                    cameraState.torchState.value = when (torch) {
+                        true -> TorchState.ON
+                        false -> TorchState.OFF
+                    }
+                }
+
+                // Wait until the command is received by the camera.
+                result.await()
+            }
+        }
+    }
+
+    override fun startFocusAndMetering(
+        action: FocusMeteringAction
+    ): ListenableFuture<FocusMeteringResult> {
+        warn { "TODO: startFocusAndMetering is not yet supported" }
+        return Futures.immediateFuture(FocusMeteringResult.emptyInstance())
+    }
+
+    override fun cancelFocusAndMetering(): ListenableFuture<Void> {
+        warn { "TODO: cancelFocusAndMetering is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun setZoomRatio(ratio: Float): ListenableFuture<Void> {
+        // Note: The current implementation waits until the update has been *submitted* to the
+        //   camera, but does not wait for the total capture result.
+        warn { "TODO: setZoomRatio is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun setLinearZoom(linearZoom: Float): ListenableFuture<Void> {
+        warn { "TODO: setLinearZoom is not yet supported" }
+        return Futures.immediateFuture(null)
+    }
+
+    override fun getFlashMode(): Int {
+        return imageCaptureFlashMode
+    }
+
+    override fun setFlashMode(flashMode: Int) {
+        warn { "TODO: setFlashMode is not yet supported" }
+        this.imageCaptureFlashMode = flashMode
+    }
+
+    override fun triggerAf(): ListenableFuture<CameraCaptureResult> {
+        warn { "TODO: triggerAf is not yet supported" }
+        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+    }
+
+    override fun triggerAePrecapture(): ListenableFuture<CameraCaptureResult> {
+        warn { "TODO: triggerAePrecapture is not yet supported" }
+        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
+    }
+
+    override fun cancelAfAeTrigger(cancelAfTrigger: Boolean, cancelAePrecaptureTrigger: Boolean) {
+        warn { "TODO: cancelAfAeTrigger is not yet supported" }
+    }
+
+    @SuppressLint("UnsafeExperimentalUsageError")
+    override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> {
+        warn { "TODO: setExposureCompensationIndex is not yet supported" }
+        return Futures.immediateFuture(exposure)
+    }
+
+    override fun submitCaptureRequests(captureConfigs: MutableList<CaptureConfig>) {
+        warn { "TODO: submitCaptureRequests is not yet supported" }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
new file mode 100644
index 0000000..120f8b9
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraFactoryAdapter.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.integration.adapter
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.impl.Debug
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Timestamps
+import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
+import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.camera2.pipe.integration.config.CameraAppConfig
+import androidx.camera.camera2.pipe.integration.config.DaggerCameraAppComponent
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.impl.CameraFactory
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.CameraThreadConfig
+
+/**
+ * The [CameraFactoryAdapter] is responsible for creating the root dagger component that is used
+ * to share resources across Camera instances.
+ */
+class CameraFactoryAdapter(
+    context: Context,
+    threadConfig: CameraThreadConfig,
+    availableCamerasSelector: CameraSelector?
+) : CameraFactory {
+    private val appComponent: CameraAppComponent by lazy {
+        Debug.traceStart { "CameraFactoryAdapter#appComponent" }
+        val start = Timestamps.now()
+        val result = DaggerCameraAppComponent.builder()
+            .config(CameraAppConfig(context, threadConfig))
+            .build()
+        debug { "Created CameraFactoryAdapter in ${start.measureNow().formatMs()}" }
+        debug { "availableCamerasSelector: $availableCamerasSelector " }
+        Debug.traceStop()
+        result
+    }
+
+    init {
+        debug { "Created CameraFactoryAdapter" }
+    }
+
+    override fun getCamera(cameraId: String): CameraInternal =
+        appComponent.cameraBuilder()
+            .config(CameraConfig(CameraId(cameraId)))
+            .build()
+            .getCameraInternal()
+
+    override fun getAvailableCameraIds(): Set<String> = appComponent.getAvailableCameraIds()
+    override fun getCameraManager(): Any? = appComponent
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
new file mode 100644
index 0000000..442db04
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInfoAdapter.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.view.Surface
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.CameraCallbackMap
+import androidx.camera.camera2.pipe.integration.impl.CameraState
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.utils.CameraOrientationUtil
+import androidx.lifecycle.LiveData
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * Adapt the [CameraInfoInternal] interface to [CameraPipe].
+ */
+@SuppressLint(
+    "UnsafeExperimentalUsageError" // Suppressed due to experimental ExposureState
+)
+@CameraScope
+class CameraInfoAdapter @Inject constructor(
+    private val lazyCameraMetadata: Provider<CameraMetadata>,
+    private val cameraConfig: CameraConfig,
+    private val cameraState: CameraState,
+    private val cameraCallbackMap: CameraCallbackMap
+) : CameraInfoInternal {
+
+    private val cameraMetadata: CameraMetadata
+        get() = lazyCameraMetadata.get()
+
+    override fun getCameraId(): String = cameraConfig.cameraId.value
+    override fun getLensFacing(): Int? = cameraMetadata[CameraCharacteristics.LENS_FACING]
+    override fun getSensorRotationDegrees(): Int = getSensorRotationDegrees(Surface.ROTATION_0)
+    override fun hasFlashUnit(): Boolean =
+        cameraMetadata[CameraCharacteristics.FLASH_INFO_AVAILABLE]!!
+
+    override fun getSensorRotationDegrees(relativeRotation: Int): Int {
+        val sensorOrientation: Int = cameraMetadata[CameraCharacteristics.SENSOR_ORIENTATION]!!
+        val relativeRotationDegrees =
+            CameraOrientationUtil.surfaceRotationToDegrees(relativeRotation)
+        // Currently this assumes that a back-facing camera is always opposite to the screen.
+        // This may not be the case for all devices, so in the future we may need to handle that
+        // scenario.
+        val lensFacing = lensFacing
+        val isOppositeFacingScreen =
+            lensFacing != null && CameraSelector.LENS_FACING_BACK == lensFacing
+        return CameraOrientationUtil.getRelativeImageRotation(
+            relativeRotationDegrees,
+            sensorOrientation,
+            isOppositeFacingScreen
+        )
+    }
+
+    override fun getZoomState(): LiveData<ZoomState> = cameraState.zoomState
+    override fun getTorchState(): LiveData<Int> = cameraState.torchState
+    @SuppressLint("UnsafeExperimentalUsageError")
+    override fun getExposureState(): ExposureState = cameraState.exposureState.value!!
+
+    override fun addSessionCaptureCallback(executor: Executor, callback: CameraCaptureCallback) =
+        cameraCallbackMap.addCaptureCallback(callback, executor)
+    override fun removeSessionCaptureCallback(callback: CameraCaptureCallback) =
+        cameraCallbackMap.removeCaptureCallback(callback)
+
+    override fun getImplementationType(): String = "CameraPipe"
+    override fun toString(): String = "CameraInfoAdapter<$cameraConfig.cameraId>"
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
new file mode 100644
index 0000000..3174c2c
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraInternalAdapter.kt
@@ -0,0 +1,112 @@
+/*
+ * 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.integration.adapter
+
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.warn
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.impl.UseCaseManager
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import androidx.camera.core.impl.LiveDataObservable
+import androidx.camera.core.impl.Observable
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.utils.futures.Futures
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.atomicfu.atomic
+import javax.inject.Inject
+
+internal val defaultQuirks = Quirks(emptyList())
+internal val cameraAdapterIds = atomic(0)
+
+/**
+ * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
+ */
+@CameraScope
+class CameraInternalAdapter @Inject constructor(
+    config: CameraConfig,
+    private val useCaseManager: UseCaseManager,
+    private val cameraInfo: CameraInfoInternal,
+    private val cameraController: CameraControlInternal
+) : CameraInternal {
+    private val cameraId = config.cameraId
+    private val debugId = cameraAdapterIds.incrementAndGet()
+    private val cameraState = LiveDataObservable<CameraInternal.State>()
+
+    init {
+        cameraState.postValue(CameraInternal.State.CLOSED)
+
+        debug { "Created $this for $cameraId" }
+        // TODO: Consider preloading the list of camera ids and metadata.
+    }
+
+    override fun getCameraQuirks(): Quirks {
+        warn { "TODO: Quirks are not yet supported." }
+        return defaultQuirks
+    }
+
+    // Load / unload methods
+    override fun open() {
+        debug { "$this#open" }
+    }
+
+    override fun close() {
+        debug { "$this#close" }
+    }
+
+    override fun release(): ListenableFuture<Void> {
+        warn { "$this#release is not yet implemented." }
+        // TODO: Determine what the correct way to invoke release is.
+        return Futures.immediateFuture(null)
+    }
+
+    override fun getCameraInfoInternal(): CameraInfoInternal = cameraInfo
+    override fun getCameraState(): Observable<CameraInternal.State> = cameraState
+    override fun getCameraControlInternal(): CameraControlInternal = cameraController
+
+    // UseCase attach / detach behaviors.
+    override fun attachUseCases(useCasesToAdd: MutableCollection<UseCase>) {
+        useCaseManager.attach(useCasesToAdd.toList())
+    }
+
+    override fun detachUseCases(useCasesToRemove: MutableCollection<UseCase>) {
+        useCaseManager.detach(useCasesToRemove.toList())
+    }
+
+    // UseCase state callbacks
+    override fun onUseCaseActive(useCase: UseCase) {
+        useCaseManager.enable(useCase)
+    }
+
+    override fun onUseCaseUpdated(useCase: UseCase) {
+        useCaseManager.update(useCase)
+    }
+
+    override fun onUseCaseReset(useCase: UseCase) {
+        useCaseManager.update(useCase)
+    }
+
+    override fun onUseCaseInactive(useCase: UseCase) {
+        useCaseManager.disable(useCase)
+    }
+
+    override fun toString(): String = "CameraInternalAdapter<$cameraId>"
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
new file mode 100644
index 0000000..d026e65
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraSurfaceAdapter.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.integration.adapter
+
+import android.content.Context
+import android.graphics.ImageFormat
+import android.util.Size
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraAppComponent
+import androidx.camera.core.impl.CameraDeviceSurfaceManager
+import androidx.camera.core.impl.SurfaceConfig
+import androidx.camera.core.impl.SurfaceConfig.ConfigSize
+import androidx.camera.core.impl.UseCaseConfig
+
+internal val MAXIMUM_PREVIEW_SIZE = Size(1920, 1080)
+
+/**
+ * Adapt the [CameraDeviceSurfaceManager] interface to [CameraPipe].
+ *
+ * This class provides Context-specific utility methods for querying and computing supported
+ * outputs.
+ */
+class CameraSurfaceAdapter(
+    context: Context,
+    cameraComponent: Any?,
+    availableCameraIds: Set<String>
+) : CameraDeviceSurfaceManager {
+    private val component = cameraComponent as CameraAppComponent
+
+    init {
+        debug { "AvailableCameraIds = $availableCameraIds" }
+        debug { "Created StreamConfigurationMap from $context" }
+    }
+
+    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
+        // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
+        //   guaranteed stream configurations for this camera's support level.
+        return component.getAvailableCameraIds().contains(cameraId)
+    }
+
+    override fun transformSurfaceConfig(
+        cameraId: String,
+        imageFormat: Int,
+        size: Size
+    ): SurfaceConfig? {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        val configType = when (imageFormat) {
+            ImageFormat.YUV_420_888 -> SurfaceConfig.ConfigType.YUV
+            ImageFormat.JPEG -> SurfaceConfig.ConfigType.JPEG
+            ImageFormat.RAW_SENSOR -> SurfaceConfig.ConfigType.RAW
+            else -> SurfaceConfig.ConfigType.PRIV
+        }
+
+        val configSize = ConfigSize.PREVIEW
+        return SurfaceConfig.create(configType, configSize)
+    }
+
+    override fun getSuggestedResolutions(
+        cameraId: String,
+        existingSurfaces: List<SurfaceConfig>,
+        newUseCaseConfigs: List<UseCaseConfig<*>?>
+    ): Map<UseCaseConfig<*>, Size> {
+        // TODO: Many of the "find a stream combination that will work" is already provided by the
+        //   existing camera2 implementation, and this implementation should leverage that work.
+
+        val sizes: MutableMap<UseCaseConfig<*>, Size> = mutableMapOf()
+        for (config in newUseCaseConfigs) {
+            sizes[config as UseCaseConfig<*>] = MAXIMUM_PREVIEW_SIZE
+        }
+        return sizes
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
new file mode 100644
index 0000000..dcbcba5
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CameraUseCaseAdapter.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.integration.adapter
+
+import android.content.Context
+import android.graphics.Point
+import android.hardware.camera2.CameraDevice
+import android.util.Size
+import android.view.Display
+import android.view.WindowManager
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.impl.Log.info
+import androidx.camera.camera2.pipe.integration.impl.asLandscape
+import androidx.camera.camera2.pipe.integration.impl.minByArea
+import androidx.camera.camera2.pipe.integration.impl.toSize
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.Config
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.MutableOptionsBundle
+import androidx.camera.core.impl.OptionsBundle
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.UseCaseConfig
+import androidx.camera.core.impl.UseCaseConfigFactory
+
+/**
+ * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
+ *
+ * This includes things like default template and session parameters, as well as maximum resolution
+ * and aspect ratios for the display.
+ */
+class CameraUseCaseAdapter(context: Context) : UseCaseConfigFactory {
+
+    private val display: Display by lazy {
+        @Suppress("deprecation")
+        (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay!!
+    }
+
+    init {
+        if (context === context.applicationContext) {
+            info {
+                "The provided context ($context) is application scoped and will be used to infer " +
+                    "the default display for computing the default preview size, orientation, " +
+                    "and default aspect ratio for UseCase outputs."
+            }
+        }
+        debug { "Created UseCaseConfigurationMap" }
+    }
+
+    /**
+     * Returns the configuration for the given capture type, or `null` if the
+     * configuration cannot be produced.
+     */
+    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
+        debug { "Creating config for $captureType" }
+
+        val mutableConfig = MutableOptionsBundle.create()
+        val sessionBuilder = SessionConfig.Builder()
+        // TODO(b/114762170): Must set to preview here until we allow for multiple template
+        //  types
+        sessionBuilder.setTemplateType(CameraDevice.TEMPLATE_PREVIEW)
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG,
+            sessionBuilder.build()
+        )
+        val captureBuilder = CaptureConfig.Builder()
+        when (captureType) {
+            UseCaseConfigFactory.CaptureType.IMAGE_CAPTURE ->
+                captureBuilder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+            UseCaseConfigFactory.CaptureType.PREVIEW,
+            UseCaseConfigFactory.CaptureType.IMAGE_ANALYSIS,
+            UseCaseConfigFactory.CaptureType.VIDEO_CAPTURE ->
+                captureBuilder.templateType = CameraDevice.TEMPLATE_PREVIEW
+        }
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_DEFAULT_CAPTURE_CONFIG,
+            captureBuilder.build()
+        )
+
+        // Only CAPTURE_TYPE_IMAGE_CAPTURE has its own ImageCaptureOptionUnpacker. Other
+        // capture types all use the standard Camera2CaptureOptionUnpacker.
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER,
+            DefaultCaptureOptionsUnpacker
+        )
+        mutableConfig.insertOption(
+            UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER,
+            DefaultSessionOptionsUnpacker
+        )
+
+        if (captureType == UseCaseConfigFactory.CaptureType.PREVIEW) {
+            mutableConfig.insertOption(
+                ImageOutputConfig.OPTION_MAX_RESOLUTION,
+                getPreviewSize()
+            )
+        }
+
+        mutableConfig.insertOption(
+            ImageOutputConfig.OPTION_TARGET_ROTATION,
+            display.rotation
+        )
+        return OptionsBundle.from(mutableConfig)
+    }
+
+    /**
+     * Returns the device's screen resolution, or 1080p, whichever is smaller.
+     */
+    private fun getPreviewSize(): Size? {
+        val displaySize = Point()
+        display.getRealSize(displaySize)
+        return minByArea(MAXIMUM_PREVIEW_SIZE, displaySize.toSize().asLandscape())
+    }
+
+    object DefaultCaptureOptionsUnpacker : CaptureConfig.OptionUnpacker {
+        override fun unpack(config: UseCaseConfig<*>, builder: CaptureConfig.Builder) {
+            // Unused.
+        }
+    }
+
+    object DefaultSessionOptionsUnpacker : SessionConfig.OptionUnpacker {
+        override fun unpack(config: UseCaseConfig<*>, builder: SessionConfig.Builder) {
+            // Unused.
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
new file mode 100644
index 0000000..63f471f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CaptureResultAdapter.kt
@@ -0,0 +1,122 @@
+/*
+ * 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.integration.adapter
+
+import android.hardware.camera2.CaptureResult
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.impl.CAMERAX_TAG_BUNDLE
+import androidx.camera.core.impl.CameraCaptureMetaData.AeState
+import androidx.camera.core.impl.CameraCaptureMetaData.AfMode
+import androidx.camera.core.impl.CameraCaptureMetaData.AfState
+import androidx.camera.core.impl.CameraCaptureMetaData.AwbState
+import androidx.camera.core.impl.CameraCaptureMetaData.FlashState
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.TagBundle
+
+/**
+ * Adapts the [CameraCaptureResult] interface to [CameraPipe].
+ */
+class CaptureResultAdapter(
+    private val requestMetadata: RequestMetadata,
+    private val frameNumber: FrameNumber,
+    private val result: FrameInfo
+) : CameraCaptureResult {
+    override fun getAfMode(): AfMode =
+        when (val mode = result.metadata[CaptureResult.CONTROL_AF_MODE]) {
+            CaptureResult.CONTROL_AF_MODE_OFF,
+            CaptureResult.CONTROL_AF_MODE_EDOF -> AfMode.OFF
+            CaptureResult.CONTROL_AF_MODE_AUTO,
+            CaptureResult.CONTROL_AF_MODE_MACRO -> AfMode.ON_MANUAL_AUTO
+            CaptureResult.CONTROL_AF_MODE_CONTINUOUS_PICTURE,
+            CaptureResult.CONTROL_AF_MODE_CONTINUOUS_VIDEO -> AfMode.ON_CONTINUOUS_AUTO
+            null -> AfMode.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AF mode ($mode) for $frameNumber!" }
+                AfMode.UNKNOWN
+            }
+        }
+
+    override fun getAfState(): AfState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AF_STATE]) {
+            CaptureResult.CONTROL_AF_STATE_INACTIVE -> AfState.INACTIVE
+            CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED -> AfState.SCANNING
+            CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED -> AfState.LOCKED_FOCUSED
+            CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> AfState.LOCKED_NOT_FOCUSED
+            CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED -> AfState.FOCUSED
+            null -> AfState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AF state ($state) for $frameNumber!" }
+                AfState.UNKNOWN
+            }
+        }
+
+    override fun getAeState(): AeState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AE_STATE]) {
+            CaptureResult.CONTROL_AE_STATE_INACTIVE -> AeState.INACTIVE
+            CaptureResult.CONTROL_AE_STATE_SEARCHING,
+            CaptureResult.CONTROL_AE_STATE_PRECAPTURE -> AeState.SEARCHING
+            CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED -> AeState.FLASH_REQUIRED
+            CaptureResult.CONTROL_AE_STATE_CONVERGED -> AeState.CONVERGED
+            CaptureResult.CONTROL_AE_STATE_LOCKED -> AeState.LOCKED
+            null -> AeState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AE state ($state) for $frameNumber!" }
+                AeState.UNKNOWN
+            }
+        }
+
+    override fun getAwbState(): AwbState =
+        when (val state = result.metadata[CaptureResult.CONTROL_AWB_STATE]) {
+            CaptureResult.CONTROL_AWB_STATE_INACTIVE -> AwbState.INACTIVE
+            CaptureResult.CONTROL_AWB_STATE_SEARCHING -> AwbState.METERING
+            CaptureResult.CONTROL_AWB_STATE_CONVERGED -> AwbState.CONVERGED
+            CaptureResult.CONTROL_AWB_STATE_LOCKED -> AwbState.LOCKED
+            null -> AwbState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown AWB state ($state) for $frameNumber!" }
+                AwbState.UNKNOWN
+            }
+        }
+
+    override fun getFlashState(): FlashState =
+        when (val state = result.metadata[CaptureResult.FLASH_STATE]) {
+            CaptureResult.FLASH_STATE_UNAVAILABLE,
+            CaptureResult.FLASH_STATE_CHARGING -> FlashState.NONE
+            CaptureResult.FLASH_STATE_READY -> FlashState.READY
+            CaptureResult.FLASH_STATE_FIRED,
+            CaptureResult.FLASH_STATE_PARTIAL -> FlashState.FIRED
+            null -> FlashState.UNKNOWN
+            else -> {
+                Log.debug { "Unknown flash state ($state) for $frameNumber!" }
+                FlashState.UNKNOWN
+            }
+        }
+
+    override fun getTimestamp(): Long {
+        return result.metadata.getOrDefault(CaptureResult.SENSOR_TIMESTAMP, -1L)
+    }
+
+    override fun getTagBundle(): TagBundle {
+        return requestMetadata.getOrDefault(CAMERAX_TAG_BUNDLE, TagBundle.emptyBundle())
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
new file mode 100644
index 0000000..96c3dc8
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.integration.adapter
+
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.guava.asListenableFuture
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+/**
+ * This allows a java method that returns a ListenableFuture<Void> to be implemented by calling a
+ * suspend function.
+ *
+ * Exceptions thrown from the coroutine scope are propagated to the returned future.
+ * Canceling the future will attempt to cancel the coroutine.
+ */
+fun CoroutineScope.launchAsVoidFuture(
+    context: CoroutineContext = EmptyCoroutineContext,
+    start: CoroutineStart = CoroutineStart.DEFAULT,
+    block: suspend CoroutineScope.() -> Unit
+): ListenableFuture<Void> {
+    // TODO: This method currently uses guava.asListenableFuture. This may be an expensive
+    //  dependency to take on. We may need to evaluate this.
+    @Suppress("UNCHECKED_CAST")
+    return this.async(context = context, start = start, block = block)
+        .asListenableFuture() as ListenableFuture<Void>
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
new file mode 100644
index 0000000..bb05d8d
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ExposureStateAdapter.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import android.util.Range
+import android.util.Rational
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ExposureState
+
+internal val EMPTY_RANGE = Range(0, 0)
+
+/** Adapt [ExposureState] to a [CameraMetadata] instance. */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ExposureStateAdapter(
+    private val cameraMetadata: CameraMetadata,
+    private val exposureCompensation: Int
+) : ExposureState {
+    override fun isExposureCompensationSupported(): Boolean {
+        val range = exposureCompensationRange
+        return range.lower != 0 && range.upper != 0
+    }
+
+    override fun getExposureCompensationIndex(): Int = exposureCompensation
+    override fun getExposureCompensationStep(): Rational {
+        if (!isExposureCompensationSupported) {
+            return Rational.ZERO
+        }
+        return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP]!!
+    }
+
+    override fun getExposureCompensationRange(): Range<Int> {
+        return cameraMetadata[CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE]
+            ?: EMPTY_RANGE
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
new file mode 100644
index 0000000..4d42273
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/ZoomStateAdapter.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.integration.adapter
+
+import android.annotation.SuppressLint
+import android.hardware.camera2.CameraCharacteristics
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.core.ZoomState
+
+/**
+ * Adapt [ZoomState] to a [CameraMetadata] instance.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+class ZoomStateAdapter(
+    cameraMetadata: CameraMetadata,
+    private val zoomRatio: Float
+) : ZoomState {
+    // TODO: Zoom state has API-specific compat requirements. This is a placeholder for newer
+    //  android API versions and will require compat changes to support older versions.
+    private val zoomRange = cameraMetadata[CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE]!!
+
+    override fun getMaxZoomRatio(): Float = zoomRange.upper as Float
+    override fun getMinZoomRatio(): Float = zoomRange.lower as Float
+    override fun getLinearZoom(): Float {
+        val range = zoomRange.upper - zoomRange.lower
+        if (range > 0) {
+            return (zoomRatio - zoomRange.lower) / range
+        }
+        return 1.0f
+    }
+    override fun getZoomRatio(): Float = zoomRatio
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
index 8a6dfd3..f7a9ea2 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/package-info.java
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui.test
 
-import org.jetbrains.skiko.Library
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.adapter;
 
-fun initCompose() {
-    ComposeInit
-}
-
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
new file mode 100644
index 0000000..d8eadf0
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraAppConfig.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.integration.config
+
+import android.content.Context
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.core.impl.CameraThreadConfig
+import androidx.camera.core.impl.CameraFactory
+import dagger.Component
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/** Dependency bindings for adapting a [CameraFactory] instance to [CameraPipe] */
+@Module(
+    subcomponents = [CameraComponent::class]
+)
+abstract class CameraAppModule {
+    companion object {
+        @Singleton
+        @Provides
+        fun provideCameraPipe(context: Context): CameraPipe {
+            return CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
+        }
+
+        @Provides
+        fun provideAvailableCameraIds(cameraPipe: CameraPipe): Set<String> {
+            return cameraPipe.cameras().findAll().map { it.value }.toSet()
+        }
+    }
+}
+
+/** Configuration properties that are shared across this app process */
+@Module
+class CameraAppConfig(
+    private val context: Context,
+    private val threadConfig: CameraThreadConfig
+) {
+    @Provides
+    fun provideContext(): Context = context
+}
+
+/** Dagger component for Application (Process) scoped dependencies. */
+@Singleton
+@Component(
+    modules = [
+        CameraAppModule::class,
+        CameraAppConfig::class
+    ]
+)
+interface CameraAppComponent {
+    fun cameraBuilder(): CameraComponent.Builder
+    fun getAvailableCameraIds(): Set<String>
+
+    @Component.Builder
+    interface Builder {
+        fun config(config: CameraAppConfig): Builder
+        fun build(): CameraAppComponent
+    }
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
new file mode 100644
index 0000000..803d605
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/CameraConfig.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.integration.config
+
+import androidx.camera.camera2.pipe.CameraId
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInfoAdapter
+import androidx.camera.camera2.pipe.integration.adapter.CameraInternalAdapter
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.CameraInternal
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import javax.inject.Scope
+
+@Scope
+annotation class CameraScope
+
+/** Dependency bindings for adapting an individual [CameraInternal] instance to [CameraPipe] */
+@Module(
+    subcomponents = [UseCaseCameraComponent::class]
+)
+abstract class CameraModule {
+    companion object {
+        @CameraScope
+        @Provides
+        fun provideCameraCoroutineScope(cameraConfig: CameraConfig): CoroutineScope {
+            // TODO: Dispatchers.Default is the standard kotlin coroutine executor for background
+            //   work, but we may want to pass something in.
+            return CoroutineScope(
+                Job() +
+                    Dispatchers.Default +
+                    CoroutineName("CXCP-Camera-${cameraConfig.cameraId.value}")
+            )
+        }
+
+        @Provides
+        fun provideCameraMetadata(cameraPipe: CameraPipe, config: CameraConfig): CameraMetadata =
+            cameraPipe.cameras().awaitMetadata(config.cameraId)
+    }
+
+    @Binds
+    abstract fun bindCameraInternal(adapter: CameraInternalAdapter): CameraInternal
+
+    @Binds
+    abstract fun bindCameraInfoInternal(adapter: CameraInfoAdapter): CameraInfoInternal
+
+    @Binds
+    abstract fun bindCameraControlInternal(adapter: CameraControlAdapter): CameraControlInternal
+}
+
+/** Configuration properties used when creating a [CameraInternal] instance. */
+@Module
+class CameraConfig(val cameraId: CameraId) {
+    @Provides
+    fun provideCameraConfig(): CameraConfig = this
+}
+
+/** Dagger subcomponent for a single [CameraInternal] instance. */
+@CameraScope
+@Subcomponent(
+    modules = [
+        CameraModule::class,
+        CameraConfig::class
+    ]
+)
+interface CameraComponent {
+    @Subcomponent.Builder
+    interface Builder {
+        fun config(config: CameraConfig): Builder
+        fun build(): CameraComponent
+    }
+
+    fun getCameraInternal(): CameraInternal
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
new file mode 100644
index 0000000..8ea76cf
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/UseCaseCameraConfig.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.integration.config
+
+import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
+import androidx.camera.core.UseCase
+import dagger.Module
+import dagger.Provides
+import dagger.Subcomponent
+import javax.inject.Scope
+
+@Scope
+annotation class UseCaseCameraScope
+
+/** Dependency bindings for building a [UseCaseCamera] */
+@Module(includes = [UseCaseCamera.Bindings::class])
+abstract class UseCaseCameraModule {
+    // Used for dagger provider methods that are static.
+    companion object
+}
+
+/** Dagger module for binding the [UseCase]'s to the [UseCaseCamera]. */
+@Module
+class UseCaseCameraConfig(
+    private val useCases: List<UseCase>
+) {
+    @UseCaseCameraScope
+    @Provides
+    fun provideUseCaseList(): java.util.ArrayList<UseCase> {
+        return java.util.ArrayList(useCases)
+    }
+}
+
+/** Dagger subcomponent for a single [UseCaseCamera] instance. */
+@UseCaseCameraScope
+@Subcomponent(
+    modules = [
+        UseCaseCameraModule::class,
+        UseCaseCameraConfig::class
+    ]
+)
+interface UseCaseCameraComponent {
+    fun getUseCaseCamera(): UseCaseCamera
+
+    @Subcomponent.Builder
+    interface Builder {
+        fun config(config: UseCaseCameraConfig): Builder
+        fun build(): UseCaseCameraComponent
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
index 8a6dfd3..b7d2f6a 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/config/package-info.java
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui.test
 
-import org.jetbrains.skiko.Library
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.camera.camera2.pipe.integration.config;
 
-fun initCompose() {
-    ComposeInit
-}
-
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+import androidx.annotation.RestrictTo;
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
deleted file mode 100644
index 3d24397..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraAdaptor.kt
+++ /dev/null
@@ -1,101 +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.integration.impl
-
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.UseCase
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CameraInfoInternal
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.Observable
-import androidx.camera.core.impl.Quirks
-import androidx.camera.core.impl.utils.futures.Futures
-import com.google.common.util.concurrent.ListenableFuture
-
-/**
- * Adapt the [CameraInternal] class to one or more [CameraPipe] based Camera instances.
- */
-class CameraAdaptor(
-    private val cameraPipe: CameraPipe,
-    private val cameraId: CameraId
-) : CameraInternal {
-
-    init {
-        debug { "Created CameraAdaptor from $cameraPipe for $cameraId" }
-        // TODO: Consider preloading the list of camera ids and metadata.
-    }
-
-    // Load / unload methods
-    override fun open() {
-        TODO("Not yet implemented")
-    }
-
-    override fun close() {
-        TODO("Not yet implemented")
-    }
-
-    override fun release(): ListenableFuture<Void> {
-        // TODO: Determine what the correct way to invoke release is.
-        return Futures.immediateFuture(null)
-    }
-
-    // Static properties of this camera
-    override fun getCameraInfoInternal(): CameraInfoInternal {
-        TODO("Not yet implemented")
-    }
-
-    override fun getCameraQuirks(): Quirks {
-        TODO("Not yet implemented")
-    }
-
-    // Controls for interacting with or observing the state of the camera.
-    override fun getCameraState(): Observable<CameraInternal.State> {
-        TODO("Not yet implemented")
-    }
-
-    override fun getCameraControlInternal(): CameraControlInternal {
-        TODO("Not yet implemented")
-    }
-
-    // UseCase attach / detach behaviors.
-    override fun attachUseCases(useCases: MutableCollection<UseCase>) {
-        TODO("Not yet implemented")
-    }
-
-    override fun detachUseCases(useCases: MutableCollection<UseCase>) {
-        TODO("Not yet implemented")
-    }
-
-    // UseCase state callbacks
-    override fun onUseCaseActive(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseUpdated(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseReset(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-
-    override fun onUseCaseInactive(useCase: UseCase) {
-        TODO("Not yet implemented")
-    }
-}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
new file mode 100644
index 0000000..1619e7f
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraCallbackMap.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.integration.impl
+
+import android.hardware.camera2.CaptureFailure
+import androidx.camera.camera2.pipe.FrameInfo
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestMetadata
+import androidx.camera.camera2.pipe.integration.adapter.CaptureResultAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.impl.CameraCaptureCallback
+import androidx.camera.core.impl.CameraCaptureFailure
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/**
+ * A map of [CameraCaptureCallback] that are invoked on each [Request].
+ */
+@CameraScope
+class CameraCallbackMap @Inject constructor() : Request.Listener {
+    private val callbackMap = mutableMapOf<CameraCaptureCallback, Executor>()
+    @Volatile
+    private var callbacks: Map<CameraCaptureCallback, Executor> = mapOf()
+
+    fun addCaptureCallback(callback: CameraCaptureCallback, executor: Executor) {
+        check(!callbacks.contains(callback)) { "$callback was already registered!" }
+
+        synchronized(callbackMap) {
+            callbackMap[callback] = executor
+            callbacks = callbackMap.toMap()
+        }
+    }
+
+    fun removeCaptureCallback(callback: CameraCaptureCallback) {
+        synchronized(callbackMap) {
+            callbackMap.remove(callback)
+            callbacks = callbackMap.toMap()
+        }
+    }
+
+    override fun onComplete(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        result: FrameInfo
+    ) {
+        val captureResult = CaptureResultAdapter(requestMetadata, frameNumber, result)
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureCompleted(captureResult) }
+        }
+    }
+
+    override fun onFailed(
+        requestMetadata: RequestMetadata,
+        frameNumber: FrameNumber,
+        captureFailure: CaptureFailure
+    ) {
+        val failure = CameraCaptureFailure(CameraCaptureFailure.Reason.ERROR)
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureFailed(failure) }
+        }
+    }
+
+    override fun onAborted(request: Request) {
+        for ((callback, executor) in callbacks) {
+            executor.execute { callback.onCaptureCancelled() }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
deleted file mode 100644
index 071da5d..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraPipeFactory.kt
+++ /dev/null
@@ -1,77 +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.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Debug
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Timestamps
-import androidx.camera.camera2.pipe.impl.Timestamps.measureNow
-import androidx.camera.camera2.pipe.impl.Timestamps.formatMs
-import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraInternal
-import androidx.camera.core.impl.CameraThreadConfig
-
-/**
- * The CameraPipeCameraFactory is responsible for creating and configuring CameraPipe for CameraX.
- */
-class CameraPipeFactory(
-    context: Context,
-    threadConfig: CameraThreadConfig
-) : CameraFactory {
-    // Lazily create and configure a CameraPipe instance.
-    private val cameraPipe: CameraPipe by lazy {
-        Debug.traceStart { "CameraPipeCameraFactory#cameraPipe" }
-        val result: CameraPipe?
-        val start = Timestamps.now()
-
-        // TODO: CameraPipe should find a way to make sure callbacks are executed on the configured
-        //   executors that are provided in `threadConfig`
-        debug { "TODO: Use $threadConfig if defined" }
-
-        result = CameraPipe(CameraPipe.Config(appContext = context.applicationContext))
-        debug { "Created CameraPipe in ${start.measureNow().formatMs()}" }
-        Debug.traceStop()
-        result
-    }
-
-    init {
-        debug { "Created CameraPipeCameraFactory" }
-        // TODO: Consider preloading the list of camera ids and metadata.
-    }
-
-    override fun getCamera(cameraId: String): CameraInternal {
-        // TODO: The CameraInternal object is an facade that covers most of the high level camera
-        //   state and interactions. CameraInternal objects are persistent across camera switches.
-
-        return CameraAdaptor(cameraPipe, CameraId(cameraId))
-    }
-
-    override fun getAvailableCameraIds(): Set<String> {
-        // TODO: This may need some amount of work to limit the returned values well behaved "Front"
-        //   and "Back" camera devices.
-        return cameraPipe.cameras().findAll().map { it.value }.toSet()
-    }
-
-    override fun getCameraManager(): Any? {
-        // Note: This object is passed around as an untyped parameter when constructing a few
-        // objects (Such as `DeviceSurfaceManagerProvider`). It's better to rely on the parameter
-        // passing than to try to turn this object into a singleton.
-        return cameraPipe
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
new file mode 100644
index 0000000..e66deba
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/CameraState.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.integration.impl
+
+import android.annotation.SuppressLint
+import androidx.camera.camera2.pipe.CameraMetadata
+import androidx.camera.camera2.pipe.integration.adapter.ExposureStateAdapter
+import androidx.camera.camera2.pipe.integration.adapter.ZoomStateAdapter
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.core.ExposureState
+import androidx.camera.core.ZoomState
+import androidx.lifecycle.MutableLiveData
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * [CameraState] caches and updates based on callbacks from the active CameraGraph.
+ */
+@SuppressLint("UnsafeExperimentalUsageError")
+@CameraScope
+class CameraState @Inject constructor(
+    private val cameraMetadata: Provider<CameraMetadata>,
+) {
+    val torchState = MutableLiveData<Int>()
+    val zoomState by lazy {
+        MutableLiveData<ZoomState>(
+            ZoomStateAdapter(
+                cameraMetadata.get(),
+                1.0f
+            )
+        )
+    }
+    val exposureState by lazy {
+        MutableLiveData<ExposureState>(
+            ExposureStateAdapter(
+                cameraMetadata.get(),
+                0
+            )
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
new file mode 100644
index 0000000..aecf6ab
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Sizes.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.integration.impl
+
+import android.graphics.Point
+import android.util.Size
+
+fun Size.area(): Int = this.width * this.height
+fun Size.asLandscape(): Size =
+    if (this.width >= this.height) this else Size(this.height, this.width)
+fun Size.asPortrait(): Size =
+    if (this.width <= this.height) this else Size(this.height, this.width)
+
+fun minByArea(left: Size, right: Size) = if (left.area() < right.area()) left else right
+fun maxByArea(left: Size, right: Size) = if (left.area() > right.area()) left else right
+
+fun Point.area(): Int = this.x * this.y
+fun Point.toSize() = Size(this.x, this.y)
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
deleted file mode 100644
index 56b5966..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StreamConfigurationMap.kt
+++ /dev/null
@@ -1,64 +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.integration.impl
-
-import android.content.Context
-import android.util.Size
-import androidx.camera.camera2.pipe.CameraId
-import androidx.camera.camera2.pipe.CameraPipe
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.core.impl.CameraDeviceSurfaceManager
-import androidx.camera.core.impl.SurfaceConfig
-import androidx.camera.core.impl.UseCaseConfig
-
-/**
- * Provide utilities for interacting with the set of guaranteed stream combinations.
- */
-class StreamConfigurationMap(context: Context, cameraManager: Any?) : CameraDeviceSurfaceManager {
-    private val cameraPipe: CameraPipe = cameraManager as CameraPipe
-
-    init {
-        debug { "Created StreamConfigurationMap from $context" }
-    }
-
-    override fun checkSupported(cameraId: String, surfaceConfigList: List<SurfaceConfig>): Boolean {
-        // TODO: This method needs to check to see if the list of SurfaceConfig's is in the map of
-        //   guaranteed stream configurations for this camera's support level.
-        return cameraPipe.cameras().findAll().contains(CameraId(cameraId))
-    }
-
-    override fun transformSurfaceConfig(
-        cameraId: String,
-        imageFormat: Int,
-        size: Size
-    ): SurfaceConfig? {
-        // TODO: Many of the "find a stream combination that will work" is already provided by the
-        //   existing camera2 implementation, and this implementation should leverage that work.
-
-        TODO("Not Implemented")
-    }
-
-    override fun getSuggestedResolutions(
-        cameraId: String,
-        existingSurfaces: List<SurfaceConfig>,
-        newUseCaseConfigs: List<UseCaseConfig<*>?>
-    ): Map<UseCaseConfig<*>, Size> {
-        // TODO: Many of the "find a stream combination that will work" is already provided by the
-        //   existing camera2 implementation, and this implementation should leverage that work.
-
-        TODO("Not Implemented")
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
similarity index 68%
rename from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
rename to camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
index 8a6dfd3..75a1976 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/Tags.kt
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui.test
 
-import org.jetbrains.skiko.Library
+package androidx.camera.camera2.pipe.integration.impl
 
-fun initCompose() {
-    ComposeInit
-}
+import androidx.camera.camera2.pipe.Metadata
+import androidx.camera.core.impl.TagBundle
 
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+/** Custom tags that can be passed used by CameraPipe */
+public val CAMERAX_TAG_BUNDLE = Metadata.Key.create<TagBundle>("camerax.tag_bundle")
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
new file mode 100644
index 0000000..088855a
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseCamera.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.integration.impl
+
+import android.hardware.camera2.CameraDevice
+import androidx.camera.camera2.pipe.CameraGraph
+import androidx.camera.camera2.pipe.CameraPipe
+import androidx.camera.camera2.pipe.FrameNumber
+import androidx.camera.camera2.pipe.Request
+import androidx.camera.camera2.pipe.RequestTemplate
+import androidx.camera.camera2.pipe.StreamConfig
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.StreamId
+import androidx.camera.camera2.pipe.StreamType
+import androidx.camera.camera2.pipe.TorchState
+import androidx.camera.camera2.pipe.impl.Log.debug
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.DeferrableSurface
+import dagger.Module
+import dagger.Provides
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+
+internal val useCaseCameraIds = atomic(0)
+
+/**
+ * API for interacting with a [CameraGraph] that has been configured with a set of [UseCase]'s
+ */
+class UseCaseCamera(
+    private val cameraGraph: CameraGraph,
+    private val useCases: List<UseCase>,
+    private val surfaceToStreamMap: Map<DeferrableSurface, StreamId>,
+    private val cameraScope: CoroutineScope
+) {
+    private val debugId = useCaseCameraIds.incrementAndGet()
+
+    private var _activeUseCases = setOf<UseCase>()
+
+    var activeUseCases: Set<UseCase>
+        get() = _activeUseCases
+        set(value) {
+            // Note: This may be called with the same set of values that was previously set. This
+            // is used as a signal to indicate the properties of the UseCase may have changed.
+            _activeUseCases = value
+            updateUseCases()
+        }
+
+    init {
+        debug { "Configured $this for $useCases" }
+    }
+
+    fun close() {
+        debug { "Closing $this" }
+        cameraGraph.close()
+    }
+
+    suspend fun enableTorchAsync(enabled: Boolean): Deferred<FrameNumber> {
+        return cameraGraph.acquireSession().use {
+            it.setTorch(
+                when (enabled) {
+                    true -> TorchState.ON
+                    false -> TorchState.OFF
+                }
+            )
+        }
+    }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    private fun updateUseCases() {
+        val repeatingStreamIds = mutableSetOf<StreamId>()
+        for (useCase in activeUseCases) {
+            val repeatingCapture = useCase.sessionConfig?.repeatingCaptureConfig
+            if (repeatingCapture != null) {
+                for (deferrableSurface in repeatingCapture.surfaces) {
+                    val streamId = surfaceToStreamMap[deferrableSurface]
+                    if (streamId != null) {
+                        repeatingStreamIds.add(streamId)
+                    }
+                }
+            }
+        }
+
+        // TODO: This needs to aggregate the current parameters and pass them to the request.
+
+        // In order to preserve ordering, this starts acquiring the session on the current thread,
+        // and will only switch to the cameraScope threads if it needs to suspend. This is important
+        // because access to the cameraGraph is well ordered, and if the coroutine suspends, it will
+        // resume in the order it accessed the cameraGraph.
+        cameraScope.launch(start = CoroutineStart.UNDISPATCHED) {
+            cameraGraph.acquireSession().use {
+                it.setRepeating(
+                    Request(
+                        streams = repeatingStreamIds.toList()
+                    )
+                )
+            }
+        }
+    }
+
+    override fun toString(): String = "UseCaseCamera-$debugId"
+
+    @Module
+    class Bindings {
+        companion object {
+            @UseCaseCameraScope
+            @Provides
+            fun provideCameraGraphController(
+                cameraPipe: CameraPipe,
+                useCases: java.util.ArrayList<UseCase>,
+                cameraConfig: CameraConfig,
+                callbackMap: CameraCallbackMap,
+                coroutineScope: CoroutineScope,
+            ): UseCaseCamera {
+                val streamConfigs = mutableListOf<StreamConfig>()
+                val useCaseMap = mutableMapOf<StreamConfig, UseCase>()
+
+                // TODO: This may need to combine outputs that are (or will) share the same output
+                //  imageReader or surface. Right now, each UseCase gets its own [StreamConfig]
+                // TODO: useCases only have a single `attachedSurfaceResolution`, yet they have a
+                //  list of deferrableSurfaces.
+                for (useCase in useCases) {
+                    val config = StreamConfig(
+                        size = useCase.attachedSurfaceResolution!!,
+                        format = StreamFormat(useCase.imageFormat),
+                        camera = cameraConfig.cameraId,
+                        type = StreamType.SURFACE,
+                        deferrable = false
+                    )
+                    streamConfigs.add(config)
+                    useCaseMap[config] = useCase
+                }
+
+                // Build up a config (using TEMPLATE_PREVIEW by default)
+                val config = CameraGraph.Config(
+                    camera = cameraConfig.cameraId,
+                    streams = streamConfigs,
+                    listeners = listOf(callbackMap),
+                    template = RequestTemplate(CameraDevice.TEMPLATE_PREVIEW)
+                )
+                val graph = cameraPipe.create(config)
+
+                val surfaceToStreamMap = mutableMapOf<DeferrableSurface, StreamId>()
+                for ((streamConfig, useCase) in useCaseMap) {
+                    val stream = graph.streams[streamConfig]
+                    val useCaseSessionConfig = useCase.sessionConfig
+
+                    // TODO: UseCases have inconsistent opinions about how surfaces are handled,
+                    //  this code assumes only a single surface per UseCase.
+                    val deferredSurfaces = useCaseSessionConfig?.surfaces
+                    if (stream != null && deferredSurfaces != null && deferredSurfaces.size == 1) {
+                        val deferredSurface = deferredSurfaces[0]
+                        graph.setSurface(stream.id, deferredSurface.surface.get())
+                        surfaceToStreamMap[deferredSurface] = stream.id
+                    }
+                }
+
+                graph.start()
+                return UseCaseCamera(graph, useCases, surfaceToStreamMap, coroutineScope)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
deleted file mode 100644
index a0ddab2..0000000
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseConfigurationMap.kt
+++ /dev/null
@@ -1,50 +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.integration.impl
-
-import android.content.Context
-import androidx.camera.camera2.pipe.impl.Log.debug
-import androidx.camera.camera2.pipe.impl.Log.info
-import androidx.camera.core.impl.Config
-import androidx.camera.core.impl.UseCaseConfigFactory
-
-/**
- * This class builds [Config] objects for a given [UseCaseConfigFactory.CaptureType].
- *
- * This includes things like default template and session parameters, as well as maximum resolution
- * and aspect ratios for the display.
- */
-class UseCaseConfigurationMap(context: Context) : UseCaseConfigFactory {
-    init {
-        if (context === context.applicationContext) {
-            info {
-                "The provided context ($context) is application scoped and will be used to infer " +
-                    "the default display for computing the default preview size, orientation, " +
-                    "and default aspect ratio for UseCase outputs."
-            }
-        }
-        debug { "Created UseCaseConfigurationMap" }
-    }
-
-    /**
-     * Returns the configuration for the given capture type, or `null` if the configuration
-     * cannot be produced.
-     */
-    override fun getConfig(captureType: UseCaseConfigFactory.CaptureType): Config? {
-        TODO("Not Implemented")
-    }
-}
\ No newline at end of file
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
new file mode 100644
index 0000000..85f4155
--- /dev/null
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.integration.impl
+
+import androidx.camera.camera2.pipe.impl.Log
+import androidx.camera.camera2.pipe.integration.config.CameraConfig
+import androidx.camera.camera2.pipe.integration.config.CameraScope
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
+import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
+import androidx.camera.core.UseCase
+import javax.inject.Inject
+
+/**
+ * This class keeps track of the currently attached and active [UseCase]'s for a specific camera.
+ */
+@CameraScope
+class UseCaseManager @Inject constructor(
+    private val cameraConfig: CameraConfig,
+    private val builder: UseCaseCameraComponent.Builder
+) {
+    private val attachedUseCases = mutableListOf<UseCase>()
+    private val enabledUseCases = mutableSetOf<UseCase>()
+
+    @Volatile
+    private var _activeComponent: UseCaseCameraComponent? = null
+    val camera: UseCaseCamera?
+        get() = _activeComponent?.getUseCaseCamera()
+
+    fun attach(useCases: List<UseCase>) {
+        if (useCases.isEmpty()) {
+            Log.warn { "Attach [] from $this (Ignored)" }
+            return
+        }
+        Log.debug { "Attaching $useCases from $this" }
+
+        var modified = false
+        for (useCase in useCases) {
+            if (!attachedUseCases.contains(useCase)) {
+                attachedUseCases.add(useCase)
+                modified = true
+            }
+        }
+
+        if (modified) {
+            start(attachedUseCases)
+        }
+    }
+
+    fun detach(useCases: List<UseCase>) {
+        if (useCases.isEmpty()) {
+            Log.warn { "Detaching [] from $this (Ignored)" }
+            return
+        }
+        Log.debug { "Detaching $useCases from $this" }
+
+        var modified = false
+        for (useCase in useCases) {
+            modified = attachedUseCases.remove(useCase) || modified
+        }
+
+        // TODO: We might only want to tear down when the number of attached use cases goes to
+        //  zero. If a single UseCase is removed, we could deactivate it?
+        if (modified) {
+            start(attachedUseCases)
+        }
+    }
+
+    fun enable(useCase: UseCase) {
+        if (enabledUseCases.add(useCase)) {
+            invalidate()
+        }
+    }
+
+    fun disable(useCase: UseCase) {
+        if (enabledUseCases.remove(useCase)) {
+            invalidate()
+        }
+    }
+
+    fun update(useCase: UseCase) {
+        if (attachedUseCases.contains(useCase)) {
+            invalidate()
+        }
+    }
+
+    override fun toString(): String = "UseCaseManager<${cameraConfig.cameraId}>"
+
+    private fun invalidate() {
+        camera?.let {
+            it.activeUseCases = enabledUseCases.toSet()
+        }
+    }
+
+    private fun start(newUseCases: List<UseCase>) {
+        val useCases = newUseCases.toList()
+
+        // Close prior camera graph
+        camera.let {
+            _activeComponent = null
+            it?.close()
+        }
+
+        // Update list of active useCases
+        if (useCases.isEmpty()) {
+            return
+        }
+
+        // Create and configure the new camera component.
+        _activeComponent = builder.config(UseCaseCameraConfig(useCases)).build()
+        invalidate()
+    }
+}
\ No newline at end of file
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 c7bb3b0..d514661 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,9 @@
 import androidx.camera.camera2.pipe.impl.CameraPipeComponent
 import androidx.camera.camera2.pipe.impl.CameraPipeConfigModule
 import androidx.camera.camera2.pipe.impl.DaggerCameraPipeComponent
+import kotlinx.atomicfu.atomic
+
+internal val cameraPipeIds = atomic(0)
 
 /**
  * [CameraPipe] is the top level scope for all interactions with a Camera2 camera.
@@ -33,6 +36,7 @@
  * the [CameraGraph] interface.
  */
 class CameraPipe(config: Config) {
+    private val debugId = cameraPipeIds.incrementAndGet()
     private val component: CameraPipeComponent = DaggerCameraPipeComponent.builder()
         .cameraPipeConfigModule(CameraPipeConfigModule(config))
         .build()
@@ -63,4 +67,6 @@
         val appContext: Context,
         val cameraThread: HandlerThread? = null
     )
+
+    override fun toString(): String = "CameraPipe-$debugId"
 }
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 0c3cdda..f961304 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
@@ -47,7 +47,7 @@
              * This will create a new Key instance, and will check to see that the key has not been
              * previously created somewhere else.
              */
-            internal fun <T> create(name: String): Key<T> {
+            fun <T> create(name: String): Key<T> {
                 synchronized(keys) {
                     check(keys.add(name)) { "$name is already defined!" }
                 }
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
index 5204cc8..3a87a61 100644
--- 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
@@ -277,7 +277,7 @@
 
                 val surface = surfaceMap[stream]
                 if (surface != null) {
-                    Log.debug { "  Binding $surface to $stream" }
+                    Log.debug { "  Binding $stream to $surface" }
 
                     // TODO(codelogic) There should be a more efficient way to do these lookups than
                     // having two maps.
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
index 633e483..de75e09 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraImplTest.java
@@ -46,14 +46,12 @@
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureResult;
-import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraStateRegistry;
-import androidx.camera.core.impl.CameraThreadConfig;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.DeferrableSurface;
 import androidx.camera.core.impl.ImmediateSurface;
@@ -112,7 +110,6 @@
             CameraInternal.State.OPEN,
             CameraInternal.State.RELEASED));
 
-    private static CameraFactory sCameraFactory;
     static ExecutorService sCameraExecutor;
 
     @Rule
@@ -130,13 +127,11 @@
     SemaphoreReleasingCamera2Callbacks.SessionStateCallback mSessionStateCallback;
 
     @BeforeClass
-    public static void classSetup() {
+    public static void classSetup() throws InitializationException {
         sCameraHandlerThread = new HandlerThread("cameraThread");
         sCameraHandlerThread.start();
         sCameraHandler = HandlerCompat.createAsync(sCameraHandlerThread.getLooper());
         sCameraExecutor = CameraXExecutors.newHandlerExecutor(sCameraHandler);
-        sCameraFactory = new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
-                CameraThreadConfig.create(sCameraExecutor, sCameraHandler));
     }
 
     @AfterClass
@@ -145,14 +140,18 @@
     }
 
     @Before
-    public void setup() throws CameraUnavailableException {
+    public void setup() throws Exception {
         mMockOnImageAvailableListener = Mockito.mock(ImageReader.OnImageAvailableListener.class);
         mSessionStateCallback = new SemaphoreReleasingCamera2Callbacks.SessionStateCallback();
         mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
         mSemaphore = new Semaphore(0);
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
-        mCamera2CameraImpl = new Camera2CameraImpl(
-                CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+        CameraManagerCompat cameraManagerCompat =
+                CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+        Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+                mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
+        mCamera2CameraImpl = new Camera2CameraImpl(cameraManagerCompat, mCameraId,
+                camera2CameraInfo,
                 mCameraStateRegistry, sCameraExecutor, sCameraHandler);
     }
 
@@ -513,7 +512,7 @@
     @Test
     public void cameraTransitionsThroughPendingState_whenNoCamerasAvailable() {
         @SuppressWarnings("unchecked") // Cannot mock generic type inline
-                Observable.Observer<CameraInternal.State> mockObserver =
+        Observable.Observer<CameraInternal.State> mockObserver =
                 mock(Observable.Observer.class);
 
         // Ensure real camera can't open due to max cameras being open
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
index 28660f6..a053541 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/ExposureDeviceTest.java
@@ -35,6 +35,7 @@
 import android.hardware.camera2.TotalCaptureResult;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.util.Log;
 import android.util.Size;
 import android.view.Surface;
 
@@ -44,7 +45,6 @@
 import androidx.camera.camera2.internal.util.SemaphoreReleasingCamera2Callbacks;
 import androidx.camera.camera2.interop.Camera2Interop;
 import androidx.camera.core.CameraSelector;
-import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.ExperimentalExposureCompensation;
 import androidx.camera.core.ExposureState;
 import androidx.camera.core.ImageCapture;
@@ -131,12 +131,14 @@
     }
 
     @Before
-    public void setup() throws CameraUnavailableException {
+    public void setup() throws Exception {
         // TODO(b/162296654): Workaround the google_3a specific behavior.
         assumeFalse("Cuttlefish uses google_3a v1 or v2 it might fail to set EV before "
                 + "first AE converge.", android.os.Build.MODEL.contains("Cuttlefish"));
         assumeFalse("Pixel uses google_3a v1 or v2 it might fail to set EV before "
                 + "first AE converge.", android.os.Build.MODEL.contains("Pixel"));
+        assumeFalse("Disable Nexus 5 in postsubmit for b/173743705",
+                android.os.Build.MODEL.contains("Nexus 5") && !Log.isLoggable("MH", Log.DEBUG));
 
         assumeTrue(CameraUtil.deviceHasCamera());
         assumeTrue(CameraUtil.hasCameraWithLensFacing(DEFAULT_LENS_FACING));
@@ -144,8 +146,13 @@
         mCameraId = CameraUtil.getCameraIdWithLensFacing(DEFAULT_LENS_FACING);
         mSemaphore = new Semaphore(0);
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_AVAILABLE_CAMERA_COUNT);
+        CameraManagerCompat cameraManagerCompat =
+                CameraManagerCompat.from(ApplicationProvider.getApplicationContext());
+        Camera2CameraInfoImpl camera2CameraInfo = new Camera2CameraInfoImpl(
+                mCameraId, cameraManagerCompat.getCameraCharacteristicsCompat(mCameraId));
         mCamera2CameraImpl = new Camera2CameraImpl(
                 CameraManagerCompat.from(ApplicationProvider.getApplicationContext()), mCameraId,
+                camera2CameraInfo,
                 mCameraStateRegistry, sCameraExecutor, sCameraHandler);
 
         mCameraInfoInternal = mCamera2CameraImpl.getCameraInfoInternal();
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
index 4b02c8f..0df28bd 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/Camera2Config.java
@@ -47,9 +47,10 @@
 
         // Create the DeviceSurfaceManager for Camera2
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> {
+                (context, cameraManager, availableCameraIds) -> {
                     try {
-                        return new Camera2DeviceSurfaceManager(context, cameraManager);
+                        return new Camera2DeviceSurfaceManager(context, cameraManager,
+                                availableCameraIds);
                     } catch (CameraUnavailableException e) {
                         throw new InitializationException(e);
                     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
index 2236387..61c4069 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraFactory.java
@@ -19,17 +19,21 @@
 import android.content.Context;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
 import androidx.camera.core.impl.CameraFactory;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CameraStateRegistry;
 import androidx.camera.core.impl.CameraThreadConfig;
 
-import java.util.Arrays;
+import java.util.HashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -38,40 +42,57 @@
 public final class Camera2CameraFactory implements CameraFactory {
     private static final int DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS = 1;
     private final CameraThreadConfig mThreadConfig;
-
     private final CameraStateRegistry mCameraStateRegistry;
     private final CameraManagerCompat mCameraManager;
+    private final List<String> mAvailableCameraIds;
+    private final Map<String, Camera2CameraInfoImpl> mCameraInfos = new HashMap<>();
 
     /** Creates a Camera2 implementation of CameraFactory */
     public Camera2CameraFactory(@NonNull Context context,
-            @NonNull CameraThreadConfig threadConfig) {
+            @NonNull CameraThreadConfig threadConfig,
+            @Nullable CameraSelector availableCamerasSelector) throws InitializationException {
         mThreadConfig = threadConfig;
         mCameraStateRegistry = new CameraStateRegistry(DEFAULT_ALLOWED_CONCURRENT_OPEN_CAMERAS);
         mCameraManager = CameraManagerCompat.from(context, mThreadConfig.getSchedulerHandler());
+
+        mAvailableCameraIds = CameraSelectionOptimizer
+                .getSelectedAvailableCameraIds(this, availableCamerasSelector);
     }
 
     @Override
     @NonNull
     public CameraInternal getCamera(@NonNull String cameraId) throws CameraUnavailableException {
-        if (!getAvailableCameraIds().contains(cameraId)) {
+        if (!mAvailableCameraIds.contains(cameraId)) {
             throw new IllegalArgumentException(
                     "The given camera id is not on the available camera id list.");
         }
-        return new Camera2CameraImpl(mCameraManager, cameraId, mCameraStateRegistry,
-                mThreadConfig.getCameraExecutor(), mThreadConfig.getSchedulerHandler());
+        return new Camera2CameraImpl(mCameraManager,
+                cameraId,
+                getCameraInfo(cameraId),
+                mCameraStateRegistry,
+                mThreadConfig.getCameraExecutor(),
+                mThreadConfig.getSchedulerHandler());
     }
 
-    @Override
-    @NonNull
-    public Set<String> getAvailableCameraIds() throws CameraUnavailableException {
-        List<String> camerasList;
+    Camera2CameraInfoImpl getCameraInfo(@NonNull String cameraId)
+            throws CameraUnavailableException {
         try {
-            camerasList = Arrays.asList(mCameraManager.getCameraIdList());
+            Camera2CameraInfoImpl camera2CameraInfoImpl = mCameraInfos.get(cameraId);
+            if (camera2CameraInfoImpl == null) {
+                camera2CameraInfoImpl = new Camera2CameraInfoImpl(
+                        cameraId, mCameraManager.getCameraCharacteristicsCompat(cameraId));
+                mCameraInfos.put(cameraId, camera2CameraInfoImpl);
+            }
+            return camera2CameraInfoImpl;
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
+    }
+    @Override
+    @NonNull
+    public Set<String> getAvailableCameraIds() {
         // Use a LinkedHashSet to preserve order
-        return new LinkedHashSet<>(camerasList);
+        return new LinkedHashSet<>(mAvailableCameraIds);
     }
 
     @NonNull
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
index 3cd5d0d..7adafc2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraImpl.java
@@ -185,6 +185,7 @@
      */
     Camera2CameraImpl(@NonNull CameraManagerCompat cameraManager,
             @NonNull String cameraId,
+            @NonNull Camera2CameraInfoImpl cameraInfoImpl,
             @NonNull CameraStateRegistry cameraStateRegistry,
             @NonNull Executor executor,
             @NonNull Handler schedulerHandler) throws CameraUnavailableException {
@@ -206,10 +207,8 @@
             mCameraControlInternal = new Camera2CameraControlImpl(cameraCharacteristicsCompat,
                     executorScheduler, mExecutor, new ControlUpdateListenerInternal(),
                     mCameraQuirks);
-            mCameraInfoInternal = new Camera2CameraInfoImpl(
-                    cameraId,
-                    cameraCharacteristicsCompat,
-                    mCameraControlInternal);
+            mCameraInfoInternal = cameraInfoImpl;
+            mCameraInfoInternal.linkWithCameraControl(mCameraControlInternal);
         } catch (CameraAccessExceptionCompat e) {
             throw CameraUnavailableExceptionHelper.createFrom(e);
         }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index ccea681..43c735b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -18,8 +18,10 @@
 
 import android.hardware.camera2.CameraCharacteristics;
 import android.hardware.camera2.CameraMetadata;
+import android.util.Pair;
 import android.view.Surface;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.experimental.UseExperimental;
@@ -37,12 +39,24 @@
 import androidx.camera.core.impl.utils.CameraOrientationUtil;
 import androidx.core.util.Preconditions;
 import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MediatorLiveData;
+import androidx.lifecycle.Observer;
 
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
 import java.util.concurrent.Executor;
 
 /**
  * Implementation of the {@link CameraInfoInternal} interface that exposes parameters through
  * camera2.
+ *
+ * <p>Construction consists of two stages. The constructor creates a implementation without a
+ * {@link Camera2CameraControlImpl} and will return default values for camera control related
+ * states like zoom/exposure/torch. After {@link #linkWithCameraControl} is called,
+ * zoom/exposure/torch API will reflect the states in the {@link Camera2CameraControlImpl}. Any
+ * CameraCaptureCallbacks added before this link will also be added
+ * to the {@link Camera2CameraControlImpl}.
  */
 @UseExperimental(markerClass = ExperimentalCamera2Interop.class)
 public final class Camera2CameraInfoImpl implements CameraInfoInternal {
@@ -50,22 +64,62 @@
     private static final String TAG = "Camera2CameraInfo";
     private final String mCameraId;
     private final CameraCharacteristicsCompat mCameraCharacteristicsCompat;
-    private final Camera2CameraControlImpl mCamera2CameraControlImpl;
-    private final ZoomControl mZoomControl;
-    private final TorchControl mTorchControl;
-    private final ExposureControl mExposureControl;
     private final Camera2CameraInfo mCamera2CameraInfo;
 
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    @Nullable
+    private Camera2CameraControlImpl mCamera2CameraControlImpl;
+    @GuardedBy("mLock")
+    @Nullable
+    private RedirectableLiveData<Integer> mRedirectTorchStateLiveData = null;
+    @GuardedBy("mLock")
+    @Nullable
+    private RedirectableLiveData<ZoomState> mRedirectZoomStateLiveData = null;
+    @GuardedBy("mLock")
+    @Nullable
+    private List<Pair<CameraCaptureCallback, Executor>> mCameraCaptureCallbacks = null;
+
+    /**
+     * Constructs an instance. Before {@link #linkWithCameraControl(Camera2CameraControlImpl)} is
+     * called, camera control related API (torch/exposure/zoom) will return default values.
+     */
     Camera2CameraInfoImpl(@NonNull String cameraId,
-            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat,
-            @NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+            @NonNull CameraCharacteristicsCompat cameraCharacteristicsCompat) {
         mCameraId = Preconditions.checkNotNull(cameraId);
         mCameraCharacteristicsCompat = cameraCharacteristicsCompat;
-        mCamera2CameraControlImpl = camera2CameraControlImpl;
-        mZoomControl = camera2CameraControlImpl.getZoomControl();
-        mTorchControl = camera2CameraControlImpl.getTorchControl();
-        mExposureControl = camera2CameraControlImpl.getExposureControl();
         mCamera2CameraInfo = new Camera2CameraInfo(this);
+    }
+
+    /**
+     * Links with a {@link Camera2CameraControlImpl}. After the link, zoom/torch/exposure
+     * operations of CameraControl will modify the states in this Camera2CameraInfoImpl.
+     * Also, any CameraCaptureCallbacks added before this link will be added to the
+     * {@link Camera2CameraControlImpl}.
+     */
+    void linkWithCameraControl(@NonNull Camera2CameraControlImpl camera2CameraControlImpl) {
+        synchronized (mLock) {
+            mCamera2CameraControlImpl = camera2CameraControlImpl;
+
+            if (mRedirectZoomStateLiveData != null) {
+                mRedirectZoomStateLiveData.redirectTo(
+                        mCamera2CameraControlImpl.getZoomControl().getZoomState());
+            }
+
+            if (mRedirectTorchStateLiveData != null) {
+                mRedirectTorchStateLiveData.redirectTo(
+                        mCamera2CameraControlImpl.getTorchControl().getTorchState());
+            }
+
+            if (mCameraCaptureCallbacks != null) {
+                for (Pair<CameraCaptureCallback, Executor> pair :
+                        mCameraCaptureCallbacks) {
+                    mCamera2CameraControlImpl.addSessionCameraCaptureCallback(pair.second,
+                            pair.first);
+                }
+                mCameraCaptureCallbacks = null;
+            }
+        }
         logDeviceInfo();
     }
 
@@ -175,20 +229,55 @@
     @NonNull
     @Override
     public LiveData<Integer> getTorchState() {
-        return mTorchControl.getTorchState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectTorchStateLiveData == null) {
+                    mRedirectTorchStateLiveData =
+                            new RedirectableLiveData<>(TorchControl.DEFAULT_TORCH_STATE);
+                }
+                return mRedirectTorchStateLiveData;
+            }
+
+            // if RedirectableLiveData exists,  use it directly.
+            if (mRedirectTorchStateLiveData != null) {
+                return mRedirectTorchStateLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getTorchControl().getTorchState();
+        }
     }
 
     @NonNull
     @Override
     public LiveData<ZoomState> getZoomState() {
-        return mZoomControl.getZoomState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mRedirectZoomStateLiveData == null) {
+                    mRedirectZoomStateLiveData = new RedirectableLiveData<>(
+                            ZoomControl.getDefaultZoomState(mCameraCharacteristicsCompat));
+                }
+                return mRedirectZoomStateLiveData;
+            }
+
+            // if RedirectableLiveData exists,  use it directly.
+            if (mRedirectZoomStateLiveData != null) {
+                return mRedirectZoomStateLiveData;
+            }
+
+            return mCamera2CameraControlImpl.getZoomControl().getZoomState();
+        }
     }
 
     @NonNull
     @Override
     @ExperimentalExposureCompensation
     public ExposureState getExposureState() {
-        return mExposureControl.getExposureState();
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                return ExposureControl.getDefaultExposureState(mCameraCharacteristicsCompat);
+            }
+            return mCamera2CameraControlImpl.getExposureControl().getExposureState();
+        }
     }
 
     /**
@@ -213,12 +302,38 @@
     @Override
     public void addSessionCaptureCallback(@NonNull Executor executor,
             @NonNull CameraCaptureCallback callback) {
-        mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mCameraCaptureCallbacks == null) {
+                    mCameraCaptureCallbacks = new ArrayList<>();
+                }
+                mCameraCaptureCallbacks.add(new Pair<>(callback, executor));
+                return;
+            }
+
+            mCamera2CameraControlImpl.addSessionCameraCaptureCallback(executor, callback);
+        }
     }
 
     @Override
     public void removeSessionCaptureCallback(@NonNull CameraCaptureCallback callback) {
-        mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+        synchronized (mLock) {
+            if (mCamera2CameraControlImpl == null) {
+                if (mCameraCaptureCallbacks == null) {
+                    return;
+                }
+                Iterator<Pair<CameraCaptureCallback, Executor>> it =
+                        mCameraCaptureCallbacks.iterator();
+                while (it.hasNext()) {
+                    Pair<CameraCaptureCallback, Executor> pair = it.next();
+                    if (pair.first == callback) {
+                        it.remove();
+                    }
+                }
+                return;
+            }
+            mCamera2CameraControlImpl.removeSessionCameraCaptureCallback(callback);
+        }
     }
 
     /**
@@ -228,4 +343,40 @@
     public Camera2CameraInfo getCamera2CameraInfo() {
         return mCamera2CameraInfo;
     }
+
+    /**
+     * A {@link LiveData} which can be redirected to another {@link LiveData}. If no redirection
+     * is set, initial value will be used.
+     */
+    static class RedirectableLiveData<T> extends MediatorLiveData<T> {
+        private LiveData<T> mLiveDataSource;
+        private T mInitialValue;
+
+        RedirectableLiveData(T initialValue) {
+            mInitialValue = initialValue;
+        }
+
+        void redirectTo(@NonNull LiveData<T> liveDataSource) {
+            if (mLiveDataSource != null) {
+                super.removeSource(mLiveDataSource);
+            }
+            mLiveDataSource = liveDataSource;
+            super.addSource(liveDataSource, this::setValue);
+        }
+
+        @Override
+        public <S> void addSource(@NonNull LiveData<S> source,
+                @NonNull Observer<? super S> onChanged) {
+            throw new UnsupportedOperationException();
+        }
+
+        // Overrides getValue() to reflect the correct value from source. This is required to ensure
+        // getValue() is correct when observe() or observeForever() is not called.
+        @Override
+        public T getValue() {
+            // Returns initial value if source is not set.
+            return mLiveDataSource == null ? mInitialValue : mLiveDataSource.getValue();
+        }
+    }
+
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
index e5eb1ff..9479bf5 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManager.java
@@ -24,7 +24,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.RestrictTo.Scope;
-import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
 import androidx.camera.camera2.internal.compat.CameraManagerCompat;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.impl.CameraDeviceSurfaceManager;
@@ -36,6 +35,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -60,13 +60,15 @@
      */
     @RestrictTo(Scope.LIBRARY)
     public Camera2DeviceSurfaceManager(@NonNull Context context,
-            @Nullable Object cameraManager) throws CameraUnavailableException {
-        this(context, CamcorderProfile::hasProfile, cameraManager);
+            @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
+            throws CameraUnavailableException {
+        this(context, CamcorderProfile::hasProfile, cameraManager, availableCameraIds);
     }
 
     Camera2DeviceSurfaceManager(@NonNull Context context,
             @NonNull CamcorderProfileHelper camcorderProfileHelper,
-            @Nullable Object cameraManager)
+            @Nullable Object cameraManager,
+            @NonNull Set<String> availableCameraIds)
             throws CameraUnavailableException {
         Preconditions.checkNotNull(camcorderProfileHelper);
         mCamcorderProfileHelper = camcorderProfileHelper;
@@ -77,25 +79,22 @@
         } else {
             cameraManagerCompat = CameraManagerCompat.from(context);
         }
-        init(context, cameraManagerCompat);
+        init(context, cameraManagerCompat, availableCameraIds);
     }
 
     /**
      * Prepare necessary resources for the surface manager.
      */
-    private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager)
+    private void init(@NonNull Context context, @NonNull CameraManagerCompat cameraManager,
+            @NonNull Set<String> availableCameraIds)
             throws CameraUnavailableException {
         Preconditions.checkNotNull(context);
 
-        try {
-            for (String cameraId : cameraManager.getCameraIdList()) {
-                mCameraSupportedSurfaceCombinationMap.put(
-                        cameraId,
-                        new SupportedSurfaceCombination(
-                                context, cameraId, cameraManager, mCamcorderProfileHelper));
-            }
-        } catch (CameraAccessExceptionCompat e) {
-            throw CameraUnavailableExceptionHelper.createFrom(e);
+        for (String cameraId : availableCameraIds) {
+            mCameraSupportedSurfaceCombinationMap.put(
+                    cameraId,
+                    new SupportedSurfaceCombination(
+                            context, cameraId, cameraManager, mCamcorderProfileHelper));
         }
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
index 2d6d755..703e6a1 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraBurstCaptureCallback.java
@@ -42,6 +42,7 @@
 class CameraBurstCaptureCallback extends CameraCaptureSession.CaptureCallback {
 
     final Map<CaptureRequest, List<CameraCaptureSession.CaptureCallback>> mCallbackMap;
+    CaptureSequenceCallback mCaptureSequenceCallback = null;
 
     CameraBurstCaptureCallback() {
         mCallbackMap = new HashMap<>();
@@ -98,13 +99,18 @@
     @Override
     public void onCaptureSequenceAborted(
             @NonNull CameraCaptureSession session, int sequenceId) {
-        // No-op.
+        if (mCaptureSequenceCallback != null) {
+            mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId, true);
+        }
     }
 
     @Override
     public void onCaptureSequenceCompleted(
             @NonNull CameraCaptureSession session, int sequenceId, long frameNumber) {
-        // No-op.
+        if (mCaptureSequenceCallback != null) {
+            mCaptureSequenceCallback.onCaptureSequenceCompletedOrAborted(session, sequenceId,
+                    false);
+        }
     }
 
     private List<CameraCaptureSession.CaptureCallback> getCallbacks(CaptureRequest request) {
@@ -131,4 +137,20 @@
         }
     }
 
+    /**
+     * Sets the callback to receive the notification when the capture sequence is completed or
+     * aborted.
+     */
+    public void setCaptureSequenceCallback(@NonNull CaptureSequenceCallback callback) {
+        mCaptureSequenceCallback = callback;
+    }
+
+    /**
+     * A interface to receive the notification of onCaptureSequenceCompleted or
+     * onCaptureSequenceAborted.
+     */
+    interface CaptureSequenceCallback {
+        void onCaptureSequenceCompletedOrAborted(
+                @NonNull CameraCaptureSession session, int sequenceId, boolean isAborted);
+    }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
new file mode 100644
index 0000000..d0a146f
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CameraSelectionOptimizer.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraMetadata;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.camera2.internal.compat.CameraAccessExceptionCompat;
+import androidx.camera.camera2.internal.compat.CameraManagerCompat;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.CameraUnavailableException;
+import androidx.camera.core.InitializationException;
+import androidx.camera.core.impl.CameraInfoInternal;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class CameraSelectionOptimizer {
+    private CameraSelectionOptimizer() {
+    }
+
+    static List<String> getSelectedAvailableCameraIds(
+            @NonNull Camera2CameraFactory cameraFactory,
+            @Nullable CameraSelector availableCamerasSelector)
+            throws InitializationException {
+        try {
+            List<String> availableCameraIds = new ArrayList<>();
+            String[] cameraIdList = cameraFactory.getCameraManager().getCameraIdList();
+            if (availableCamerasSelector == null) {
+                for (String id : cameraIdList) {
+                    availableCameraIds.add(id);
+                }
+                return availableCameraIds;
+            }
+
+            // Skip camera ID by heuristic: 0 is back lens facing, 1 is front lens facing.
+            Integer lensFacingInteger = availableCamerasSelector.getLensFacing();
+            String skippedCameraId = decideSkippedCameraIdByHeuristic(
+                    cameraFactory.getCameraManager(), lensFacingInteger);
+            List<CameraInfo> cameraInfos = new ArrayList<>();
+
+            for (String id : cameraIdList) {
+                if (id.equals(skippedCameraId)) {
+                    continue;
+                }
+                Camera2CameraInfoImpl cameraInfo = cameraFactory.getCameraInfo(id);
+                cameraInfos.add(cameraInfo);
+            }
+
+            List<CameraInfo> filteredCameraInfos =
+                    availableCamerasSelector.filter(cameraInfos);
+
+            for (CameraInfo cameraInfo : filteredCameraInfos) {
+                String cameraId = ((CameraInfoInternal) cameraInfo).getCameraId();
+                availableCameraIds.add(cameraId);
+            }
+
+            return availableCameraIds;
+        } catch (CameraAccessExceptionCompat e) {
+            throw new InitializationException(CameraUnavailableExceptionHelper.createFrom(e));
+        } catch (CameraUnavailableException e) {
+            throw new InitializationException(e);
+        }
+    }
+
+    // Returns the camera id that can be safely skipped.
+    // Returns null if no camera ids can be skipped.
+    private static String decideSkippedCameraIdByHeuristic(CameraManagerCompat cameraManager,
+            Integer lensFacingInteger) throws CameraAccessExceptionCompat {
+        String skippedCameraId = null;
+        if (lensFacingInteger == null) { // Not specifying lens facing,  cannot skip any camera id.
+            return null;
+        } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_BACK) {
+            if (cameraManager.getCameraCharacteristicsCompat("0").get(
+                    CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_BACK) {
+                // If apps requires back lens facing,  and "0" is confirmed to be back
+                // We can safely ignore "1" as a optimization for initialization latency
+                skippedCameraId = "1";
+            }
+        } else if (lensFacingInteger.intValue() == CameraSelector.LENS_FACING_FRONT) {
+            if (cameraManager.getCameraCharacteristicsCompat("1").get(
+                    CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT) {
+                // If apps requires front lens facing,  and "1" is confirmed to be back
+                // We can safely ignore "0" as a optimization for initialization latency
+                skippedCameraId = "0";
+            }
+        }
+
+        return skippedCameraId;
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
index 7418b99..f61d60e 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/CaptureSession.java
@@ -32,6 +32,7 @@
 import androidx.camera.camera2.impl.CameraEventCallbacks;
 import androidx.camera.camera2.internal.compat.params.OutputConfigurationCompat;
 import androidx.camera.camera2.internal.compat.params.SessionConfigurationCompat;
+import androidx.camera.camera2.internal.compat.workaround.StillCaptureFlow;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.CameraCaptureCallback;
@@ -120,6 +121,7 @@
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     @GuardedBy("mStateLock")
     CallbackToFutureAdapter.Completer<Void> mReleaseCompleter;
+    final StillCaptureFlow mStillCaptureFlow = new StillCaptureFlow();
 
     /**
      * Constructor for CaptureSession.
@@ -633,6 +635,7 @@
         try {
             CameraBurstCaptureCallback callbackAggregator = new CameraBurstCaptureCallback();
             List<CaptureRequest> captureRequests = new ArrayList<>();
+            boolean isStillCapture = false;
             Logger.d(TAG, "Issuing capture request.");
             for (CaptureConfig captureConfig : captureConfigs) {
                 if (captureConfig.getSurfaces().isEmpty()) {
@@ -657,6 +660,9 @@
                     continue;
                 }
 
+                if (captureConfig.getTemplateType() == CameraDevice.TEMPLATE_STILL_CAPTURE) {
+                    isStillCapture = true;
+                }
                 CaptureConfig.Builder captureConfigBuilder = CaptureConfig.Builder.from(
                         captureConfig);
 
@@ -692,6 +698,18 @@
             }
 
             if (!captureRequests.isEmpty()) {
+                if (mStillCaptureFlow
+                        .shouldStopRepeatingBeforeCapture(captureRequests, isStillCapture)) {
+                    mSynchronizedCaptureSession.stopRepeating();
+                    callbackAggregator.setCaptureSequenceCallback(
+                            (session, sequenceId, isAborted) -> {
+                                synchronized (mStateLock) {
+                                    if (mState == State.OPENED) {
+                                        issueRepeatingCaptureRequests();
+                                    }
+                                }
+                            });
+                }
                 mSynchronizedCaptureSession.captureBurstRequests(captureRequests,
                         callbackAggregator);
             } else {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
index 9b8e776..12e6f7f 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureControl.java
@@ -93,6 +93,11 @@
         mExecutor = executor;
     }
 
+    static ExposureState getDefaultExposureState(
+            CameraCharacteristicsCompat cameraCharacteristics) {
+        return new ExposureStateImpl(cameraCharacteristics, DEFAULT_EXPOSURE_COMPENSATION);
+    }
+
     /**
      * Set current active state. Set active if it is ready to accept operations.
      *
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
index 13fdc47..bf44fdf 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ExposureStateImpl.java
@@ -75,6 +75,8 @@
     public boolean isExposureCompensationSupported() {
         Range<Integer> compensationRange =
                 mCameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE);
-        return compensationRange.getLower() != 0 && compensationRange.getUpper() != 0;
+        return compensationRange != null
+                && compensationRange.getLower() != 0
+                && compensationRange.getUpper() != 0;
     }
 }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
index 7c86acb..695b9bc 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/TorchControl.java
@@ -48,6 +48,7 @@
  */
 final class TorchControl {
     private static final String TAG = "TorchControl";
+    static final int DEFAULT_TORCH_STATE = TorchState.OFF;
 
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     private final Camera2CameraControlImpl mCamera2CameraControlImpl;
@@ -77,7 +78,7 @@
         Boolean hasFlashUnit =
                 cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
         mHasFlashUnit = hasFlashUnit != null && hasFlashUnit.booleanValue();
-        mTorchState = new MutableLiveData<>(TorchState.OFF);
+        mTorchState = new MutableLiveData<>(DEFAULT_TORCH_STATE);
         mCamera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
     }
 
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
index 7d79583..888e0b2 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZoomControl.java
@@ -98,7 +98,15 @@
         camera2CameraControlImpl.addCaptureResultListener(mCaptureResultListener);
     }
 
-    private ZoomImpl createZoomImpl(@NonNull CameraCharacteristicsCompat cameraCharacteristics) {
+    static ZoomState getDefaultZoomState(CameraCharacteristicsCompat cameraCharacteristics) {
+        ZoomImpl zoomImpl = createZoomImpl(cameraCharacteristics);
+        ZoomStateImpl zoomState = new ZoomStateImpl(zoomImpl.getMaxZoom(), zoomImpl.getMinZoom());
+        zoomState.setZoomRatio(DEFAULT_ZOOM_RATIO);
+        return ImmutableZoomState.create(zoomState);
+    }
+
+    private static ZoomImpl createZoomImpl(
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics) {
         if (isAndroidRZoomSupported(cameraCharacteristics)) {
             return new AndroidRZoomImpl(cameraCharacteristics);
         } else {
@@ -106,7 +114,8 @@
         }
     }
 
-    private boolean isAndroidRZoomSupported(CameraCharacteristicsCompat cameraCharacteristics) {
+    private static boolean isAndroidRZoomSupported(
+            CameraCharacteristicsCompat cameraCharacteristics) {
         return Build.VERSION.SDK_INT >= 30 && cameraCharacteristics.get(
                 CameraCharacteristics.CONTROL_ZOOM_RATIO_RANGE) != null;
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index ba59291..374c616 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -57,6 +57,9 @@
         if (PreviewPixelHDRnetQuirk.load()) {
             quirks.add(new PreviewPixelHDRnetQuirk());
         }
+        if (StillCaptureFlashStopRepeatingQuirk.load()) {
+            quirks.add(new StillCaptureFlashStopRepeatingQuirk());
+        }
 
         return quirks;
     }
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
new file mode 100644
index 0000000..bc50f27
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/StillCaptureFlashStopRepeatingQuirk.java
@@ -0,0 +1,39 @@
+/*
+ * 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.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Locale;
+
+/**
+ * Quirk that still capture with flash on/auto requires stopRepeating() being called ahead of
+ * capture.
+ *
+ * <p>On some devices like Samsung SM-A716B, it could lead to CaptureRequest not being completed
+ * when taking photos in dark environment with flash on/auto. Calling stopRepeating ahead of
+ * still capture and setRepeating again after capture is done can fix the issue. See b/172036589.
+ */
+public class StillCaptureFlashStopRepeatingQuirk implements Quirk {
+    static boolean load() {
+        return "SAMSUNG".equals(Build.MANUFACTURER.toUpperCase(Locale.US))
+                // Enables it on all A716 models.
+                && android.os.Build.MODEL.toUpperCase(Locale.US).startsWith("SM-A716");
+    }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
new file mode 100644
index 0000000..7a22956
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlow.java
@@ -0,0 +1,63 @@
+/*
+ * 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.internal.compat.workaround;
+
+import android.hardware.camera2.CaptureRequest;
+
+import androidx.annotation.NonNull;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.StillCaptureFlashStopRepeatingQuirk;
+
+import java.util.List;
+
+/**
+ * Workaround to fix device issues such as calling stopRepeating ahead of still
+ * capture on some devices when flash is on or auto. See b/172036589.
+ */
+public class StillCaptureFlow {
+    private final boolean mShouldStopRepeatingBeforeStillCapture;
+    public StillCaptureFlow() {
+        final StillCaptureFlashStopRepeatingQuirk quirk = DeviceQuirks.get(
+                StillCaptureFlashStopRepeatingQuirk.class);
+
+        mShouldStopRepeatingBeforeStillCapture = (quirk != null);
+    }
+
+    /**
+     * Returns whether or not it should call stopRepeating ahead of capture request.
+     *
+     * @param captureRequests captureRequests to be executed
+     * @param isStillCapture true if captureRequests contain a still capture request.
+     * @return
+     */
+    public boolean shouldStopRepeatingBeforeCapture(
+            @NonNull List<CaptureRequest> captureRequests, boolean isStillCapture) {
+        if (!mShouldStopRepeatingBeforeStillCapture || !isStillCapture) {
+            return false;
+        }
+
+        for (CaptureRequest request : captureRequests) {
+            int aeMode = request.get(CaptureRequest.CONTROL_AE_MODE);
+            if (aeMode == CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
+                    || aeMode == CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index ffa744e..f528b07c 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -32,18 +33,19 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.core.CameraInfo;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ExposureState;
 import androidx.camera.core.TorchState;
 import androidx.camera.core.ZoomState;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.internal.ImmutableZoomState;
+import androidx.lifecycle.LiveData;
 import androidx.lifecycle.MutableLiveData;
 import androidx.test.core.app.ApplicationProvider;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
@@ -78,6 +80,7 @@
     private CameraCharacteristicsCompat mCameraCharacteristics1;
     private ZoomControl mMockZoomControl;
     private TorchControl mMockTorchControl;
+    private ExposureControl mExposureControl;
     private Camera2CameraControlImpl mMockCameraControl;
 
     @Before
@@ -97,23 +100,26 @@
 
         mMockZoomControl = mock(ZoomControl.class);
         mMockTorchControl = mock(TorchControl.class);
+        mExposureControl = mock(ExposureControl.class);
         mMockCameraControl = mock(Camera2CameraControlImpl.class);
 
         when(mMockCameraControl.getZoomControl()).thenReturn(mMockZoomControl);
         when(mMockCameraControl.getTorchControl()).thenReturn(mMockTorchControl);
+        when(mMockCameraControl.getExposureControl()).thenReturn(mExposureControl);
     }
 
     @Test
     public void canCreateCameraInfo() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
         assertThat(cameraInfoInternal).isNotNull();
     }
 
     @Test
     public void cameraInfo_canReturnSensorOrientation() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.getSensorRotationDegrees()).isEqualTo(
                 CAMERA0_SENSOR_ORIENTATION);
     }
@@ -121,7 +127,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forBackCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
 
         // Note: these numbers depend on the camera being a back-facing camera.
         assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -137,7 +143,7 @@
     @Test
     public void cameraInfo_canCalculateCorrectRelativeRotation_forFrontCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
 
         // Note: these numbers depend on the camera being a front-facing camera.
         assertThat(cameraInfoInternal.getSensorRotationDegrees(Surface.ROTATION_0))
@@ -153,47 +159,130 @@
     @Test
     public void cameraInfo_canReturnLensFacing() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.getLensFacing()).isEqualTo(CAMERA0_LENS_FACING_ENUM);
     }
 
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forBackCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
         assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA0_FLASH_INFO_BOOLEAN);
     }
 
     @Test
     public void cameraInfo_canReturnHasFlashUnit_forFrontCamera() {
         CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1, mMockCameraControl);
+                new Camera2CameraInfoImpl(CAMERA1_ID, mCameraCharacteristics1);
         assertThat(cameraInfoInternal.hasFlashUnit()).isEqualTo(CAMERA1_FLASH_INFO_BOOLEAN);
     }
 
     @Test
-    public void cameraInfo_canReturnTorchState() {
-        CameraInfoInternal cameraInfoInternal =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
-        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.OFF));
-        assertThat(cameraInfoInternal.getTorchState().getValue()).isEqualTo(TorchState.OFF);
+    public void cameraInfoWithoutCameraControl_canReturnDefaultTorchState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue())
+                .isEqualTo(TorchControl.DEFAULT_TORCH_STATE);
+    }
+
+    @Test
+    public void cameraInfoWithCameraControl_canReturnTorchState() {
+        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
+    }
+
+    @Test
+    public void torchStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        // Calls getTorchState() to trigger RedirectableLiveData
+        LiveData<Integer> torchStateLiveData = camera2CameraInfoImpl.getTorchState();
+
+        when(mMockTorchControl.getTorchState()).thenReturn(new MutableLiveData<>(TorchState.ON));
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+        assertThat(camera2CameraInfoImpl.getTorchState()).isSameInstanceAs(torchStateLiveData);
+        assertThat(camera2CameraInfoImpl.getTorchState().getValue()).isEqualTo(TorchState.ON);
     }
 
     // zoom related tests just ensure it uses ZoomControl to get the value
     // Full tests are performed at ZoomControlDeviceTest / ZoomControlTest.
     @Test
-    public void cameraInfo_getZoom_valueIsCorrect() {
-        CameraInfoInternal cameraInfo =
-                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0, mMockCameraControl);
+    public void cameraInfoWithCameraControl_getZoom_valueIsCorrect() {
         ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
         when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
-        assertThat(mMockZoomControl.getZoomState().getValue()).isEqualTo(zoomState);
+
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+    }
+
+    @Test
+    public void cameraInfoWithoutCameraControl_getDetaultZoomState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue())
+                .isEqualTo(ZoomControl.getDefaultZoomState(mCameraCharacteristics0));
+    }
+
+    @Test
+    public void zoomStateLiveData_SameInstanceBeforeAndAfterCameraControlLink() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        // Calls getZoomState() to trigger RedirectableLiveData
+        LiveData<ZoomState> zoomStateLiveData = camera2CameraInfoImpl.getZoomState();
+
+        ZoomState zoomState = ImmutableZoomState.create(3.0f, 8.0f, 1.0f, 0.2f);
+        when(mMockZoomControl.getZoomState()).thenReturn(new MutableLiveData<>(zoomState));
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        // TorchState LiveData instances are the same before and after the linkWithCameraControl.
+        assertThat(camera2CameraInfoImpl.getZoomState()).isSameInstanceAs(zoomStateLiveData);
+        assertThat(camera2CameraInfoImpl.getZoomState().getValue()).isEqualTo(zoomState);
+    }
+
+    @Test
+    public void cameraInfoWithCameraControl_canReturnExposureState() {
+        ExposureState exposureState = new ExposureStateImpl(mCameraCharacteristics0, 2);
+        when(mExposureControl.getExposureState()).thenReturn(exposureState);
+
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+        camera2CameraInfoImpl.linkWithCameraControl(mMockCameraControl);
+
+        assertThat(camera2CameraInfoImpl.getExposureState()).isEqualTo(exposureState);
+    }
+
+    @Test
+    public void cameraInfoWithoutCameraControl_canReturnDefaultExposureState() {
+        Camera2CameraInfoImpl camera2CameraInfoImpl =
+                new Camera2CameraInfoImpl(CAMERA0_ID, mCameraCharacteristics0);
+
+        ExposureState defaultState =
+                ExposureControl.getDefaultExposureState(mCameraCharacteristics0);
+
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationIndex())
+                .isEqualTo(defaultState.getExposureCompensationIndex());
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationRange())
+                .isEqualTo(defaultState.getExposureCompensationRange());
+        assertThat(camera2CameraInfoImpl.getExposureState().getExposureCompensationStep())
+                .isEqualTo(defaultState.getExposureCompensationStep());
+        assertThat(camera2CameraInfoImpl.getExposureState().isExposureCompensationSupported())
+                .isEqualTo(defaultState.isExposureCompensationSupported());
     }
 
     @Test
     public void cameraInfo_getImplementationType_legacy() {
         final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA0_ID,
-                mCameraCharacteristics0, mMockCameraControl);
+                mCameraCharacteristics0);
         assertThat(cameraInfo.getImplementationType()).isEqualTo(
                 CameraInfo.IMPLEMENTATION_TYPE_CAMERA2_LEGACY);
     }
@@ -201,43 +290,68 @@
     @Test
     public void cameraInfo_getImplementationType_noneLegacy() {
         final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+                mCameraCharacteristics1);
         assertThat(cameraInfo.getImplementationType()).isEqualTo(
                 CameraInfo.IMPLEMENTATION_TYPE_CAMERA2);
     }
 
     @Test
     public void addSessionCameraCaptureCallback_isCalledToCameraControl() {
-        final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
 
         Executor executor = mock(Executor.class);
         CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
         cameraInfo.addSessionCaptureCallback(executor, callback);
 
-        ArgumentCaptor<Executor> executorCaptor = ArgumentCaptor.forClass(Executor.class);
-        ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
-                ArgumentCaptor.forClass(CameraCaptureCallback.class);
-
-        verify(mMockCameraControl).addSessionCameraCaptureCallback(executorCaptor.capture(),
-                callbackCaptor.capture());
-        assertThat(executorCaptor.getValue()).isSameInstanceAs(executor);
-        assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
     }
 
     @Test
     public void removeSessionCameraCaptureCallback_isCalledToCameraControl() {
-        final CameraInfoInternal cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
-                mCameraCharacteristics1, mMockCameraControl);
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
 
         CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
         cameraInfo.removeSessionCaptureCallback(callback);
 
-        ArgumentCaptor<CameraCaptureCallback> callbackCaptor =
-                ArgumentCaptor.forClass(CameraCaptureCallback.class);
+        verify(mMockCameraControl).removeSessionCameraCaptureCallback(callback);
+    }
 
-        verify(mMockCameraControl).removeSessionCameraCaptureCallback(callbackCaptor.capture());
-        assertThat(callbackCaptor.getValue()).isSameInstanceAs(callback);
+    @Test
+    public void addSessionCameraCaptureCallbackWithoutCameraControl_attachedToCameraControlLater() {
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        Executor executor = mock(Executor.class);
+        CameraCaptureCallback callback = mock(CameraCaptureCallback.class);
+        cameraInfo.addSessionCaptureCallback(executor, callback);
+
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
+
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor, callback);
+    }
+
+    @Test
+    public void removeSessionCameraCaptureCallbackWithoutCameraControl_callbackIsRemoved() {
+        final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(CAMERA1_ID,
+                mCameraCharacteristics1);
+        // Add two callbacks
+        Executor executor1 = mock(Executor.class);
+        CameraCaptureCallback callback1 = mock(CameraCaptureCallback.class);
+        Executor executor2 = mock(Executor.class);
+        CameraCaptureCallback callback2 = mock(CameraCaptureCallback.class);
+        cameraInfo.addSessionCaptureCallback(executor1, callback1);
+        cameraInfo.addSessionCaptureCallback(executor2, callback2);
+
+        // Remove first callback.
+        cameraInfo.removeSessionCaptureCallback(callback1);
+
+        // Only second callback will be added to camera control.
+        cameraInfo.linkWithCameraControl(mMockCameraControl);
+        verify(mMockCameraControl, never()).addSessionCameraCaptureCallback(executor1, callback1);
+        verify(mMockCameraControl).addSessionCameraCaptureCallback(executor2, callback2);
     }
 
     private void initCameras() {
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
index b248170..0ec8db2 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2DeviceSurfaceManagerTest.java
@@ -570,9 +570,7 @@
         @CameraSelector.LensFacing int lensFacingEnum = CameraUtil.getLensFacingEnumFromInt(
                 lensFacing);
         mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
-                new Camera2CameraInfoImpl(cameraId,
-                        getCameraCharacteristicsCompat(cameraId),
-                        mock(Camera2CameraControlImpl.class))));
+                new Camera2CameraInfoImpl(cameraId, getCameraCharacteristicsCompat(cameraId))));
     }
 
     private void initCameraX() {
@@ -592,11 +590,11 @@
 
         // Create the DeviceSurfaceManager for Camera2
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> {
+                (context, cameraManager, availableCameraIds) -> {
                     try {
                         return new Camera2DeviceSurfaceManager(mContext,
                                 mMockCamcorderProfileHelper,
-                                (CameraManagerCompat) cameraManager);
+                                (CameraManagerCompat) cameraManager, availableCameraIds);
                     } catch (CameraUnavailableException e) {
                         throw new InitializationException(e);
                     }
@@ -608,7 +606,7 @@
 
         CameraXConfig.Builder appConfigBuilder =
                 new CameraXConfig.Builder()
-                        .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+                        .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
                         .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
                         .setUseCaseConfigFactoryProvider(factoryProvider);
 
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
index 1d588c5..a8ddb0e 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraBurstCaptureCallbackTest.java
@@ -117,4 +117,28 @@
         burstCaptureCallback.onCaptureStarted(mSession, mRequest0, 0, 0);
         // No listener called.
     }
+
+    @Test
+    public void captureSequenceCallback_calledWhenSequenceCompleted() {
+        CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+        CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+                CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+        burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+        burstCaptureCallback.onCaptureSequenceCompleted(mSession, 0, 0);
+
+        verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, false);
+    }
+
+    @Test
+    public void captureSequenceCallback_calledWhenSequenceAborted() {
+        CameraBurstCaptureCallback burstCaptureCallback = new CameraBurstCaptureCallback();
+        CameraBurstCaptureCallback.CaptureSequenceCallback sequenceCallback = mock(
+                CameraBurstCaptureCallback.CaptureSequenceCallback.class);
+        burstCaptureCallback.setCaptureSequenceCallback(sequenceCallback);
+
+        burstCaptureCallback.onCaptureSequenceAborted(mSession, 0);
+
+        verify(sequenceCallback).onCaptureSequenceCompletedOrAborted(mSession, 0, true);
+    }
 }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
new file mode 100644
index 0000000..de4b238
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/CameraSelectionOptimizerTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.internal;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.camera2.CameraCharacteristics;
+import android.os.Build;
+import android.os.Handler;
+
+import androidx.camera.camera2.interop.Camera2CameraFilter;
+import androidx.camera.camera2.interop.Camera2CameraInfo;
+import androidx.camera.core.CameraFilter;
+import androidx.camera.core.CameraSelector;
+import androidx.camera.core.impl.CameraThreadConfig;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.shadow.api.Shadow;
+import org.robolectric.shadows.ShadowCameraCharacteristics;
+import org.robolectric.shadows.ShadowCameraManager;
+
+import java.util.Arrays;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class CameraSelectionOptimizerTest {
+    private Camera2CameraFactory mCamera2CameraFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        mCamera2CameraFactory =
+                spy(new Camera2CameraFactory(ApplicationProvider.getApplicationContext(),
+                        CameraThreadConfig.create(CameraXExecutors.mainThreadExecutor(),
+                                new Handler()),
+                        null));
+    }
+
+    void setupNormalCameras() throws Exception {
+        initCharacterisic("0", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+        initCharacterisic("1", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+        initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+        initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+    }
+
+    void setupAbnormalCameras() throws Exception {
+        // "0" is front
+        initCharacterisic("0", CameraCharacteristics.LENS_FACING_FRONT, 3.52f);
+        // "1" is back
+        initCharacterisic("1", CameraCharacteristics.LENS_FACING_BACK, 3.52f);
+        initCharacterisic("2", CameraCharacteristics.LENS_FACING_BACK, 2.7f);
+        initCharacterisic("3", CameraCharacteristics.LENS_FACING_BACK, 10.0f);
+    }
+
+    @Test
+    public void availableCamerasSelectorNull_returnAllCameras() throws Exception {
+        setupNormalCameras();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        null);
+
+        assertThat(cameraIds).containsExactly("0", "1", "2", "3");
+    }
+
+    @Test
+    public void requireLensFacingBack() throws Exception {
+        setupNormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("0", "2", "3");
+        verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+    }
+
+    @Test
+    public void requireLensFacingFront() throws Exception {
+        setupNormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("1");
+        // only camera "0" 's getCameraCharacteristics can be avoided.
+        verify(mCamera2CameraFactory, never()).getCameraInfo("0");
+    }
+
+    @Test
+    public void requireLensFacingBack_andSelectWidestAngle() throws Exception {
+        setupNormalCameras();
+
+        CameraFilter widestAngleFilter = Camera2CameraFilter.createCameraFilter(
+                cameraInfoList -> {
+                    float minFocalLength = 10000;
+                    Camera2CameraInfo minFocalCameraInfo = null;
+                    for (Camera2CameraInfo camera2CameraInfo : cameraInfoList) {
+                        float focalLength = camera2CameraInfo.getCameraCharacteristic(
+                                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)[0];
+                        if (focalLength < minFocalLength) {
+                            minFocalLength = focalLength;
+                            minFocalCameraInfo = camera2CameraInfo;
+                        }
+                    }
+                    return Arrays.asList(minFocalCameraInfo);
+                });
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .addCameraFilter(widestAngleFilter)
+                        .build();
+
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        assertThat(cameraIds).containsExactly("2");
+        // only camera "1" 's getCameraCharacteristics can be avoided.
+        verify(mCamera2CameraFactory, never()).getCameraInfo("1");
+
+    }
+
+    @Test
+    public void abnormalCameraSetup_requireLensFacingBack() throws Exception {
+        setupAbnormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_BACK)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        // even though heuristic failed, it still works as expected.
+        assertThat(cameraIds).containsExactly("1", "2", "3");
+    }
+
+    @Test
+    public void abnormalCameraSetup_requireLensFacingFront() throws Exception {
+        setupAbnormalCameras();
+
+        CameraSelector cameraSelector =
+                new CameraSelector.Builder()
+                        .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
+                        .build();
+        List<String> cameraIds =
+                CameraSelectionOptimizer.getSelectedAvailableCameraIds(mCamera2CameraFactory,
+                        cameraSelector);
+
+        // even though heuristic failed, it still works as expected.
+        assertThat(cameraIds).containsExactly("0");
+    }
+
+    private void initCharacterisic(String cameraId, int lensFacing, float focalLength) {
+        CameraCharacteristics characteristics =
+                ShadowCameraCharacteristics.newCameraCharacteristics();
+
+        ShadowCameraCharacteristics shadowCharacteristics = Shadow.extract(characteristics);
+
+        shadowCharacteristics.set(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL,
+                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL);
+
+        // Add a lens facing to the camera
+        shadowCharacteristics.set(CameraCharacteristics.LENS_FACING, lensFacing);
+
+        shadowCharacteristics.set(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS,
+                new float[]{focalLength});
+
+        // Add the camera to the camera service
+        ((ShadowCameraManager)
+                Shadow.extract(
+                        ApplicationProvider.getApplicationContext()
+                                .getSystemService(Context.CAMERA_SERVICE)))
+                .addCamera(cameraId, characteristics);
+    }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
index 129f334..e530f60 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSizeConstraintsTest.java
@@ -195,8 +195,7 @@
         cameraFactory.insertCamera(lensFacingEnum, BACK_CAMERA_ID,
                 () -> new FakeCamera(BACK_CAMERA_ID, null,
                         new Camera2CameraInfoImpl(BACK_CAMERA_ID,
-                                getCameraCharacteristicsCompat(BACK_CAMERA_ID),
-                                mock(Camera2CameraControlImpl.class))));
+                                getCameraCharacteristicsCompat(BACK_CAMERA_ID))));
 
         initCameraX(cameraFactory);
     }
@@ -204,7 +203,7 @@
     private void initCameraX(final FakeCameraFactory cameraFactory) {
         CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
                 Camera2Config.defaultConfig())
-                .setCameraFactoryProvider((ignored0, ignored1) -> cameraFactory)
+                .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> cameraFactory)
                 .build();
         CameraX.initialize(mContext, cameraXConfig);
     }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
index c4bf842..368e5fe 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/SupportedSurfaceCombinationTest.java
@@ -2172,8 +2172,7 @@
 
         mCameraFactory.insertCamera(lensFacingEnum, cameraId, () -> new FakeCamera(cameraId, null,
                 new Camera2CameraInfoImpl(cameraId,
-                        mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId),
-                        mock(Camera2CameraControlImpl.class))));
+                        mCameraManagerCompat.getCameraCharacteristicsCompat(cameraId))));
 
         initCameraX();
     }
@@ -2181,7 +2180,7 @@
     private void initCameraX() {
         CameraXConfig cameraXConfig = CameraXConfig.Builder.fromConfig(
                 Camera2Config.defaultConfig())
-                .setCameraFactoryProvider((ignored0, ignored1) -> mCameraFactory)
+                .setCameraFactoryProvider((ignored0, ignored1, ignored2) -> mCameraFactory)
                 .build();
         CameraX.initialize(mContext, cameraXConfig);
         CameraX cameraX;
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
new file mode 100644
index 0000000..6f52773
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/workaround/StillCaptureFlowTest.java
@@ -0,0 +1,99 @@
+/*
+ * 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.internal.compat.workaround;
+
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_ALWAYS_FLASH;
+import static android.hardware.camera2.CameraMetadata.CONTROL_AE_MODE_ON_AUTO_FLASH;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.hardware.camera2.CaptureRequest;
+import android.os.Build;
+
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.ParameterizedRobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.internal.DoNotInstrument;
+import org.robolectric.util.ReflectionHelpers;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+@RunWith(ParameterizedRobolectricTestRunner.class)
+@DoNotInstrument
+@Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
+public class StillCaptureFlowTest {
+    @ParameterizedRobolectricTestRunner.Parameters
+    public static Collection<Object[]> data() {
+        final List<Object[]> data = new ArrayList<>();
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON, true, false});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716B", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON, true, false});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_ALWAYS_FLASH, true, true});
+        data.add(new Object[]{"Samsung", "SM-A716U", CONTROL_AE_MODE_ON_AUTO_FLASH, false, false});
+
+        data.add(new Object[]{"Google", "Pixel 2", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+        data.add(new Object[]{"Moto", "G3", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+        data.add(new Object[]{"Samsung", "SM-A722", CONTROL_AE_MODE_ON_AUTO_FLASH, true, false});
+
+        return data;
+    }
+
+    private final String mBrand;
+    private final String mModel;
+    private final int mAeMode;
+    private final boolean mIsStillCapture;
+    private final boolean mExpectedShouldStopRepeating;
+    public StillCaptureFlowTest(
+            String brand,
+            String model,
+            int aeMode,
+            boolean isStillCapture,
+            boolean expectedShouldStopRepeating) {
+        mBrand = brand;
+        mModel = model;
+        mAeMode = aeMode;
+        mIsStillCapture = isStillCapture;
+        mExpectedShouldStopRepeating = expectedShouldStopRepeating;
+    }
+
+    @Test
+    public void shouldStopRepeating() {
+        ReflectionHelpers.setStaticField(Build.class, "MANUFACTURER", mBrand);
+        ReflectionHelpers.setStaticField(Build.class, "MODEL", mModel);
+
+        StillCaptureFlow stillCaptureFlow = new StillCaptureFlow();
+        CaptureRequest captureRequest = mock(CaptureRequest.class);
+        when(captureRequest.get(CaptureRequest.CONTROL_AE_MODE)).thenReturn(mAeMode);
+
+        assertThat(stillCaptureFlow.shouldStopRepeatingBeforeCapture(
+                Arrays.asList(captureRequest), mIsStillCapture))
+                .isEqualTo(mExpectedShouldStopRepeating);
+    }
+}
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
index 779d45f..8157db1 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/CameraXTest.java
@@ -165,9 +165,11 @@
     @Test
     public void init_withDifferentCameraXConfig() throws ExecutionException, InterruptedException {
         CameraFactory cameraFactory0 = new FakeCameraFactory();
-        CameraFactory.Provider cameraFactoryProvider0 = (ignored0, ignored1) -> cameraFactory0;
+        CameraFactory.Provider cameraFactoryProvider0 =
+                (ignored0, ignored1, ignored2) -> cameraFactory0;
         CameraFactory cameraFactory1 = new FakeCameraFactory();
-        CameraFactory.Provider cameraFactoryProvider1 = (ignored0, ignored1) -> cameraFactory1;
+        CameraFactory.Provider cameraFactoryProvider1 =
+                (ignored0, ignored1, ignored2) -> cameraFactory1;
 
         mConfigBuilder.setCameraFactoryProvider(cameraFactoryProvider0);
         CameraX.initialize(mContext, mConfigBuilder.build());
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
index 74dc16b..eb06e28 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraExecutor.java
@@ -74,12 +74,7 @@
             executor = mThreadPoolExecutor;
         }
 
-        int cameraNumber = 0;
-        try {
-            cameraNumber = cameraFactory.getAvailableCameraIds().size();
-        } catch (CameraUnavailableException e) {
-            e.printStackTrace();
-        }
+        int cameraNumber = cameraFactory.getAvailableCameraIds().size();
         // According to the document of ThreadPoolExecutor, "If there are more than corePoolSize
         // but less than maximumPoolSize threads running, a new thread will be created only if
         // the queue is full."
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
index cf53190..ea5eea6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraX.java
@@ -563,7 +563,7 @@
                         mSchedulerHandler);
 
                 mCameraFactory = cameraFactoryProvider.newInstance(mAppContext,
-                        cameraThreadConfig);
+                        cameraThreadConfig, null);
                 CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
                         mCameraXConfig.getDeviceSurfaceManagerProvider(null);
                 if (surfaceManagerProvider == null) {
@@ -572,7 +572,8 @@
                                     + "CameraDeviceSurfaceManager."));
                 }
                 mSurfaceManager = surfaceManagerProvider.newInstance(mAppContext,
-                        mCameraFactory.getCameraManager());
+                        mCameraFactory.getCameraManager(),
+                        mCameraFactory.getAvailableCameraIds());
 
                 UseCaseConfigFactory.Provider configFactoryProvider =
                         mCameraXConfig.getUseCaseConfigFactoryProvider(null);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
index 12fe5ea..ba1a31e 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/VideoCapture.java
@@ -622,6 +622,7 @@
                 if (isCurrentCamera(cameraId)) {
                     // Only reset the pipeline when the bound camera is the same.
                     setupEncoder(cameraId, resolution);
+                    notifyReset();
                 }
             }
         });
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
index d0a633d..00df7bd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraDeviceSurfaceManager.java
@@ -25,6 +25,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Camera device manager to provide the guaranteed supported stream capabilities related info for
@@ -41,12 +42,13 @@
          *
          * @param context the android context
          * @param cameraManager the camera manager object used to query the camera information.
+         * @param availableCameraIds current available camera ids.
          * @return the factory instance
          * @throws InitializationException if it fails to create the factory
          */
         @NonNull
         CameraDeviceSurfaceManager newInstance(@NonNull Context context,
-                @Nullable Object cameraManager)
+                @Nullable Object cameraManager, @NonNull Set<String> availableCameraIds)
                 throws InitializationException;
     }
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
index 6f35f16..3efbda6 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraFactory.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.camera.core.CameraSelector;
 import androidx.camera.core.CameraUnavailableException;
 import androidx.camera.core.InitializationException;
 
@@ -39,11 +40,14 @@
          *
          * @param context the android context
          * @param threadConfig the thread config to run the camera operations
+         * @param availableCamerasSelector a CameraSelector used to specify which cameras will be
+         *                                 loaded and available to CameraX.
          * @return the factory instance
          * @throws InitializationException if it fails to create the factory.
          */
         @NonNull CameraFactory newInstance(@NonNull Context context,
-                @NonNull CameraThreadConfig threadConfig) throws InitializationException;
+                @NonNull CameraThreadConfig threadConfig,
+                @Nullable CameraSelector availableCamerasSelector) throws InitializationException;
     }
 
     /**
@@ -63,11 +67,9 @@
      * Gets the ids of all available cameras.
      *
      * @return the list of available cameras
-     * @throws CameraUnavailableException if unable to access cameras, perhaps due
-     *                                    to insufficient permissions.
      */
     @NonNull
-    Set<String> getAvailableCameraIds() throws CameraUnavailableException;
+    Set<String> getAvailableCameraIds();
 
     /**
      * Gets the camera manager instance that is used to access the camera API.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
index 15de65f..7fa7f6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageAnalysisTest.java
@@ -104,7 +104,7 @@
 
         CameraInternal camera = new FakeCamera();
 
-        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
             FakeCameraFactory cameraFactory = new FakeCameraFactory();
             cameraFactory.insertDefaultBackCamera(camera.getCameraInfoInternal().getCameraId(),
                     () -> camera);
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
index c085467..25a96c7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureTest.kt
@@ -30,7 +30,6 @@
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor
 import androidx.camera.core.ImageCapture.ImageCaptureRequestProcessor.ImageCaptor
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.TagBundle
@@ -71,7 +70,6 @@
 import java.util.concurrent.ExecutionException
 import java.util.concurrent.Executor
 import java.util.concurrent.atomic.AtomicReference
-import kotlin.jvm.Throws
 
 private const val MAX_IMAGES = 3
 
@@ -104,7 +102,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 3ee0eb8..986d317 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -23,6 +23,7 @@
 import android.util.Rational
 import android.util.Size
 import android.view.Surface
+import androidx.camera.core.SurfaceRequest.TransformationInfo
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.SessionConfig
@@ -68,7 +69,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(
                     camera.cameraInfoInternal.cameraId
@@ -169,7 +170,7 @@
         cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
 
         // Set SurfaceProvider
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
@@ -209,7 +210,7 @@
             ApplicationProvider.getApplicationContext(), TEST_CAMERA_SELECTOR
         )
         cameraUseCaseAdapter!!.addUseCases(Collections.singleton<UseCase>(preview))
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
@@ -248,7 +249,7 @@
         // Get pending SurfaceRequest created by pipeline.
         val pendingSurfaceRequest = preview.mCurrentSurfaceRequest
         var receivedSurfaceRequest: SurfaceRequest? = null
-        var receivedTransformationInfo: SurfaceRequest.TransformationInfo? = null
+        var receivedTransformationInfo: TransformationInfo? = null
 
         // Act: set a SurfaceProvider after attachment.
         preview.setSurfaceProvider { request ->
@@ -323,13 +324,12 @@
         return bindToLifecycleAndGetResult(null).first
     }
 
-    private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?):
-        SurfaceRequest.TransformationInfo {
-            return bindToLifecycleAndGetResult(viewPort).second
-        }
+    private fun bindToLifecycleAndGetTransformationInfo(viewPort: ViewPort?): TransformationInfo {
+        return bindToLifecycleAndGetResult(viewPort).second
+    }
 
     private fun bindToLifecycleAndGetResult(viewPort: ViewPort?): Pair<SurfaceRequest,
-        SurfaceRequest.TransformationInfo> {
+        TransformationInfo> {
         // Arrange.
         val sessionOptionUnpacker =
             { _: UseCaseConfig<*>?, _: SessionConfig.Builder? -> }
@@ -338,7 +338,7 @@
             .setSessionOptionUnpacker(sessionOptionUnpacker)
             .build()
         var surfaceRequest: SurfaceRequest? = null
-        var transformationInfo: SurfaceRequest.TransformationInfo? = null
+        var transformationInfo: TransformationInfo? = null
         preview.setSurfaceProvider { request ->
             request.setTransformationInfoListener(
                 CameraXExecutors.directExecutor(),
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
index 65074ab..2cc5e17 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/VideoCaptureTest.kt
@@ -20,7 +20,6 @@
 import android.os.Build
 import android.os.Looper
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
@@ -52,7 +51,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index f38206c..a361f64 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,7 +26,6 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.Preview
 import androidx.camera.core.impl.CameraFactory
-import androidx.camera.core.impl.CameraThreadConfig
 import androidx.camera.testing.fakes.FakeAppConfig
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
@@ -422,7 +421,7 @@
     @Test
     fun bindUseCases_withNotExistedLensFacingCamera() {
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _, _, _ ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertCamera(
                     CameraSelector.LENS_FACING_BACK,
@@ -441,7 +440,7 @@
 
         val appConfigBuilder = CameraXConfig.Builder()
             .setCameraFactoryProvider(cameraFactoryProvider)
-            .setDeviceSurfaceManagerProvider { _, _ -> FakeCameraDeviceSurfaceManager() }
+            .setDeviceSurfaceManagerProvider { _, _, _ -> FakeCameraDeviceSurfaceManager() }
             .setUseCaseConfigFactoryProvider { FakeUseCaseConfigFactory() }
 
         ProcessCameraProvider.configureInstance(appConfigBuilder.build())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 8a551a6..9dd7dec 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -38,7 +38,7 @@
     /** Generates a fake {@link CameraXConfig}. */
     @NonNull
     public static CameraXConfig create() {
-        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2) -> {
+        CameraFactory.Provider cameraFactoryProvider = (ignored1, ignored2, ignored3) -> {
             FakeCameraFactory cameraFactory = new FakeCameraFactory();
             cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK, CAMERA_ID_0,
                     () -> new FakeCamera(CAMERA_ID_0, null,
@@ -52,7 +52,7 @@
         };
 
         CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
-                (context, cameraManager) -> new FakeCameraDeviceSurfaceManager();
+                (ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
 
         CameraXConfig.Builder appConfigBuilder =
                 new CameraXConfig.Builder()
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
index 8e0613f..3b16b89 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/VideoCaptureLegacy.java
@@ -627,6 +627,7 @@
                 if (isCurrentCamera(cameraId)) {
                     // Only reset the pipeline when the bound camera is the same.
                     setupEncoder(cameraId, resolution);
+                    notifyReset();
                 }
             }
         });
diff --git a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
index 5c97071..085efd3 100644
--- a/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
+++ b/camera/camera-video/src/test/java/androidx/camera/video/VideoCaptureLegacyTest.kt
@@ -19,6 +19,7 @@
 import android.content.Context
 import android.os.Build
 import android.os.Looper
+import androidx.camera.core.CameraSelector
 import androidx.camera.core.CameraX
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.impl.CameraFactory
@@ -54,7 +55,7 @@
         val camera = FakeCamera()
 
         val cameraFactoryProvider =
-            CameraFactory.Provider { _: Context?, _: CameraThreadConfig? ->
+            CameraFactory.Provider { _: Context?, _: CameraThreadConfig?, _: CameraSelector? ->
                 val cameraFactory = FakeCameraFactory()
                 cameraFactory.insertDefaultBackCamera(camera.cameraInfoInternal.cameraId) {
                     camera
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 21e6f30..46089e9 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -114,6 +114,7 @@
      */
     @UseExperimental(markerClass = ExperimentalVideo.class)
     @Retention(RetentionPolicy.SOURCE)
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
     @IntDef(flag = true, value = {IMAGE_CAPTURE, IMAGE_ANALYSIS, VIDEO_CAPTURE})
     public @interface UseCases {
     }
diff --git a/camera/integration-tests/coretestapp/build.gradle b/camera/integration-tests/coretestapp/build.gradle
index a304989..bee7e6ae 100644
--- a/camera/integration-tests/coretestapp/build.gradle
+++ b/camera/integration-tests/coretestapp/build.gradle
@@ -69,13 +69,13 @@
     implementation(project(":camera:camera-camera2-pipe-integration"))
     implementation(project(":camera:camera-core"))
     implementation(project(":camera:camera-lifecycle"))
+    implementation(project(":appcompat:appcompat"))
+    implementation(project(":activity:activity"))
+    implementation(project(":fragment:fragment"))
     implementation("androidx.concurrent:concurrent-futures:1.0.0")
 
     // Android Support Library
     api(CONSTRAINT_LAYOUT, { transitive = true })
-    implementation("androidx.appcompat:appcompat:1.1.0")
-    implementation("androidx.activity:activity:1.2.0-alpha05")
-    implementation("androidx.fragment:fragment:1.3.0-alpha05")
     implementation(GUAVA_ANDROID)
     implementation(ESPRESSO_IDLING_RESOURCE)
 
diff --git a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
index e6efb98..e288a77 100644
--- a/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
+++ b/car/app/app/src/androidTest/java/androidx/car/app/model/ActionStripTest.java
@@ -36,6 +36,20 @@
     }
 
     @Test
+    public void defaultBackgroundColor_doesNotThrow() {
+        Action action = Action.builder().setTitle("Test").setBackgroundColor(
+                CarColor.DEFAULT).build();
+    }
+
+    @Test
+    public void backgroundColor_throws() {
+        Action action1 = Action.builder().setTitle("Test").setBackgroundColor(
+                CarColor.BLUE).build();
+        assertThrows(IllegalArgumentException.class,
+                () -> ActionStrip.builder().addAction(action1));
+    }
+
+    @Test
     public void addDuplicatedTypes_throws() {
         Action action1 = Action.BACK;
         Action action2 = Action.builder().setTitle("Test").setOnClickListener(() -> {
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index ecfad77..00ce152 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -51,14 +51,15 @@
  * <h4>Accessing Location</h4>
  *
  * When the app is running in the car display, the system will not consider it as being in the
- * foreground, and hence it will considered in the background for the purpose of retrieving location
- * as described <a
+ * foreground, and hence it will be considered in the background for the purpose of retrieving
+ * location as described <a
  * href="https://developer.android.com/about/versions/10/privacy/changes#app-access-device
  * -location">here</a>.
  *
  * <p>To reliably get location for your car app, we recommended that you use a <a
  * href="https://developer.android.com/guide/components/services?#Types-of-services">foreground
- * service</a>.
+ * service</a>. Also note that accessing location may become unreliable when the phone is in the
+ * battery saver mode.
  */
 // This lint warning is triggered because this has a finish() API. Suppress because we are not
 // actually cleaning any held resources in that method.
diff --git a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
index 19ee2d5..ee98a8c 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/ActionStrip.java
@@ -117,17 +117,23 @@
         /**
          * Adds an {@link Action} to the list.
          *
+         * @throws IllegalArgumentException if the background color of the action is specified.
          * @throws IllegalArgumentException if {@code action} is a standard action and an action of
          *                                  the same type has already been added.
          * @throws NullPointerException     if {@code action} is {@code null}.
          */
         @NonNull
         public Builder addAction(@NonNull Action action) {
-            int actionType = requireNonNull(action).getType();
+            Action actionObj = requireNonNull(action);
+            int actionType = actionObj.getType();
             if (actionType != Action.TYPE_CUSTOM && mAddedActionTypes.contains(actionType)) {
                 throw new IllegalArgumentException(
                         "Duplicated action types are disallowed: " + action);
             }
+            if (!CarColor.DEFAULT.equals(actionObj.getBackgroundColor())) {
+                throw new IllegalArgumentException(
+                        "Action strip actions don't support background colors");
+            }
             mAddedActionTypes.add(actionType);
             mActions.add(action);
             return this;
diff --git a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
index ae04556..5ae0e97 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/CarIcon.java
@@ -49,7 +49,8 @@
  *
  * <p>Similar to Android devices, car screens cover a wide range of pixel densities. To ensure that
  * icons and images render well across all car screens, use vector assets whenever possible to avoid
- * scaling issues.
+ * scaling issues. If you use a bitmap instead, ensure that you have resources that address multiple
+ * pixel density buckets.
  *
  * <p>In order to support all car screen sizes and pixel density, you can use configuration
  * qualifiers in your resource files (e.g. "mdpi", "hdpi", etc). See
diff --git a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
index 49e6d3a..95fa7cf 100644
--- a/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
+++ b/car/app/app/src/main/java/androidx/car/app/model/GridItem.java
@@ -69,7 +69,7 @@
     /**
      * Represents a large image to be displayed in the grid item.
      *
-     * <p>If necessary, these images will be scaled down to fit within a 80 x 80 dp bounding box,
+     * <p>If necessary, these images will be scaled down to fit within a 64 x 64 dp bounding box,
      * preserving their aspect ratio.
      */
     public static final int IMAGE_TYPE_LARGE = (1 << 1);
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
index 7a1c100..25721ed 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/Step.java
@@ -58,6 +58,9 @@
      * <p>A cue must always be set when the step is created and is used as a fallback when {@link
      * Maneuver} is not set or is unavailable.
      *
+     * <p>Some cluster displays do not support UTF-8 encoded characters, in which case unsupported
+     * characters will not be displayed properly.
+     *
      * @throws NullPointerException if {@code cue} is {@code null}.
      * @see Builder#setCue(CharSequence)
      */
diff --git a/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java b/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java
new file mode 100644
index 0000000..9994190
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/ArrayMapTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.collection;
+
+import static org.junit.Assert.fail;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ConcurrentModificationException;
+
+@RunWith(JUnit4.class)
+public class ArrayMapTest {
+    ArrayMap<String, String> mMap = new ArrayMap<>();
+
+    /**
+     * Attempt to generate a ConcurrentModificationException in ArrayMap.
+     * <p>
+     * ArrayMap is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+     * this up; ArrayMap should (in the spirit of the core Java collection types) make an effort to
+     * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+     * internals.
+     */
+    @Test
+    public void testConcurrentModificationException() throws Exception {
+        final int testLenMs = 5000;
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                int i = 0;
+                while (mMap != null) {
+                    try {
+                        mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT");
+                    } catch (ArrayIndexOutOfBoundsException e) {
+                        fail(e.getMessage());
+                    } catch (ClassCastException e) {
+                        fail(e.getMessage());
+                    } catch (ConcurrentModificationException e) {
+                        System.out.println("[successfully caught CME at put #" + i
+                                + " size=" + (mMap == null ? "??" : String.valueOf(mMap.size()))
+                                + "]");
+                    }
+                }
+            }
+        }).start();
+        for (int i = 0; i < (testLenMs / 100); i++) {
+            try {
+                Thread.sleep(100);
+                mMap.clear();
+            } catch (InterruptedException e) {
+            } catch (ArrayIndexOutOfBoundsException e) {
+                fail(e.getMessage());
+            } catch (ClassCastException e) {
+                fail(e.getMessage());
+            } catch (ConcurrentModificationException e) {
+                System.out.println(
+                        "[successfully caught CME at clear #"
+                                + i + " size=" + mMap.size() + "]");
+            }
+        }
+        mMap = null; // will stop other thread
+        System.out.println();
+    }
+
+    /**
+     * Check to make sure the same operations behave as expected in a single thread.
+     */
+    @Test
+    public void testNonConcurrentAccesses() throws Exception {
+        for (int i = 0; i < 100000; i++) {
+            try {
+                mMap.put(String.format("key %d", i++), "B_DONT_DO_THAT");
+                if (i % 200 == 0) {
+                    System.out.print(".");
+                }
+                if (i % 500 == 0) {
+                    mMap.clear();
+                    System.out.print("X");
+                }
+            } catch (ConcurrentModificationException e) {
+                fail(e.getMessage());
+            }
+        }
+    }
+}
diff --git a/collection/collection/src/test/java/androidx/collection/ArraySetTest.java b/collection/collection/src/test/java/androidx/collection/ArraySetTest.java
new file mode 100644
index 0000000..643e67d
--- /dev/null
+++ b/collection/collection/src/test/java/androidx/collection/ArraySetTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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.collection;
+
+import static org.junit.Assert.fail;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.ConcurrentModificationException;
+
+@RunWith(JUnit4.class)
+public class ArraySetTest {
+    ArraySet<String> mSet = new ArraySet<>();
+
+    @After
+    public void tearDown() {
+        mSet = null;
+    }
+
+    /**
+     * Attempt to generate a ConcurrentModificationException in ArraySet.
+     * <p>
+     * ArraySet is explicitly documented to be non-thread-safe, yet it's easy to accidentally screw
+     * this up; ArraySet should (in the spirit of the core Java collection types) make an effort to
+     * catch this and throw ConcurrentModificationException instead of crashing somewhere in its
+     * internals.
+     */
+    @Test
+    public void testConcurrentModificationException() throws Exception {
+        final int testDurMs = 10_000;
+        System.out.println("Starting ArraySet concurrency test");
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                int i = 0;
+                while (mSet != null) {
+                    try {
+                        mSet.add(String.format("key %d", i++));
+                    } catch (ArrayIndexOutOfBoundsException e) {
+                        fail(e.getMessage());
+                    } catch (ClassCastException e) {
+                        fail(e.getMessage());
+                    } catch (ConcurrentModificationException e) {
+                        System.out.println("[successfully caught CME at put #" + i
+                                + " size=" + (mSet == null ? "??" : String.valueOf(mSet.size()))
+                                + "]");
+                    }
+                }
+            }
+        }).start();
+        for (int i = 0; i < (testDurMs / 100); i++) {
+            try {
+                if (mSet.size() % 4 == 0) {
+                    mSet.clear();
+                }
+                System.out.print("X");
+            } catch (ArrayIndexOutOfBoundsException e) {
+                fail(e.getMessage());
+            } catch (ClassCastException e) {
+                fail(e.getMessage());
+            } catch (ConcurrentModificationException e) {
+                System.out.println(
+                        "[successfully caught CME at clear #" + i + " size=" + mSet.size() + "]");
+            }
+        }
+    }
+
+    /**
+     * Check to make sure the same operations behave as expected in a single thread.
+     */
+    @Test
+    public void testNonConcurrentAccesses() throws Exception {
+        for (int i = 0; i < 100000; i++) {
+            try {
+                mSet.add(String.format("key %d", i++));
+                if (i % 200 == 0) {
+                    System.out.print(".");
+                }
+                if (i % 500 == 0) {
+                    mSet.clear();
+                }
+            } catch (ConcurrentModificationException e) {
+                fail(e.getMessage());
+            }
+        }
+    }
+
+}
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
index 668a4f8..1dbecc6 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/AnimationDemos.kt
@@ -40,7 +40,6 @@
             listOf(
                 ComposableDemo("Animated scrolling") { FancyScrollingDemo() },
                 ComposableDemo("animate()") { SingleValueAnimationDemo() },
-                ComposableDemo("Swipe to dismiss") { SwipeToDismissDemo() }
             )
         ),
         DemoCategory(
@@ -58,9 +57,10 @@
         DemoCategory(
             "Suspend Animation Demos",
             listOf(
-                ComposableDemo("Spring back scrolling") { SpringBackScrollingDemo() },
                 ComposableDemo("Follow the tap") { SuspendAnimationDemo() },
                 ComposableDemo("Infinitely Animating") { InfiniteAnimationDemo() },
+                ComposableDemo("Spring back scrolling") { SpringBackScrollingDemo() },
+                ComposableDemo("Swipe to dismiss") { SwipeToDismissDemo() },
             )
         ),
     )
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
index f131f17..9d70c16 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SpringBackScrollingDemo.kt
@@ -21,6 +21,8 @@
 import androidx.compose.animation.core.SpringSpec
 import androidx.compose.animation.core.animateDecay
 import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.isFinished
+import androidx.compose.animation.core.velocity
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.MutatePriority
 import androidx.compose.foundation.MutatorMutex
@@ -64,7 +66,6 @@
 
         var scrollPosition by remember { mutableStateOf(0f) }
         val itemWidth = remember { mutableStateOf(0f) }
-        val isFlinging = remember { mutableStateOf(false) }
         val mutatorMutex = remember { MutatorMutex() }
         var animation by remember { mutableStateOf(AnimationState(scrollPosition)) }
 
@@ -91,31 +92,35 @@
                     launch {
                         mutatorMutex.mutate {
                             animation = AnimationState(scrollPosition, velocity)
+                            val target = ExponentialDecay().getTarget(scrollPosition, velocity)
+                            val springBackTarget: Float = calculateSpringBackTarget(
+                                target,
+                                velocity,
+                                itemWidth.value
+                            )
+
                             animation.animateDecay(ExponentialDecay()) {
                                 scrollPosition = this.value
-
-                                val springBackTarget = calculateSpringBackTarget(
-                                    targetValue,
-                                    this.velocityVector.value, itemWidth.value
-                                )
-
                                 // Spring back as soon as the target position is crossed.
-                                if ((this.velocityVector.value > 0 && value > springBackTarget) ||
-                                    (this.velocityVector.value < 0 && value < springBackTarget)
+                                if ((this.velocity > 0 && value > springBackTarget) ||
+                                    (this.velocity < 0 && value < springBackTarget)
                                 ) {
                                     cancelAnimation()
-                                    launch {
-                                        animation.animateTo(
-                                            springBackTarget,
-                                            SpringSpec(
-                                                dampingRatio = 0.8f,
-                                                stiffness = 200f
-                                            ),
-                                            sequentialAnimation = true
-                                        ) {
-                                            scrollPosition = this.value
-                                        }
-                                    }
+                                }
+                            }
+
+                            // The previous animation is either finished or interrupted (via
+                            // cancelAnimation(). If interrupted, spring back.
+                            if (!animation.isFinished) {
+                                animation.animateTo(
+                                    springBackTarget,
+                                    SpringSpec(
+                                        dampingRatio = 0.8f,
+                                        stiffness = 200f
+                                    ),
+                                    sequentialAnimation = true
+                                ) {
+                                    scrollPosition = this.value
                                 }
                             }
                         }
@@ -125,9 +130,6 @@
         }
         Canvas(gesture.fillMaxWidth().preferredHeight(400.dp)) {
             itemWidth.value = size.width / 2f
-            if (isFlinging.value) {
-                // Figure out what position to spring back to
-            }
             if (DEBUG) {
                 println(
                     "Anim, Spring back scrolling, redrawing with new" +
diff --git a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
index ca72348..1e5ef0d 100644
--- a/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
+++ b/compose/animation/animation/integration-tests/animation-demos/src/main/java/androidx/compose/animation/demos/SwipeToDismissDemo.kt
@@ -16,166 +16,135 @@
 
 package androidx.compose.animation.demos
 
-import androidx.compose.animation.animatedFloat
-import androidx.compose.animation.core.AnimationEndReason
-import androidx.compose.animation.core.ExponentialDecay
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.SpringSpec
-import androidx.compose.animation.core.TargetAnimation
-import androidx.compose.animation.core.fling
-import androidx.compose.foundation.Canvas
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.MutatorMutex
+import androidx.compose.foundation.animation.AndroidFlingDecaySpec
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.verticalDrag
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.gesture.DragObserver
-import androidx.compose.ui.gesture.rawDragGestureFilter
+import androidx.compose.ui.composed
+import androidx.compose.ui.gesture.ExperimentalPointerInput
+import androidx.compose.ui.gesture.util.VelocityTracker
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.drawscope.DrawScope
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.input.pointer.positionChange
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
-import kotlin.math.sign
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlin.math.abs
 
 @Composable
 fun SwipeToDismissDemo() {
     Column {
-        SwipeToDismiss()
+        var index by remember { mutableStateOf(0) }
+        Box(Modifier.height(500.dp).fillMaxWidth()) {
+            Box(
+                Modifier.swipeToDismiss(index).align(Alignment.BottomCenter).size(300.dp)
+                    .background(pastelColors[index])
+            )
+        }
         Text(
             "Swipe up to dismiss",
             fontSize = 30.sp,
-            modifier = Modifier.padding(40.dp)
+            modifier = Modifier.padding(40.dp).align(Alignment.CenterHorizontally)
         )
+        Button(
+            onClick = {
+                index = (index + 1) % pastelColors.size
+            },
+            modifier = Modifier.align(Alignment.CenterHorizontally)
+        ) {
+            Text("New Card")
+        }
     }
 }
 
-private val height = 1600f
-private val itemHeight = 1600f * 2 / 3f
-private val padding = 10f
+@OptIn(ExperimentalPointerInput::class)
+private fun Modifier.swipeToDismiss(index: Int): Modifier = composed {
+    val mutatorMutex = remember { MutatorMutex() }
+    var alpha by remember { mutableStateOf(1f) }
+    var offset by remember { mutableStateOf(0f) }
 
-@Composable
-private fun SwipeToDismiss() {
-    val itemBottom = animatedFloat(height)
-    val index = remember { mutableStateOf(0) }
-    val itemWidth = remember { mutableStateOf(0f) }
-    val isFlinging = remember { mutableStateOf(false) }
-    val modifier = Modifier.rawDragGestureFilter(
-        dragObserver = object : DragObserver {
-            override fun onStart(downPosition: Offset) {
-                itemBottom.setBounds(0f, height)
-                if (isFlinging.value && itemBottom.targetValue < 100f) {
-                    reset()
+    remember(index) {
+        // Reset internal states if index has been updated
+        alpha = 1f
+        offset = 0f
+    }
+    this.pointerInput {
+        fun updateOffset(value: Float) {
+            offset = value
+            alpha = 1f - abs(offset / size.height)
+        }
+        coroutineScope {
+            while (true) {
+                val pointerId = handlePointerInput {
+                    awaitFirstDown().id
                 }
-            }
-
-            private fun reset() {
-                itemBottom.snapTo(height)
-                index.value--
-                if (index.value < 0) {
-                    index.value += pastelColors.size
+                val velocityTracker = VelocityTracker()
+                // Set a high priority on the mutatorMutex for gestures
+                mutatorMutex.mutate(MutatePriority.UserInput) {
+                    handlePointerInput {
+                        verticalDrag(pointerId) {
+                            updateOffset(offset + it.positionChange().y)
+                            velocityTracker.addPosition(
+                                it.current.uptime,
+                                it.current.position
+                            )
+                        }
+                    }
                 }
-            }
-
-            override fun onDrag(dragDistance: Offset): Offset {
-                itemBottom.snapTo(itemBottom.targetValue + dragDistance.y)
-                return dragDistance
-            }
-
-            fun adjustTarget(velocity: Float): (Float) -> TargetAnimation? {
-                return { target: Float ->
-                    // The velocity is fast enough to fly off screen
-                    if (target <= 0) {
-                        null
-                    } else {
-                        val animation = SpringSpec<Float>(
-                            dampingRatio = 0.8f, stiffness = 300f
-                        )
-                        val projectedTarget = target + sign(velocity) * 0.2f * height
-                        if (projectedTarget < 0.6 * height) {
-                            TargetAnimation(0f, animation)
+                val velocity = velocityTracker.calculateVelocity().pixelsPerSecond.y
+                launch {
+                    // Use mutatorMutex to make sure drag gesture would cancel any on-going
+                    // animation job.
+                    mutatorMutex.mutate {
+                        // Either fling out of the sight, or snap back
+                        val animationState = AnimationState(offset, velocity)
+                        val decay = AndroidFlingDecaySpec(this@pointerInput)
+                        if (decay.getTarget(offset, velocity) >= -size.height) {
+                            // Not enough velocity to be dismissed
+                            animationState.animateTo(0f) {
+                                updateOffset(value)
+                            }
                         } else {
-                            TargetAnimation(height, animation)
+                            animationState.animateDecay(decay) {
+                                // End animation early if it reaches the bounds
+                                if (value <= -size.height) {
+                                    cancelAnimation()
+                                    updateOffset(-size.height.toFloat())
+                                } else {
+                                    updateOffset(value)
+                                }
+                            }
                         }
                     }
                 }
             }
-
-            override fun onStop(velocity: Offset) {
-                isFlinging.value = true
-                itemBottom.fling(
-                    velocity.y,
-                    ExponentialDecay(3.0f),
-                    adjustTarget(velocity.y),
-                    onEnd = { endReason, final, _ ->
-                        isFlinging.value = false
-                        if (endReason != AnimationEndReason.Interrupted && final == 0f) {
-                            reset()
-                        }
-                    }
-                )
-            }
         }
-    )
-
-    val heightDp = with(AmbientDensity.current) { height.toDp() }
-
-    Canvas(
-        modifier.fillMaxWidth()
-            .preferredHeight(heightDp)
-            .onGloballyPositioned { coordinates ->
-                itemWidth.value = coordinates.size.width * 2 / 3f
-            }
-    ) {
-        val progress = 1 - itemBottom.value / height
-        // TODO: this progress can be used to drive state transitions
-        val alpha = 1f - FastOutSlowInEasing(progress)
-        val horizontalOffset = progress * itemWidth.value
-        drawLeftItems(horizontalOffset, itemWidth.value, itemHeight, index.value)
-        drawDismissingItem(itemBottom.value, itemWidth.value, itemHeight, index.value + 1, alpha)
-    }
+    }.offset(y = { offset }).graphicsLayer(alpha = alpha)
 }
 
-private fun DrawScope.drawLeftItems(
-    horizontalOffset: Float,
-    width: Float,
-    height: Float,
-    index: Int
-) {
-    val offset = Offset(center.x - width * 1.5f + horizontalOffset + padding, size.height - height)
-    val rectSize = Size(width - (2 * padding), height)
-    drawRect(pastelColors[index % pastelColors.size], offset, rectSize)
-
-    if (offset.x >= 0) {
-        // draw another item
-        drawRect(
-            pastelColors[(index - 1 + pastelColors.size) % pastelColors.size],
-            offset - Offset(width, 0.0f),
-            rectSize
-        )
-    }
-}
-
-private fun DrawScope.drawDismissingItem(
-    bottom: Float,
-    width: Float,
-    height: Float,
-    index: Int,
-    alpha: Float
-) = drawRect(
-    pastelColors[index % pastelColors.size],
-    topLeft = Offset(center.x - width / 2 + padding, bottom - height),
-    size = Size(width - (2 * padding), height),
-    alpha = alpha
-)
-
 internal val pastelColors = listOf(
     Color(0xFFffd7d7),
     Color(0xFFffe9d6),
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index d5b7b43..b12654e 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -31,20 +31,21 @@
         1600 to "0.1.0-dev16",
         1700 to "1.0.0-alpha06",
         1800 to "1.0.0-alpha07",
-        1900 to "1.0.0-alpha08"
+        1900 to "1.0.0-alpha08",
+        2000 to "1.0.0-alpha09"
     )
 
     /**
      * The minimum version int that this compiler is guaranteed to be compatible with. Typically
      * this will match the version int that is in ComposeVersion.kt in the runtime.
      */
-    private val minimumRuntimeVersionInt: Int = 1900
+    private val minimumRuntimeVersionInt: Int = 2000
 
     /**
      * The maven version string of this compiler. This string should be updated before/after every
      * release.
      */
-    private val compilerVersion: String = "1.0.0-alpha08"
+    private val compilerVersion: String = "1.0.0-alpha09"
     private val minimumRuntimeVersion: String
         get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
 
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 1cb6ffb..23699a4 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
@@ -68,10 +68,10 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.key.ExperimentalKeyInput
 import androidx.compose.ui.input.key.Key
 import androidx.compose.ui.input.key.plus
@@ -83,7 +83,7 @@
 import androidx.compose.ui.text.PlaceholderVerticalAlign
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.fontFamily
 import androidx.compose.ui.text.platform.font
 import androidx.compose.ui.text.style.TextAlign
@@ -179,7 +179,7 @@
         val inlineIndicatorId = "indicator"
 
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append("The quick ")
                 if (animation.value) {
                     appendInlineContent(inlineIndicatorId)
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
index a36969c..163625a 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/popupexample/AppContent.kt
@@ -18,6 +18,7 @@
 import androidx.compose.desktop.AppManager
 import androidx.compose.desktop.AppWindow
 import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxSize
@@ -31,6 +32,8 @@
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.AlertDialog
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
 import androidx.compose.material.Text
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonConstants
@@ -156,7 +159,8 @@
                         .background(color = Color(255, 255, 255, 10))
                         .fillMaxWidth()
                 ) {
-                    Spacer(modifier = Modifier.height(60.dp))
+                    ContextMenu()
+                    Spacer(modifier = Modifier.height(30.dp))
                     Spacer(modifier = Modifier.height(60.dp))
                     Row {
                         Checkbox(
@@ -339,9 +343,9 @@
 }
 
 @Composable
-fun TextBox(text: String = "") {
+fun TextBox(text: String = "", modifier: Modifier = Modifier.height(30.dp)) {
     Box(
-        modifier = Modifier.height(30.dp),
+        modifier = modifier,
         contentAlignment = Alignment.Center
     ) {
         Text(
@@ -352,6 +356,45 @@
 }
 
 @Composable
+fun ContextMenu() {
+    val items = listOf("Item A", "Item B", "Item C", "Item D", "Item E", "Item F")
+    val showMenu = remember { mutableStateOf(false) }
+    val selectedIndex = remember { mutableStateOf(0) }
+
+    Surface(
+        modifier = Modifier
+            .padding(start = 4.dp, top = 2.dp),
+        color = Color(255, 255, 255, 40),
+        shape = RoundedCornerShape(4.dp)
+    ) {
+        DropdownMenu(
+            toggle = {
+                TextBox(
+                    text = "Selected: ${items[selectedIndex.value]}",
+                    modifier = Modifier
+                        .height(26.dp)
+                        .padding(start = 4.dp, end = 4.dp)
+                        .clickable(onClick = { showMenu.value = true })
+                )
+            },
+            expanded = showMenu.value,
+            onDismissRequest = { showMenu.value = false }
+        ) {
+            items.forEachIndexed { index, name ->
+                DropdownMenuItem(
+                    onClick = {
+                        selectedIndex.value = index
+                        showMenu.value = false
+                    }
+                ) {
+                    Text(text = name)
+                }
+            }
+        }
+    }
+}
+
+@Composable
 fun RadioButton(text: String, state: MutableState<Boolean>) {
     Box(
         modifier = Modifier.height(30.dp),
diff --git a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
index c8141df..fb477e1 100644
--- a/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
+++ b/compose/desktop/desktop/samples/src/jvmMain/kotlin/androidx/compose/desktop/examples/swingexample/Main.kt
@@ -18,7 +18,6 @@
 import androidx.compose.desktop.AppManager
 import androidx.compose.desktop.AppWindowAmbient
 import androidx.compose.desktop.ComposePanel
-import androidx.compose.desktop.setContent
 import androidx.compose.desktop.Window
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
diff --git a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
index 589cdc7..fd818bd 100644
--- a/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
+++ b/compose/foundation/foundation-layout/src/androidAndroidTest/kotlin/androidx/compose/foundation/layout/LayoutOffsetTest.kt
@@ -23,7 +23,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.drawBehind
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.layout.positionInRoot
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -472,7 +472,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
@@ -518,6 +518,10 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class MaxLinesHeightModifierKt {
+    method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+  }
+
   public final class TextFieldCursorKt {
     method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
   }
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -472,7 +472,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
@@ -518,6 +518,10 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class MaxLinesHeightModifierKt {
+    method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+  }
+
   public final class TextFieldCursorKt {
     method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
   }
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index a2571e9..338bc24 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -75,7 +75,7 @@
   }
 
   public interface IndicationInstance {
-    method public void drawIndication(androidx.compose.ui.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
+    method public void drawIndication(androidx.compose.ui.graphics.drawscope.ContentDrawScope, androidx.compose.foundation.InteractionState interactionState);
     method public default void onDispose();
   }
 
@@ -472,7 +472,7 @@
   }
 
   public final class CoreTextFieldKt {
-    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField--SxaZeI(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional int maxLines, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
+    method @androidx.compose.runtime.Composable @androidx.compose.ui.text.InternalTextApi public static void CoreTextField-lVEleRQ(androidx.compose.ui.text.input.TextFieldValue value, optional androidx.compose.ui.Modifier modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.text.TextStyle textStyle, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.ImeAction,kotlin.Unit> onImeActionPerformed, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.SoftwareKeyboardController,kotlin.Unit> onTextInputStarted, optional long cursorColor, optional boolean softWrap, optional androidx.compose.ui.text.input.ImeOptions imeOptions);
   }
 
   public final class CoreTextKt {
@@ -518,6 +518,10 @@
     property public final androidx.compose.foundation.text.KeyboardOptions Default;
   }
 
+  public final class MaxLinesHeightModifierKt {
+    method public static androidx.compose.ui.Modifier maxLinesHeight(androidx.compose.ui.Modifier, @IntRange(from=1) int maxLines, androidx.compose.ui.text.TextStyle textStyle);
+  }
+
   public final class TextFieldCursorKt {
     method @Deprecated @VisibleForTesting public static void setBlinkingCursorEnabled(boolean p);
   }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
index a4ee0a7..1f39a77 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
@@ -26,7 +26,7 @@
 import androidx.compose.ui.text.TextStyle
 
 @Composable
-fun CoreTextFieldMinMaxDemo() {
+fun BasicTextFieldMinMaxDemo() {
     ScrollableColumn {
         TagLine("empty text, no maxLines")
         TextFieldWithMaxLines("", maxLines = Int.MAX_VALUE)
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
index 1959250..2411c77 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeMultiParagraph.kt
@@ -22,7 +22,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.text.withStyle
@@ -52,7 +52,7 @@
     val text1 = "paragraph1 paragraph1 paragraph1 paragraph1 paragraph1"
     val text2 = "paragraph2 paragraph2 paragraph2 paragraph2 paragraph2"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text1)
             withStyle(ParagraphStyle()) {
                 append(text2)
@@ -64,7 +64,7 @@
 
 @Composable
 fun TextDemoParagraphTextAlign() {
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         TextAlign.values().forEach { textAlign ->
             val str = List(4) { "TextAlign.$textAlign" }.joinToString(" ")
             withStyle(ParagraphStyle(textAlign = textAlign)) {
@@ -114,7 +114,7 @@
     val text2 = "TextIndent restLine TextIndent restLine TextIndent restLine"
 
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(ParagraphStyle(textIndent = TextIndent(firstLine = 20.sp))) {
                 append(text1)
             }
@@ -131,7 +131,7 @@
     val ltrText = "Hello World! Hello World! Hello World! Hello World! Hello World!"
     val rtlText = "مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم مرحبا بالعالم"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(ParagraphStyle()) {
                 append(ltrText)
             }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
index 6e7724e..b4d10b0 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeText.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
@@ -104,7 +104,7 @@
 fun TagLine(tag: String) {
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             append("\n")
             withStyle(
                 style = SpanStyle(
@@ -121,7 +121,7 @@
 @Composable
 fun SecondTagLine(tag: String) {
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     color = Color(0xFFAAAAAA),
@@ -139,7 +139,7 @@
     // This group of text composables show different color, fontSize, fontWeight and fontStyle in
     // English.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 SpanStyle(
                     color = Color(0xFFFF0000),
@@ -186,7 +186,7 @@
     // This group of text composables show different color, fontSize, fontWeight and fontStyle in
     // Chinese, Arabic, and Hindi.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     color = Color(0xFFFF0000),
@@ -227,7 +227,7 @@
 fun TextDemoFontFamily() {
     // This group of text composables show different fontFamilies in English.
     Text(
-        annotatedString {
+        buildAnnotatedString {
             withStyle(
                 style = SpanStyle(
                     fontSize = fontSize8,
@@ -279,7 +279,7 @@
 fun TextDemoLetterSpacing() {
     // This group of text composables show different letterSpacing.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(fontSize = fontSize8)) {
                 append("$displayText   ")
             }
@@ -319,7 +319,7 @@
 fun TextDemoBackground() {
     // This group of text composables show different background.
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(background = Color(0xFFFF0000))) {
                 append("$displayText   ")
             }
@@ -341,7 +341,7 @@
     // This group of text composables show different Locales of the same Unicode codepoint.
     val text = "\u82B1"
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(localeList = LocaleList("ja-JP"))) {
                 append("$text   ")
             }
@@ -458,7 +458,7 @@
     )
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             append("text with ")
             withStyle(style = SpanStyle(shadow = shadow)) {
                 append("shadow!")
@@ -471,7 +471,7 @@
 fun TextDemoFontSizeScale() {
     Text(
         style = TextStyle(fontSize = fontSize8),
-        text = annotatedString {
+        text = buildAnnotatedString {
             for (i in 4..12 step 4) {
                 val scale = i * 0.1f
                 withStyle(style = SpanStyle(fontSize = scale.em)) {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
index 4bedcd7..9a8ecbc 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextAccessibility.kt
@@ -22,7 +22,7 @@
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.VerbatimTtsAnnotation
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.intl.LocaleList
 
 @Composable
@@ -30,7 +30,7 @@
     Column {
         TagLine("Text to speech with different locales.")
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 pushStyle(SpanStyle(localeList = LocaleList("en-us")))
                 append("Hello!\n")
                 pop()
@@ -55,7 +55,7 @@
 
         TagLine("VerbatimTtsAnnotation ")
         Text(
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append("This word is read verbatim: ")
                 pushTtsAnnotation(VerbatimTtsAnnotation(verbatim = "hello"))
                 append("hello\n")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
index dda5da3..725ef4c 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelection.kt
@@ -29,7 +29,7 @@
 import androidx.compose.ui.selection.SelectionContainer
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.intl.LocaleList
@@ -61,7 +61,7 @@
                 fontWeight = FontWeight.W200,
                 fontStyle = FontStyle.Italic
             ),
-            text = annotatedString {
+            text = buildAnnotatedString {
                 append(text = "$displayText   ")
                 append(text = "$displayTextArabic   ")
                 append(text = "$displayTextChinese   ")
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
index 4442e6b..9cf591b 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeTextSelectionSample.kt
@@ -30,8 +30,8 @@
 import androidx.compose.ui.selection.SelectionContainer
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 
@@ -114,7 +114,7 @@
     Row {
         Column(Modifier.weight(1f)) {
             Text(
-                text = annotatedString {
+                text = buildAnnotatedString {
                     append("To begin, follow the")
                     withStyle(link) {
                         append(" Jetpack Compose setup instructions ")
@@ -137,7 +137,7 @@
             .background(rectColor)
     )
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(commonStyle.toSpanStyle()) {
                 append(
                     "The setContent block defines the activity's layout. Instead of " +
@@ -165,7 +165,7 @@
         style = commonStyle.merge(header2)
     )
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(commonStyle.toSpanStyle()) {
                 withStyle(commonStyle.toParagraphStyle()) {
                     append(
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index 0f365b4..0788943 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -38,9 +38,9 @@
                 ComposableDemo("Tricky input field") { InputFieldTrickyUseCase() },
                 ComposableDemo("Focus transition") { TextFieldFocusTransition() },
                 ComposableDemo("Tail Following Text Field") { TailFollowingTextFieldDemo() },
-                ComposableDemo("TextField in Scroller") { TextFieldWithScrollerDemo() },
+                ComposableDemo("Scrollable text fields") { ScrollableTextFieldDemo() },
                 ComposableDemo("Soft Wrap") { SoftWrapDemo() },
-                ComposableDemo("Min/Max Lines") { CoreTextFieldMinMaxDemo() },
+                ComposableDemo("Min/Max Lines") { BasicTextFieldMinMaxDemo() },
                 ComposableDemo("Ime SingleLine") { ImeSingleLineDemo() },
                 ComposableDemo("Capitalization/AutoCorrect") { CapitalizationAutoCorrectDemo() },
                 ComposableDemo("TextFieldValue") { TextFieldValueDemo() }
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
index eb8c609..aeb630d 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextFieldWIthScroller.kt
@@ -17,25 +17,49 @@
 package androidx.compose.foundation.demos.text
 
 import androidx.compose.foundation.ScrollableColumn
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.unit.dp
 
+private val LongText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam pellentesque" +
+    " arcu quis diam malesuada pulvinar. In id condimentum metus. Suspendisse potenti. " +
+    "Praesent arcu tortor, ultrices ut vehicula sit amet, accumsan id sem."
+
 @Composable
-fun TextFieldWithScrollerDemo() {
+fun ScrollableTextFieldDemo() {
     ScrollableColumn {
-        val state = remember {
-            mutableStateOf(List(100) { "Line: $it" }.joinToString("\n"))
-        }
-        BasicTextField(
-            value = state.value,
-            onValueChange = { state.value = it },
-            modifier = Modifier
-                .padding(20.dp)
-        )
+        TagLine("Multiline with 200.dp height")
+        MultilineTextField()
+        TagLine("Single line with 200.dp width")
+        SingleLineTextField()
     }
 }
+
+@Composable
+fun MultilineTextField() {
+    val state = remember { mutableStateOf(LongText) }
+    BasicTextField(
+        value = state.value,
+        onValueChange = { state.value = it },
+        modifier = demoTextFieldModifiers.size(200.dp),
+        singleLine = false,
+        textStyle = TextStyle(fontSize = fontSize8)
+    )
+}
+
+@Composable
+fun SingleLineTextField() {
+    val state = remember { mutableStateOf(LongText) }
+    BasicTextField(
+        value = state.value,
+        onValueChange = { state.value = it },
+        modifier = demoTextFieldModifiers.width(200.dp),
+        singleLine = true,
+        textStyle = TextStyle(fontSize = fontSize8)
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
index fe4bcdc..58b2dca 100644
--- a/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
+++ b/compose/foundation/foundation/samples/src/main/java/androidx/compose/foundation/samples/InlineTextContentSample.kt
@@ -28,14 +28,14 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.Placeholder
 import androidx.compose.ui.text.PlaceholderVerticalAlign
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.em
 
 @Sampled
 @Composable
 fun InlineTextContentSample() {
     val myId = "inlineContent"
-    val text = annotatedString {
+    val text = buildAnnotatedString {
         append("Hello")
         // Append a placeholder string "[myBox]" and attach an annotation "inlineContent" on it.
         appendInlineContent(myId, "[myBox]")
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
index 64dc751..591300c 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/ImageTest.kt
@@ -23,6 +23,8 @@
 import androidx.compose.foundation.layout.preferredSizeIn
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.test.R
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawBehind
@@ -50,7 +52,11 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.graphics.toPixelMap
 import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.res.imageResource
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.test.performClick
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import androidx.test.filters.SdkSuppress
@@ -516,4 +522,40 @@
             Assert.assertEquals(Color.Red.toArgb(), getPixel(50, height - 1))
         }
     }
+
+    @Test
+    fun testPainterResourceWithImage() {
+        val testTag = "testTag"
+        var imageColor = Color.Black
+
+        rule.setContent {
+            val painterId = remember {
+                mutableStateOf(R.drawable.ic_vector_square_asset_test)
+            }
+            with(imageResource(R.drawable.ic_image_test)) {
+                // Sample the actual color of the image to make sure we are loading it properly
+                // when we toggle the state of the resource id
+                imageColor = toPixelMap()[width / 2, height / 2]
+            }
+            Image(
+                painterResource(painterId.value),
+                contentScale = ContentScale.FillBounds,
+                modifier = Modifier.testTag(testTag).clickable {
+                    if (painterId.value == R.drawable.ic_vector_square_asset_test) {
+                        painterId.value = R.drawable.ic_image_test
+                    } else {
+                        painterId.value = R.drawable.ic_vector_square_asset_test
+                    }
+                }
+            )
+        }
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { Color.Red }
+
+        rule.onNodeWithTag(testTag).performClick()
+
+        rule.waitForIdle()
+
+        rule.onNodeWithTag(testTag).captureToImage().assertPixels { imageColor }
+    }
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
index ea79178..509ba73 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/IndicationTest.kt
@@ -19,7 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.runtime.mutableStateOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.InspectableValue
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt
new file mode 100644
index 0000000..79c8407
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/MaxLinesHeightModifierTest.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.foundation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.text.CoreTextField
+import androidx.compose.foundation.text.maxLinesHeight
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.platform.InspectableValue
+import androidx.compose.ui.platform.ValueElement
+import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+@OptIn(InternalTextApi::class)
+class MaxLinesHeightModifierTest {
+
+    private val longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " +
+        "eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam," +
+        " quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " +
+        "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu " +
+        "fugiat nulla pariatur."
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Before
+    fun before() {
+        isDebugInspectorInfoEnabled = true
+    }
+
+    @After
+    fun after() {
+        isDebugInspectorInfoEnabled = false
+    }
+
+    @Test
+    fun maxLinesHeight_shortInputText() {
+        val (textLayoutResult, height) = setTextFieldWithMaxLines(TextFieldValue("abc"), 5)
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.lineCount).isEqualTo(1)
+            assertThat(textLayoutResult.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test
+    fun maxLinesHeight_notApplied_infiniteMaxLines() {
+        val (textLayoutResult, height) =
+            setTextFieldWithMaxLines(TextFieldValue(longText), Int.MAX_VALUE)
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.size.height).isEqualTo(height)
+        }
+    }
+
+    @Test(expected = IllegalArgumentException::class)
+    fun maxLinesHeight_invalidValue() {
+        rule.setContent {
+            CoreTextField(
+                value = TextFieldValue(),
+                onValueChange = {},
+                modifier = Modifier.maxLinesHeight(0, TextStyle.Default)
+            )
+        }
+    }
+
+    @Test
+    fun maxLinesHeight_longInputText() {
+        val (textLayoutResult, _) = setTextFieldWithMaxLines(TextFieldValue(longText), 2)
+
+        rule.runOnIdle {
+            assertThat(textLayoutResult).isNotNull()
+            assertThat(textLayoutResult!!.lineCount).isGreaterThan(2)
+        }
+    }
+
+    @Test
+    fun testInspectableValue() {
+        val modifier = Modifier.maxLinesHeight(10, TextStyle.Default) as InspectableValue
+        assertThat(modifier.nameFallback).isEqualTo("maxLinesHeight")
+        assertThat(modifier.inspectableElements.asIterable()).containsExactly(
+            ValueElement("maxLines", 10),
+            ValueElement("textStyle", TextStyle.Default)
+        )
+    }
+
+    private fun setTextFieldWithMaxLines(
+        textFieldValue: TextFieldValue,
+        maxLines: Int
+    ): Pair<TextLayoutResult?, Int?> {
+        var textLayoutResult: TextLayoutResult? = null
+        var height: Int? = null
+        val positionedLatch = CountDownLatch(1)
+
+        rule.setContent {
+            Box(
+                Modifier.onGloballyPositioned {
+                    height = it.size.height
+                    positionedLatch.countDown()
+                }
+            ) {
+                CoreTextField(
+                    value = textFieldValue,
+                    onValueChange = {},
+                    textStyle = TextStyle.Default,
+                    modifier = Modifier
+                        .width(100.dp)
+                        .maxLinesHeight(maxLines, TextStyle.Default),
+                    onTextLayout = { textLayoutResult = it }
+                )
+            }
+        }
+        assertThat(positionedLatch.await(1, TimeUnit.SECONDS)).isTrue()
+
+        return Pair(textLayoutResult, height)
+    }
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
index e170d76..3afb9ea 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/TextFieldScrollTest.kt
@@ -22,8 +22,10 @@
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.preferredSize
+import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.text.CoreTextField
 import androidx.compose.foundation.text.TextFieldScrollerPosition
+import androidx.compose.foundation.text.maxLinesHeight
 import androidx.compose.foundation.text.textFieldScroll
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.savedinstancestate.rememberSavedInstanceState
@@ -102,9 +104,9 @@
                 onValueChange = {},
                 onTextLayout = { textLayoutResultRef.value = it },
                 softWrap = false,
-                maxLines = 1,
                 modifier = Modifier
                     .preferredSize(width = 300.dp, height = 50.dp)
+                    .maxLinesHeight(1, TextStyle.Default)
                     .textFieldScroll(
                         orientation = Orientation.Horizontal,
                         remember { scrollerPosition },
@@ -134,6 +136,37 @@
                 onTextLayout = { textLayoutResultRef.value = it },
                 modifier = Modifier
                     .preferredSize(width = 300.dp, height = 50.dp)
+                    .maxLinesHeight(Int.MAX_VALUE, TextStyle.Default)
+                    .textFieldScroll(
+                        orientation = Orientation.Vertical,
+                        remember { scrollerPosition },
+                        value,
+                        VisualTransformation.None,
+                        textLayoutResultRef,
+                    )
+            )
+        }
+
+        rule.runOnIdle {
+            assertThat(scrollerPosition.maximum).isLessThan(Float.POSITIVE_INFINITY)
+            assertThat(scrollerPosition.maximum).isGreaterThan(0f)
+        }
+    }
+
+    @Test
+    fun testTextField_verticallyScrollable_withLongInput_whenMaxLinesProvided() {
+        val scrollerPosition = TextFieldScrollerPosition()
+        val value = TextFieldValue(longText)
+
+        rule.setContent {
+            val textLayoutResultRef: Ref<TextLayoutResult?> = remember { Ref() }
+            CoreTextField(
+                value = value,
+                onValueChange = {},
+                onTextLayout = { textLayoutResultRef.value = it },
+                modifier = Modifier
+                    .preferredWidth(100.dp)
+                    .maxLinesHeight(3, TextStyle.Default)
                     .textFieldScroll(
                         orientation = Orientation.Vertical,
                         remember { scrollerPosition },
@@ -162,9 +195,9 @@
                 onValueChange = {},
                 onTextLayout = { textLayoutResultRef.value = it },
                 softWrap = false,
-                maxLines = 1,
                 modifier = Modifier
                     .preferredSize(width = 300.dp, height = 50.dp)
+                    .maxLinesHeight(1, TextStyle.Default)
                     .textFieldScroll(
                         orientation = Orientation.Horizontal,
                         remember { scrollerPosition },
@@ -231,7 +264,6 @@
                         onValueChange = {},
                         onTextLayout = { textLayoutResultRef.value = it },
                         softWrap = false,
-                        maxLines = 1,
                         modifier = Modifier
                             .preferredSize(textFieldSize.toDp())
                             .textFieldScroll(
@@ -312,11 +344,11 @@
                 value = value,
                 onValueChange = {},
                 onTextLayout = { textLayoutResultRef.value = it },
-                maxLines = 1,
                 softWrap = false,
                 modifier = Modifier
                     .preferredSize(width = 300.dp, height = 50.dp)
                     .testTag(TextfieldTag)
+                    .maxLinesHeight(1, TextStyle.Default)
                     .textFieldScroll(
                         Orientation.Horizontal,
                         remember { scrollerPosition },
@@ -408,11 +440,11 @@
                 value = value,
                 onValueChange = {},
                 onTextLayout = { textLayoutResultRef.value = it },
-                maxLines = 1,
                 softWrap = false,
                 modifier = Modifier
                     .preferredSize(width = 300.dp, height = 50.dp)
                     .testTag(TextfieldTag)
+                    .maxLinesHeight(1, TextStyle.Default)
                     .textFieldScroll(
                         Orientation.Horizontal,
                         scrollerPosition,
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
index 1dbed00..4d6ba65 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyColumnForTest.kt
@@ -20,7 +20,6 @@
 import androidx.compose.animation.core.ManualAnimationClock
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.fillMaxSize
@@ -39,6 +38,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.TouchSlop
 import androidx.compose.ui.platform.testTag
@@ -72,7 +72,6 @@
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -335,46 +334,6 @@
             .assertIsDisplayed()
     }
 
-    @Ignore("This test is not fully working. To be fixed in b/167913500")
-    @Test
-    fun contentPaddingIsApplied() = with(rule.density) {
-        val itemTag = "item"
-
-        rule.setContent {
-            LazyColumnFor(
-                items = listOf(1),
-                modifier = Modifier.size(100.dp)
-                    .testTag(LazyColumnForTag),
-                contentPadding = PaddingValues(
-                    start = 10.dp,
-                    top = 50.dp,
-                    end = 10.dp,
-                    bottom = 50.dp
-                )
-            ) {
-                Spacer(Modifier.fillParentMaxWidth().preferredHeight(50.dp).testTag(itemTag))
-            }
-        }
-
-        var itemBounds = rule.onNodeWithTag(itemTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-        assertThat(itemBounds.bottom.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-        assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(10.dp.toIntPx())
-        assertThat(itemBounds.right.toIntPx())
-            .isWithin1PixelFrom(100.dp.toIntPx() - 10.dp.toIntPx())
-
-        rule.onNodeWithTag(LazyColumnForTag)
-            .scrollBy(y = 51.dp, density = rule.density)
-
-        itemBounds = rule.onNodeWithTag(itemTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(0)
-        assertThat(itemBounds.bottom.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-    }
-
     @Test
     fun lazyColumnWrapsContent() = with(rule.density) {
         val itemInsideLazyColumn = "itemInsideLazyColumn"
@@ -1020,6 +979,67 @@
         }
     }
 
+    @Test
+    fun itemsAreNotRedrawnDuringScroll() {
+        val items = (0..20).toList()
+        val redrawCount = Array(6) { 0 }
+        rule.setContent {
+            LazyColumnFor(
+                items = items,
+                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
+            ) {
+                Spacer(
+                    Modifier.size(20.dp)
+                        .drawBehind { redrawCount[it]++ }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(LazyColumnForTag)
+            .scrollBy(y = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            redrawCount.forEachIndexed { index, i ->
+                assertWithMessage("Item with index $index was redrawn $i times")
+                    .that(i).isEqualTo(1)
+            }
+        }
+    }
+
+    @Test
+    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+        val items = (0..1).toList()
+        val redrawCount = Array(2) { 0 }
+        var stateUsedInDrawScope by mutableStateOf(false)
+        rule.setContent {
+            LazyColumnFor(
+                items = items,
+                modifier = Modifier.size(100.dp).testTag(LazyColumnForTag)
+            ) {
+                Spacer(
+                    Modifier.size(50.dp)
+                        .drawBehind {
+                            redrawCount[it]++
+                            if (it == 1) {
+                                stateUsedInDrawScope.hashCode()
+                            }
+                        }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            stateUsedInDrawScope = true
+        }
+
+        rule.runOnIdle {
+            assertWithMessage("First items is not expected to be redrawn")
+                .that(redrawCount[0]).isEqualTo(1)
+            assertWithMessage("Second items is expected to be redrawn")
+                .that(redrawCount[1]).isEqualTo(2)
+        }
+    }
+
     private fun SemanticsNodeInteraction.assertTopPositionIsAlmost(expected: Dp) {
         getUnclippedBoundsInRoot().top.assertIsEqualTo(expected, tolerance = 1.dp)
     }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
new file mode 100644
index 0000000..c77dee6
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyListsContentPaddingTest.kt
@@ -0,0 +1,607 @@
+/*
+ * 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.foundation.lazy
+
+import androidx.compose.animation.core.snap
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.preferredHeight
+import androidx.compose.foundation.layout.preferredWidth
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+class LazyListsContentPaddingTest {
+    private val LazyListTag = "LazyList"
+    private val ItemTag = "item"
+    private val ContainerTag = "container"
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    private var itemSize: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) {
+            itemSize = 50.toDp()
+        }
+    }
+
+    @Test
+    fun column_contentPaddingIsApplied() {
+        lateinit var state: LazyListState
+        val containerSize = itemSize * 2
+        val smallPaddingSize = itemSize / 4
+        val largePaddingSize = itemSize
+        rule.setContent {
+            LazyColumnFor(
+                items = listOf(1),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(containerSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = smallPaddingSize,
+                    top = largePaddingSize,
+                    end = smallPaddingSize,
+                    bottom = largePaddingSize
+                )
+            ) {
+                Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(smallPaddingSize)
+            .assertTopPositionInRootIsEqualTo(largePaddingSize)
+            .assertWidthIsEqualTo(containerSize - smallPaddingSize * 2)
+            .assertHeightIsEqualTo(itemSize)
+
+        state.scrollBy(largePaddingSize)
+
+        rule.onNodeWithTag(ItemTag)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun column_contentPaddingIsNotAffectingScrollPosition() {
+        lateinit var state: LazyListState
+        rule.setContent {
+            LazyColumnFor(
+                items = listOf(1),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = itemSize,
+                    bottom = itemSize
+                )
+            ) {
+                Spacer(Modifier.fillParentMaxWidth().preferredHeight(itemSize).testTag(ItemTag))
+            }
+        }
+
+        state.assertScrollPosition(0, 0.dp)
+
+        state.scrollBy(itemSize)
+
+        state.assertScrollPosition(0, itemSize)
+    }
+
+    @Test
+    fun column_scrollForwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(padding)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize + padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+        state.scrollBy(padding)
+
+        state.assertScrollPosition(1, padding - itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun column_scrollBackwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize + padding * 2)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize * 1.5f)
+
+        state.assertScrollPosition(1, itemSize * 0.5f)
+
+        rule.onNodeWithTag("0")
+            .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+    }
+
+    @Test
+    fun column_scrollForwardTillTheEnd() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+        // there are no space to scroll anymore, so it should change nothing
+        state.scrollBy(10.dp)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3 - padding)
+    }
+
+    @Test
+    fun column_scrollForwardTillTheEndAndABitBack() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyColumnFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = padding,
+                    bottom = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize / 2)
+
+        state.assertScrollPosition(2, itemSize / 2)
+
+        rule.onNodeWithTag("1")
+            .assertTopPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertTopPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertTopPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    }
+
+    @Test
+    fun column_contentPaddingAndWrapContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyColumnFor(
+                    items = listOf(1),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(2.dp)
+            .assertTopPositionInRootIsEqualTo(4.dp)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+            .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+    }
+
+    @Test
+    fun column_contentPaddingAndNoContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyColumnFor(
+                    items = listOf(0),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(8.dp)
+            .assertHeightIsEqualTo(12.dp)
+    }
+
+    @Test
+    fun row_contentPaddingIsApplied() {
+        lateinit var state: LazyListState
+        val containerSize = itemSize * 2
+        val smallPaddingSize = itemSize / 4
+        val largePaddingSize = itemSize
+        rule.setContent {
+            LazyRowFor(
+                items = listOf(1),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(containerSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    top = smallPaddingSize,
+                    start = largePaddingSize,
+                    bottom = smallPaddingSize,
+                    end = largePaddingSize
+                )
+            ) {
+                Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertTopPositionInRootIsEqualTo(smallPaddingSize)
+            .assertLeftPositionInRootIsEqualTo(largePaddingSize)
+            .assertHeightIsEqualTo(containerSize - smallPaddingSize * 2)
+            .assertWidthIsEqualTo(itemSize)
+
+        state.scrollBy(largePaddingSize)
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(itemSize)
+    }
+
+    @Test
+    fun row_contentPaddingIsNotAffectingScrollPosition() {
+        lateinit var state: LazyListState
+        val itemSize = with(rule.density) {
+            50.dp.toIntPx().toDp()
+        }
+        rule.setContent {
+            LazyRowFor(
+                items = listOf(1),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize * 2)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = itemSize,
+                    end = itemSize
+                )
+            ) {
+                Spacer(Modifier.fillParentMaxHeight().preferredWidth(itemSize).testTag(ItemTag))
+            }
+        }
+
+        state.assertScrollPosition(0, 0.dp)
+
+        state.scrollBy(itemSize)
+
+        state.assertScrollPosition(0, itemSize)
+    }
+
+    @Test
+    fun row_scrollForwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyRowFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = padding,
+                    end = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(padding)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize + padding)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2 + padding)
+
+        state.scrollBy(padding)
+
+        state.assertScrollPosition(1, padding - itemSize)
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2)
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 3)
+    }
+
+    @Test
+    fun row_scrollBackwardItemWithinStartPaddingDisplayed() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyRowFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(itemSize + padding * 2)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = padding,
+                    end = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize * 1.5f)
+
+        state.assertScrollPosition(1, itemSize * 0.5f)
+
+        rule.onNodeWithTag("0")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 4.5f - padding)
+    }
+
+    @Test
+    fun row_scrollForwardTillTheEnd() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyRowFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = padding,
+                    end = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+
+        // there are no space to scroll anymore, so it should change nothing
+        state.scrollBy(10.dp)
+
+        state.assertScrollPosition(3, 0.dp)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize - padding)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2 - padding)
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 3 - padding)
+    }
+
+    @Test
+    fun row_scrollForwardTillTheEndAndABitBack() {
+        lateinit var state: LazyListState
+        val padding = itemSize * 1.5f
+        rule.setContent {
+            LazyRowFor(
+                items = (0..3).toList(),
+                state = rememberLazyListState().also { state = it },
+                modifier = Modifier.size(padding * 2 + itemSize)
+                    .testTag(LazyListTag),
+                contentPadding = PaddingValues(
+                    start = padding,
+                    end = padding
+                )
+            ) {
+                Spacer(Modifier.size(itemSize).testTag(it.toString()))
+            }
+        }
+
+        state.scrollBy(itemSize * 3)
+        state.scrollBy(-itemSize / 2)
+
+        state.assertScrollPosition(2, itemSize / 2)
+
+        rule.onNodeWithTag("1")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 1.5f - padding)
+        rule.onNodeWithTag("2")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 2.5f - padding)
+        rule.onNodeWithTag("3")
+            .assertLeftPositionInRootIsEqualTo(itemSize * 3.5f - padding)
+    }
+
+    @Test
+    fun row_contentPaddingAndWrapContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyRowFor(
+                    items = listOf(1),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                    Spacer(Modifier.size(itemSize).testTag(ItemTag))
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ItemTag)
+            .assertLeftPositionInRootIsEqualTo(2.dp)
+            .assertTopPositionInRootIsEqualTo(4.dp)
+            .assertWidthIsEqualTo(itemSize)
+            .assertHeightIsEqualTo(itemSize)
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(itemSize + 2.dp + 6.dp)
+            .assertHeightIsEqualTo(itemSize + 4.dp + 8.dp)
+    }
+
+    @Test
+    fun row_contentPaddingAndNoContent() {
+        rule.setContent {
+            Box(modifier = Modifier.testTag(ContainerTag)) {
+                LazyRowFor(
+                    items = listOf(0),
+                    contentPadding = PaddingValues(
+                        start = 2.dp,
+                        top = 4.dp,
+                        end = 6.dp,
+                        bottom = 8.dp
+                    )
+                ) {
+                }
+            }
+        }
+
+        rule.onNodeWithTag(ContainerTag)
+            .assertLeftPositionInRootIsEqualTo(0.dp)
+            .assertTopPositionInRootIsEqualTo(0.dp)
+            .assertWidthIsEqualTo(8.dp)
+            .assertHeightIsEqualTo(12.dp)
+    }
+
+    private fun LazyListState.scrollBy(offset: Dp) {
+        runBlocking {
+            smoothScrollBy(with(rule.density) { offset.toIntPx().toFloat() }, snap())
+        }
+    }
+
+    private fun LazyListState.assertScrollPosition(index: Int, offset: Dp) = with(rule.density) {
+        assertThat(firstVisibleItemIndex).isEqualTo(index)
+        assertThat(firstVisibleItemScrollOffset.toDp().value).isWithin(0.5f).of(offset.value)
+    }
+}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
index 9999744..6540372 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/LazyRowForTest.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.preferredSize
@@ -32,6 +31,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
@@ -50,9 +50,9 @@
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -171,46 +171,6 @@
             .assertIsDisplayed()
     }
 
-    @Ignore("This test is not fully working. To be fixed in b/167913500")
-    @Test
-    fun contentPaddingIsApplied() = with(rule.density) {
-        val itemTag = "item"
-
-        rule.setContent {
-            LazyRowFor(
-                items = listOf(1),
-                modifier = Modifier.size(100.dp)
-                    .testTag(LazyRowForTag),
-                contentPadding = PaddingValues(
-                    start = 50.dp,
-                    top = 10.dp,
-                    end = 50.dp,
-                    bottom = 10.dp
-                )
-            ) {
-                Spacer(Modifier.fillParentMaxHeight().preferredWidth(50.dp).testTag(itemTag))
-            }
-        }
-
-        var itemBounds = rule.onNodeWithTag(itemTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-        assertThat(itemBounds.right.toIntPx()).isWithin1PixelFrom(100.dp.toIntPx())
-        assertThat(itemBounds.top.toIntPx()).isWithin1PixelFrom(10.dp.toIntPx())
-        assertThat(itemBounds.bottom.toIntPx())
-            .isWithin1PixelFrom(100.dp.toIntPx() - 10.dp.toIntPx())
-
-        rule.onNodeWithTag(LazyRowForTag)
-            .scrollBy(x = 51.dp, density = rule.density)
-
-        itemBounds = rule.onNodeWithTag(itemTag)
-            .getUnclippedBoundsInRoot()
-
-        assertThat(itemBounds.left.toIntPx()).isWithin1PixelFrom(0)
-        assertThat(itemBounds.right.toIntPx()).isWithin1PixelFrom(50.dp.toIntPx())
-    }
-
     @Test
     fun lazyRowWrapsContent() = with(rule.density) {
         val itemInsideLazyRow = "itemInsideLazyRow"
@@ -815,4 +775,65 @@
             assertThat(state.firstVisibleItemScrollOffset).isEqualTo(10)
         }
     }
+
+    @Test
+    fun itemsAreNotRedrawnDuringScroll() {
+        val items = (0..20).toList()
+        val redrawCount = Array(6) { 0 }
+        rule.setContent {
+            LazyRowFor(
+                items = items,
+                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
+            ) {
+                Spacer(
+                    Modifier.size(20.dp)
+                        .drawBehind { redrawCount[it]++ }
+                )
+            }
+        }
+
+        rule.onNodeWithTag(LazyRowForTag)
+            .scrollBy(x = 10.dp, density = rule.density)
+
+        rule.runOnIdle {
+            redrawCount.forEachIndexed { index, i ->
+                Truth.assertWithMessage("Item with index $index was redrawn $i times")
+                    .that(i).isEqualTo(1)
+            }
+        }
+    }
+
+    @Test
+    fun itemInvalidationIsNotCausingAnotherItemToRedraw() {
+        val items = (0..1).toList()
+        val redrawCount = Array(2) { 0 }
+        var stateUsedInDrawScope by mutableStateOf(false)
+        rule.setContent {
+            LazyRowFor(
+                items = items,
+                modifier = Modifier.size(100.dp).testTag(LazyRowForTag)
+            ) {
+                Spacer(
+                    Modifier.size(50.dp)
+                        .drawBehind {
+                            redrawCount[it]++
+                            if (it == 1) {
+                                stateUsedInDrawScope.hashCode()
+                            }
+                        }
+                )
+            }
+        }
+
+        rule.runOnIdle {
+            stateUsedInDrawScope = true
+        }
+
+        rule.runOnIdle {
+            Truth.assertWithMessage("First items is not expected to be redrawn")
+                .that(redrawCount[0]).isEqualTo(1)
+            Truth.assertWithMessage("Second items is expected to be redrawn")
+                .that(redrawCount[1]).isEqualTo(2)
+        }
+    }
 }
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt
deleted file mode 100644
index 2591fcf..0000000
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/CoreTextFieldMinMaxLineTest.kt
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.foundation.text
-
-import androidx.compose.foundation.layout.width
-import androidx.compose.runtime.Providers
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.AmbientDensity
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.text.InternalTextApi
-import androidx.compose.ui.text.TextLayoutResult
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.font.ResourceFont
-import androidx.compose.ui.text.font.asFontFamily
-import androidx.compose.ui.text.font.test.R
-import androidx.compose.ui.text.input.TextFieldValue
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.LargeTest
-import com.google.common.truth.Truth.assertThat
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@LargeTest
-@RunWith(AndroidJUnit4::class)
-@OptIn(InternalTextApi::class)
-class CoreTextFieldMinMaxLineTest {
-
-    private val fontFamily = ResourceFont(
-        resId = R.font.sample_font,
-        weight = FontWeight.Normal,
-        style = FontStyle.Normal
-    ).asFontFamily()
-
-    private val density = Density(density = 1f, fontScale = 1f)
-
-    @get:Rule
-    val rule = createComposeRule()
-
-    @Test(expected = IllegalArgumentException::class)
-    fun textField_maxLines_should_be_greater_than_zero() {
-        rule.setContent {
-            Providers(AmbientDensity provides density) {
-                CoreTextField(
-                    value = TextFieldValue(""),
-                    onValueChange = {},
-                    maxLines = 0,
-                )
-            }
-        }
-    }
-
-    @Test
-    fun textField_maxLines_moreThanLineCount_doesNotChangeHeight() {
-        val string = "a"
-        val fontSize = 10.sp
-        val composableWidth = 20.dp // line count will be 1
-        val expectedLineCount = 1
-        val maxLines = 2 // any number greater than 1
-        val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
-        val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
-        with(density) {
-            assertThat(textLayout).isNotNull()
-            assertThat(height).isNotNull()
-            assertThat(height).isEqualTo(fontSize.toIntPx())
-            assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
-        }
-    }
-
-    @Test
-    fun textField_maxLines_equalToLineCount_doesNotChangeHeight() {
-        val string = "a"
-        val fontSize = 10.sp
-        val composableWidth = 20.dp // line count will be 1
-        val expectedLineCount = 1
-        val maxLines = 1 // equal to expectedLineCount
-        val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
-        val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
-        with(density) {
-            assertThat(textLayout).isNotNull()
-            assertThat(height).isNotNull()
-            assertThat(height).isEqualTo(fontSize.toIntPx())
-            assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
-        }
-    }
-
-    @Test
-    fun textField_maxLines_lessThanLineCount_changesHeight() {
-        val density = Density(density = 1f, fontScale = 1f)
-        val string = "a".repeat(10)
-        val fontSize = 10.sp
-        val composableWidth = 20.dp // line count will be 10 * 10 / 20 = 5
-        val expectedLineCount = 5
-        val maxLines = 1
-
-        val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
-        val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
-        with(density) {
-            assertThat(textLayout).isNotNull()
-            assertThat(height).isNotNull()
-            assertThat(height).isEqualTo(fontSize.toIntPx())
-            assertThat(textLayout?.lineCount).isEqualTo(expectedLineCount)
-        }
-    }
-
-    @Test
-    fun textField_maxLines_withEmptyText() {
-        val string = ""
-        val fontSize = 10.sp
-        val composableWidth = 10.dp
-        val maxLines = 5
-        val textStyle = TextStyle(fontFamily = fontFamily, fontSize = fontSize)
-
-        val (textLayout, height) = setContentMaxLines(string, textStyle, composableWidth, maxLines)
-
-        with(density) {
-            assertThat(textLayout).isNotNull()
-            assertThat(height).isNotNull()
-            assertThat(height).isEqualTo(fontSize.toIntPx())
-            assertThat(textLayout?.lineCount).isEqualTo(1)
-        }
-    }
-
-    private fun setContentMaxLines(
-        string: String,
-        textStyle: TextStyle,
-        width: Dp,
-        maxLines: Int = Int.MAX_VALUE
-    ): Pair<TextLayoutResult?, Int?> {
-        var textLayout: TextLayoutResult? = null
-        var height: Int? = null
-
-        rule.setContent {
-            Providers(AmbientDensity provides density) {
-                CoreTextField(
-                    value = TextFieldValue(string),
-                    onValueChange = {},
-                    textStyle = textStyle,
-                    onTextLayout = { textLayout = it },
-                    maxLines = maxLines,
-                    modifier = Modifier
-                        .width(width)
-                        .onGloballyPositioned {
-                            height = it.size.height
-                        }
-                )
-            }
-        }
-
-        return Pair(textLayout, height)
-    }
-}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
index a5f7e13..8297a04 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionContainerTest.kt
@@ -43,7 +43,10 @@
 import androidx.compose.ui.platform.AmbientView
 import androidx.compose.ui.selection.Selection
 import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.TextStyle
@@ -59,6 +62,7 @@
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import androidx.compose.ui.unit.width
 import androidx.test.espresso.matcher.BoundedMatcher
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -396,6 +400,8 @@
     }
 }
 
+private fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
+
 private class PointerInputChangeLog : (PointerEvent, PointerEventPass) -> Unit {
 
     val entries = mutableListOf<PointerInputChangeLogEntry>()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
index dd838db..1570a3a 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/text/selection/SelectionHandleTestUtils.kt
@@ -17,12 +17,8 @@
 package androidx.compose.foundation.text.selection
 
 import android.view.View
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
 import androidx.compose.ui.node.Owner
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.test.junit4.ComposeTestRule
-import androidx.compose.ui.test.onRoot
-import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.window.isPopupLayout
 import androidx.test.espresso.Espresso
 import androidx.test.espresso.Root
@@ -58,15 +54,3 @@
         return matches && popupsMatchedSoFar == index + 1
     }
 }
-
-internal fun ComposeTestRule.rootWidth(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
-
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.width.toDp()
-    }
-}
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
new file mode 100644
index 0000000..4eb583c
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable-hdpi/ic_image_test.png
Binary files differ
diff --git a/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
new file mode 100644
index 0000000..13c4896
--- /dev/null
+++ b/compose/foundation/foundation/src/androidAndroidTest/res/drawable/ic_vector_square_asset_test.xml
@@ -0,0 +1,25 @@
+<!--
+  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.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24"
+        android:viewportHeight="24">
+    <path
+        android:fillColor="#FF0000"
+        android:pathData="L24,0,24,24,0,24z"/>
+</vector>
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
index 31b3a08..9235b86 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt
@@ -16,7 +16,7 @@
 
 package androidx.compose.foundation
 
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
index aa0dd3f..6b94200 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Indication.kt
@@ -21,7 +21,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 04594e9..c30a7c6 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -289,7 +289,6 @@
                 isScrollEnabled,
                 reverseScrolling = reverseScrollDirection
             )
-            .clipToBounds()
             .padding(contentPadding),
         verticalArrangement = verticalArrangement,
         horizontalAlignment = horizontalAlignment,
@@ -334,7 +333,6 @@
                 isScrollEnabled,
                 reverseScrolling = reverseScrollDirection
             )
-            .clipToBounds()
             .padding(contentPadding),
         horizontalArrangement = horizontalArrangement,
         verticalAlignment = verticalAlignment,
@@ -471,7 +469,7 @@
             val absScroll = if (isReversed) scroll - side else -scroll
             val xOffset = if (isVertical) 0 else absScroll.roundToInt()
             val yOffset = if (isVertical) absScroll.roundToInt() else 0
-            placeable.placeRelative(xOffset, yOffset)
+            placeable.placeRelativeWithLayer(xOffset, yOffset)
         }
     }
 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index 8a5801a..b084605 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -45,6 +45,8 @@
     val cachingItemContentFactory = remember { CachingItemContentFactory(itemContentFactory) }
     cachingItemContentFactory.itemContentFactory = itemContentFactory
 
+    val startContentPadding = if (isVertical) contentPadding.top else contentPadding.start
+    val endContentPadding = if (isVertical) contentPadding.bottom else contentPadding.end
     SubcomposeLayout(
         modifier
             .scrollable(
@@ -66,6 +68,8 @@
             isVertical,
             horizontalAlignment,
             verticalAlignment,
+            startContentPadding.toIntPx(),
+            endContentPadding.toIntPx(),
             itemsCount,
             cachingItemContentFactory
         )
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
index 45ab3e43..456a124 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListState.kt
@@ -292,11 +292,15 @@
         isVertical: Boolean,
         horizontalAlignment: Alignment.Horizontal,
         verticalAlignment: Alignment.Vertical,
+        startContentPadding: Int,
+        endContentPadding: Int,
         itemsCount: Int,
         itemContentFactory: (Int) -> @Composable () -> Unit
     ): MeasureResult = with(scope) {
         numMeasurePasses++
         constraints.assertNotNestingScrollableContainers(isVertical)
+        require(startContentPadding >= 0)
+        require(endContentPadding >= 0)
         if (itemsCount <= 0) {
             // empty data set. reset the current scroll and report zero size
             scrollPosition.update(
@@ -340,27 +344,35 @@
             var goingForwardInitialScrollOffset = currentFirstItemScrollOffset
 
             // this will contain all the placeables representing the visible items
-            val visibleItemsPlaceables = mutableListOf<Placeable>()
+            val visibleItemsPlaceables = mutableListOf<List<Placeable>>()
 
-            // we had scrolled backward, which means items before current firstItemScrollOffset
-            // became visible. compose them and update firstItemScrollOffset
+            // include the start padding so we compose items in the padding area. in the end we
+            // will remove it back from the currentFirstItemScrollOffset calculation
+            currentFirstItemScrollOffset -= startContentPadding
+
+            // define min and max offsets (min offset currently includes startPadding)
+            val minOffset = -startContentPadding
+            val maxOffset = (if (isVertical) constraints.maxHeight else constraints.maxWidth)
+
+            // we had scrolled backward or we compose items in the start padding area, which means
+            // items before current firstItemScrollOffset should be visible. compose them and update
+            // firstItemScrollOffset
             while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
                 val previous = DataIndex(currentFirstItemIndex.value - 1)
                 val placeables =
                     subcompose(previous, itemContentFactory(previous.value)).fastMap {
                         it.measure(childConstraints)
                     }
-                visibleItemsPlaceables.addAll(0, placeables)
-                val size = placeables.fastSumBy { if (isVertical) it.height else it.width }
-                currentFirstItemScrollOffset += size
+                visibleItemsPlaceables.add(0, placeables)
+                currentFirstItemScrollOffset += placeables.mainAxisSize(isVertical)
                 currentFirstItemIndex = previous
             }
             // if we were scrolled backward, but there were not enough items before. this means
             // not the whole scroll was consumed
-            if (currentFirstItemScrollOffset < 0) {
+            if (currentFirstItemScrollOffset < minOffset) {
                 scrollDelta += currentFirstItemScrollOffset
                 goingForwardInitialScrollOffset += currentFirstItemScrollOffset
-                currentFirstItemScrollOffset = 0
+                currentFirstItemScrollOffset = minOffset
             }
 
             // remembers the composed placeables which we are not currently placing as they are out
@@ -371,7 +383,7 @@
             // composing visible items starting from goingForwardInitialIndex until we fill the
             // whole viewport
             var index = goingForwardInitialIndex
-            val maxMainAxis = if (isVertical) constraints.maxHeight else constraints.maxWidth
+            val maxMainAxis = maxOffset + endContentPadding
             var mainAxisUsed = -goingForwardInitialScrollOffset
             var maxCrossAxis = 0
             while (mainAxisUsed <= maxMainAxis && index.value < itemsCount) {
@@ -386,7 +398,7 @@
                 }
                 mainAxisUsed += size
 
-                if (mainAxisUsed < 0f) {
+                if (mainAxisUsed < minOffset) {
                     // this item is offscreen and will not be placed. advance firstVisibleItemIndex
                     currentFirstItemIndex = index + 1
                     currentFirstItemScrollOffset -= size
@@ -397,7 +409,7 @@
                     }
                     notUsedButComposedItems.add(placeables)
                 } else {
-                    visibleItemsPlaceables.addAll(placeables)
+                    visibleItemsPlaceables.add(placeables)
                 }
 
                 index++
@@ -405,8 +417,8 @@
 
             // we didn't fill the whole viewport with items starting from firstVisibleItemIndex.
             // lets try to scroll back if we have enough items before firstVisibleItemIndex.
-            if (mainAxisUsed < maxMainAxis) {
-                val toScrollBack = maxMainAxis - mainAxisUsed
+            if (mainAxisUsed < maxOffset) {
+                val toScrollBack = maxOffset - mainAxisUsed
                 currentFirstItemScrollOffset -= toScrollBack
                 mainAxisUsed += toScrollBack
                 while (currentFirstItemScrollOffset < 0 && currentFirstItemIndex > DataIndex(0)) {
@@ -419,16 +431,16 @@
                             it.measure(childConstraints)
                         }
                     }
-                    visibleItemsPlaceables.addAll(0, placeables)
-                    val size = placeables.fastSumBy { if (isVertical) it.height else it.width }
+                    visibleItemsPlaceables.add(0, placeables)
+                    val size = placeables.mainAxisSize(isVertical)
                     currentFirstItemScrollOffset += size
                     currentFirstItemIndex = previous
                 }
                 scrollDelta += toScrollBack
-                if (currentFirstItemScrollOffset < 0) {
+                if (currentFirstItemScrollOffset < minOffset) {
                     scrollDelta += currentFirstItemScrollOffset
                     mainAxisUsed += currentFirstItemScrollOffset
-                    currentFirstItemScrollOffset = 0
+                    currentFirstItemScrollOffset = minOffset
                 }
             }
 
@@ -446,40 +458,71 @@
 
             // Wrap the content of the children
             val layoutWidth = constraints.constrainWidth(
-                if (isVertical) maxCrossAxis else mainAxisUsed
+                if (isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
             )
             val layoutHeight = constraints.constrainHeight(
-                if (!isVertical) maxCrossAxis else mainAxisUsed
+                if (!isVertical) maxCrossAxis else mainAxisUsed + startContentPadding
             )
 
+            // the initial offset for placeables in visibleItemsPlaceables
+            val firstPlaceableOffset = -(currentFirstItemScrollOffset + startContentPadding)
+
+            // compensate the content padding we initially added in currentFirstItemScrollOffset.
+            // if the item is fully located in the start padding area we  need to use the next
+            // item as a value for currentFirstItemIndex
+            if (startContentPadding > 0) {
+                currentFirstItemScrollOffset += startContentPadding
+                var startPaddingItems = 0
+                while (startPaddingItems < visibleItemsPlaceables.lastIndex) {
+                    val size = visibleItemsPlaceables[startPaddingItems].mainAxisSize(isVertical)
+                    if (size <= currentFirstItemScrollOffset) {
+                        startPaddingItems++
+                        currentFirstItemScrollOffset -= size
+                        currentFirstItemIndex++
+                    } else {
+                        break
+                    }
+                }
+            }
+
             // update state with the new calculated scroll position
             scrollPosition.update(
                 index = currentFirstItemIndex,
                 scrollOffset = currentFirstItemScrollOffset,
-                canScrollForward = mainAxisUsed > maxMainAxis
+                canScrollForward = mainAxisUsed > maxOffset
             )
 
             return layout(layoutWidth, layoutHeight) {
-                var currentMainAxis = -currentFirstItemScrollOffset
-                visibleItemsPlaceables.fastForEach {
-                    if (isVertical) {
-                        val x = horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
-                        if (currentMainAxis + it.height > 0 && currentMainAxis < layoutHeight) {
-                            it.place(x, currentMainAxis)
+                var currentMainAxis = firstPlaceableOffset
+                visibleItemsPlaceables.fastForEach { placeables ->
+                    placeables.fastForEach {
+                        if (isVertical) {
+                            val x =
+                                horizontalAlignment.align(it.width, layoutWidth, layoutDirection)
+                            if (currentMainAxis + it.height > minOffset &&
+                                currentMainAxis < layoutHeight + endContentPadding
+                            ) {
+                                it.placeWithLayer(x, currentMainAxis)
+                            }
+                            currentMainAxis += it.height
+                        } else {
+                            val y = verticalAlignment.align(it.height, layoutHeight)
+                            if (currentMainAxis + it.width > minOffset &&
+                                currentMainAxis < layoutWidth + endContentPadding
+                            ) {
+                                it.placeRelativeWithLayer(currentMainAxis, y)
+                            }
+                            currentMainAxis += it.width
                         }
-                        currentMainAxis += it.height
-                    } else {
-                        val y = verticalAlignment.align(it.height, layoutHeight)
-                        if (currentMainAxis + it.width > 0 && currentMainAxis < layoutWidth) {
-                            it.placeRelative(currentMainAxis, y)
-                        }
-                        currentMainAxis += it.width
                     }
                 }
             }
         }
     }
 
+    private fun List<Placeable>.mainAxisSize(isVertical: Boolean) =
+        fastSumBy { if (isVertical) it.height else it.width }
+
     companion object {
         /**
          * The default [Saver] implementation for [LazyListState].
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index ee9880d6..57b9d1b 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -213,6 +213,7 @@
     val scrollerPosition = rememberSavedInstanceState(saver = TextFieldScrollerPosition.Saver) {
         TextFieldScrollerPosition()
     }
+
     CoreTextField(
         value = value,
         onValueChange = onValueChange,
@@ -226,14 +227,15 @@
         onTextInputStarted = onTextInputStarted,
         cursorColor = cursorColor,
         imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
-        maxLines = if (singleLine) 1 else maxLines,
         softWrap = !singleLine,
-        modifier = modifier.textFieldScroll(
-            orientation,
-            scrollerPosition,
-            value,
-            visualTransformation,
-            textLayoutResult
-        )
+        modifier = modifier
+            .maxLinesHeight(if (singleLine) 1 else maxLines, textStyle)
+            .textFieldScroll(
+                orientation,
+                scrollerPosition,
+                value,
+                visualTransformation,
+                textLayoutResult
+            )
     )
 }
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index 3d130cb..37b6ebd 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -126,8 +126,6 @@
  * @param cursorColor Color of the cursor. If [Color.Unspecified], there will be no cursor drawn
  * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
  * text will be positioned as if there was unlimited horizontal space.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1.
  */
 @Composable
 @OptIn(
@@ -147,13 +145,8 @@
     onTextInputStarted: (SoftwareKeyboardController) -> Unit = {},
     cursorColor: Color = Color.Unspecified,
     softWrap: Boolean = true,
-    maxLines: Int = Int.MAX_VALUE,
     imeOptions: ImeOptions = ImeOptions.Default
 ) {
-    require(maxLines > 0) {
-        "maxLines should be greater than 0"
-    }
-
     // If developer doesn't pass new value to TextField, recompose won't happen but internal state
     // and IME may think it is updated. To fix this inconsistent state, enforce recompose.
     val recompose = invalidate
@@ -416,7 +409,8 @@
 
     onDispose { manager.hideSelectionToolbar() }
 
-    val modifiers = modifier.focusRequester(focusRequester)
+    val modifiers = modifier
+        .focusRequester(focusRequester)
         .then(focusObserver)
         .then(cursorModifier)
         .then(pointerModifier)
@@ -433,7 +427,6 @@
                 state.textDelegate,
                 constraints,
                 layoutDirection,
-                maxLines,
                 state.layoutResult
             ).let { (width, height, result) ->
                 if (state.layoutResult != result) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
new file mode 100644
index 0000000..e37d940b
--- /dev/null
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/MaxLinesHeightModifier.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.foundation.text
+
+import androidx.compose.foundation.layout.preferredHeightIn
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.platform.AmbientDensity
+import androidx.compose.ui.platform.AmbientFontLoader
+import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.resolveDefaults
+import androidx.compose.ui.util.annotation.IntRange
+
+/**
+ * Constraint the height of the text field so that it vertically occupies no more than [maxLines]
+ * number of lines.
+ */
+fun Modifier.maxLinesHeight(
+    @IntRange(from = 1) maxLines: Int,
+    textStyle: TextStyle
+) = composed(
+    inspectorInfo = debugInspectorInfo {
+        name = "maxLinesHeight"
+        properties["maxLines"] = maxLines
+        properties["textStyle"] = textStyle
+    }
+) {
+    require(maxLines > 0) {
+        "maxLines must be greater than 0"
+    }
+    if (maxLines == Int.MAX_VALUE) return@composed Modifier
+
+    val density = AmbientDensity.current
+    val resourceLoader = AmbientFontLoader.current
+    val layoutDirection = AmbientLayoutDirection.current
+
+    // Difference between the height of two lines paragraph and one line paragraph gives us
+    // an approximation of height of one line
+    val firstLineHeight = remember(density, resourceLoader, textStyle, layoutDirection) {
+        computeSizeForDefaultText(
+            style = resolveDefaults(textStyle, layoutDirection),
+            density = density,
+            resourceLoader = resourceLoader,
+            text = EmptyTextReplacement,
+            maxLines = 1
+        ).height
+    }
+    val firstTwoLinesHeight = remember(density, resourceLoader, textStyle, layoutDirection) {
+        val twoLines = EmptyTextReplacement + "\n" + EmptyTextReplacement
+        computeSizeForDefaultText(
+            style = resolveDefaults(textStyle, layoutDirection),
+            density = density,
+            resourceLoader = resourceLoader,
+            text = twoLines,
+            maxLines = 2
+        ).height
+    }
+    val lineHeight = firstTwoLinesHeight - firstLineHeight
+    val precomputedMaxLinesHeight = firstLineHeight + lineHeight * (maxLines - 1)
+
+    Modifier.preferredHeightIn(
+        max = with(density) { precomputedMaxLinesHeight.toDp() }
+    )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
index 72c2437..895c040 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldDelegate.kt
@@ -54,7 +54,7 @@
 
 // visible for testing
 internal const val DefaultWidthCharCount = 10 // min width for TextField is 10 chars long
-private val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
+internal val EmptyTextReplacement = "H".repeat(DefaultWidthCharCount) // just a reference character.
 
 /**
  * Computed the default width and height for TextField.
@@ -67,16 +67,18 @@
  *
  * Until we have font metrics APIs, use the height of reference text as a workaround.
  */
-internal fun computeSizeForEmptyText(
+internal fun computeSizeForDefaultText(
     style: TextStyle,
     density: Density,
-    resourceLoader: Font.ResourceLoader
+    resourceLoader: Font.ResourceLoader,
+    text: String = EmptyTextReplacement,
+    maxLines: Int = 1
 ): IntSize {
     val paragraph = Paragraph(
-        text = EmptyTextReplacement,
+        text = text,
         style = style,
         spanStyles = listOf(),
-        maxLines = 1,
+        maxLines = maxLines,
         ellipsis = false,
         density = density,
         resourceLoader = resourceLoader,
@@ -107,27 +109,10 @@
             textDelegate: TextDelegate,
             constraints: Constraints,
             layoutDirection: LayoutDirection,
-            maxLines: Int = Int.MAX_VALUE,
             prevResultText: TextLayoutResult? = null
         ): Triple<Int, Int, TextLayoutResult> {
             val layoutResult = textDelegate.layout(constraints, layoutDirection, prevResultText)
-
-            val height = constrainWithMaxLines(maxLines, layoutResult.size.height, layoutResult)
-            val width = layoutResult.size.width
-
-            return Triple(width, height, layoutResult)
-        }
-
-        private fun constrainWithMaxLines(
-            maxLines: Int,
-            height: Int,
-            layoutResult: TextLayoutResult
-        ): Int {
-            return if (maxLines == Int.MAX_VALUE || layoutResult.lineCount <= maxLines) {
-                height
-            } else {
-                ceil(layoutResult.getLineBottom(maxLines - 1)).toInt()
-            }
+            return Triple(layoutResult.size.width, layoutResult.size.height, layoutResult)
         }
 
         /**
@@ -194,7 +179,7 @@
                     offsetMap.originalToTransformed(value.selection.max) - 1
                 )
             } else {
-                val defaultSize = computeSizeForEmptyText(
+                val defaultSize = computeSizeForDefaultText(
                     textDelegate.style,
                     textDelegate.density,
                     textDelegate.resourceLoader
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
index 905cd82..d51b0511 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldScroll.kt
@@ -38,6 +38,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
+import androidx.compose.ui.text.TextRange
 import androidx.compose.ui.text.input.TextFieldValue
 import androidx.compose.ui.text.input.TransformedText
 import androidx.compose.ui.text.input.VisualTransformation
@@ -75,6 +76,9 @@
             }
         )
 
+        val cursorOffset = scrollerPosition.getOffsetToFollow(textFieldValue.selection)
+        scrollerPosition.previousSelection = textFieldValue.selection
+
         val transformedText = visualTransformation.filter(
             AnnotatedString(textFieldValue.text)
         )
@@ -82,14 +86,14 @@
             Orientation.Vertical ->
                 VerticalScrollLayoutModifier(
                     scrollerPosition,
-                    textFieldValue,
+                    cursorOffset,
                     transformedText,
                     textLayoutResult
                 )
             Orientation.Horizontal ->
                 HorizontalScrollLayoutModifier(
                     scrollerPosition,
-                    textFieldValue,
+                    cursorOffset,
                     transformedText,
                     textLayoutResult
                 )
@@ -108,7 +112,7 @@
 
 private data class VerticalScrollLayoutModifier(
     val scrollerPosition: TextFieldScrollerPosition,
-    val textFieldValue: TextFieldValue,
+    val cursorOffset: Int,
     val transformedText: TransformedText,
     val textLayoutResult: Ref<TextLayoutResult?>
 ) : LayoutModifier {
@@ -122,9 +126,9 @@
 
         return layout(placeable.width, height) {
             val cursorRect = getCursorRectInScroller(
-                textFieldValue = textFieldValue,
+                cursorOffset = cursorOffset,
                 transformedText = transformedText,
-                textLayoutResult = textLayoutResult,
+                textLayoutResult = textLayoutResult.value,
                 rtl = false,
                 textFieldWidth = placeable.width
             )
@@ -144,7 +148,7 @@
 
 private data class HorizontalScrollLayoutModifier(
     val scrollerPosition: TextFieldScrollerPosition,
-    val textFieldValue: TextFieldValue,
+    val cursorOffset: Int,
     val transformedText: TransformedText,
     val textLayoutResult: Ref<TextLayoutResult?>
 ) : LayoutModifier {
@@ -158,9 +162,9 @@
 
         return layout(width, placeable.height) {
             val cursorRect = getCursorRectInScroller(
-                textFieldValue = textFieldValue,
+                cursorOffset = cursorOffset,
                 transformedText = transformedText,
-                textLayoutResult = textLayoutResult,
+                textLayoutResult = textLayoutResult.value,
                 rtl = layoutDirection == LayoutDirection.Rtl,
                 textFieldWidth = placeable.width
             )
@@ -179,14 +183,14 @@
 }
 
 private fun Density.getCursorRectInScroller(
-    textFieldValue: TextFieldValue,
+    cursorOffset: Int,
     transformedText: TransformedText,
-    textLayoutResult: Ref<TextLayoutResult?>,
+    textLayoutResult: TextLayoutResult?,
     rtl: Boolean,
     textFieldWidth: Int
 ): Rect {
-    val cursorRect = textLayoutResult.value?.getCursorRect(
-        transformedText.offsetMap.originalToTransformed(textFieldValue.selection.min)
+    val cursorRect = textLayoutResult?.getCursorRect(
+        transformedText.offsetMap.originalToTransformed(cursorOffset)
     ) ?: Rect.Zero
     val thickness = DefaultCursorThickness.toIntPx()
 
@@ -226,6 +230,12 @@
      */
     private var previousCursorRect: Rect = Rect.Zero
 
+    /**
+     * Keeps the previous selection data in TextFieldValue in order to identify what has changed
+     * in the new selection, and decide which selection offset (start, end) to follow.
+     */
+    var previousSelection: TextRange = TextRange.Zero
+
     fun update(
         orientation: Orientation,
         cursorRect: Rect,
@@ -257,6 +267,14 @@
         }
     }
 
+    fun getOffsetToFollow(selection: TextRange): Int {
+        return when {
+            selection.start != previousSelection.start -> selection.start
+            selection.end != previousSelection.end -> selection.end
+            else -> selection.min
+        }
+    }
+
     companion object {
         val Saver = Saver<TextFieldScrollerPosition, Float>(
             save = { it.offset },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
index 4c51c92..74891fb 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextFieldSize.kt
@@ -84,7 +84,7 @@
     }
 
     private fun computeMinSize(): IntSize {
-        return computeSizeForEmptyText(
+        return computeSizeForDefaultText(
             style = resolveDefaults(style, layoutDirection),
             density = density,
             resourceLoader = resourceLoader
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
index 67e7bb8..89b5fd5 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/TextFieldSelectionManager.kt
@@ -406,20 +406,13 @@
     }
 
     internal fun getHandlePosition(isStartHandle: Boolean): Offset {
-        return if (isStartHandle)
-            getSelectionHandleCoordinates(
-                textLayoutResult = state?.layoutResult!!,
-                offset = offsetMap.originalToTransformed(value.selection.start),
-                isStart = true,
-                areHandlesCrossed = value.selection.reversed
-            )
-        else
-            getSelectionHandleCoordinates(
-                textLayoutResult = state?.layoutResult!!,
-                offset = offsetMap.originalToTransformed(value.selection.end),
-                isStart = false,
-                areHandlesCrossed = value.selection.reversed
-            )
+        val offset = if (isStartHandle) value.selection.start else value.selection.end
+        return getSelectionHandleCoordinates(
+            textLayoutResult = state?.layoutResult!!,
+            offset = offsetMap.originalToTransformed(offset),
+            isStart = isStartHandle,
+            areHandlesCrossed = value.selection.reversed
+        )
     }
 
     /**
diff --git a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
index e57a8bce..087f17a 100644
--- a/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
+++ b/compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.kt
@@ -451,7 +451,7 @@
     val containerSize: Int,
     val minHeight: Float
 ) {
-    private val contentSize = adapter.maxScrollOffset(containerSize) + containerSize
+    private val contentSize get() = adapter.maxScrollOffset(containerSize) + containerSize
     private val visiblePart get() = containerSize.toFloat() / contentSize
 
     val size
diff --git a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
index efd7cab..d2ed8a5 100644
--- a/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
+++ b/compose/foundation/foundation/src/desktopTest/kotlin/androidx/compose/foundation/ScrollbarTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation
 
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
@@ -26,6 +27,7 @@
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
@@ -51,6 +53,7 @@
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
 import org.jetbrains.skija.Surface
 import org.junit.Assert.assertEquals
 import org.junit.Ignore
@@ -223,6 +226,35 @@
         }
     }
 
+    @Test(timeout = 3000)
+    fun `dynamically change content then drag slider to the end`() {
+        runBlocking(Dispatchers.Main) {
+            val isContentVisible = mutableStateOf(false)
+            rule.setContent {
+                TestBox(
+                    size = 100.dp,
+                    scrollbarWidth = 10.dp
+                ) {
+                    if (isContentVisible.value) {
+                        repeat(10) {
+                            Box(Modifier.size(20.dp).testTag("box$it"))
+                        }
+                    }
+                }
+            }
+            onFrame()
+
+            isContentVisible.value = true
+            onFrame()
+
+            rule.onNodeWithTag("scrollbar").performGesture {
+                swipe(start = Offset(0f, 25f), end = Offset(0f, 500f))
+            }
+            onFrame()
+            rule.onNodeWithTag("box0").assertTopPositionInRootIsEqualTo(-100.dp)
+        }
+    }
+
     @Suppress("SameParameterValue")
     @OptIn(ExperimentalFoundationApi::class)
     @Test(timeout = 3000)
@@ -327,6 +359,8 @@
 
     // TODO(demin): move to DesktopComposeTestRule?
     private suspend fun onFrame() {
+        // TODO(demin): probably we don't need `yield` after we fix https://github.com/JetBrains/compose-jb/issues/137
+        yield()
         (rule as DesktopComposeTestRule).owners?.onFrame(canvas, 100, 100, 0)
         rule.awaitIdle()
     }
@@ -366,6 +400,31 @@
         }
     }
 
+    @Composable
+    private fun TestBox(
+        size: Dp,
+        scrollbarWidth: Dp,
+        scrollableContent: @Composable ColumnScope.() -> Unit
+    ) = withTestEnvironment {
+        Box(Modifier.size(size)) {
+            val state = rememberScrollState()
+
+            ScrollableColumn(
+                Modifier.fillMaxSize().testTag("column"),
+                state,
+                content = scrollableContent
+            )
+
+            VerticalScrollbar(
+                adapter = rememberScrollbarAdapter(state),
+                modifier = Modifier
+                    .width(scrollbarWidth)
+                    .fillMaxHeight()
+                    .testTag("scrollbar")
+            )
+        }
+    }
+
     @Suppress("SameParameterValue")
     @OptIn(ExperimentalFoundationApi::class)
     @Composable
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
index 1114272..d3b5a40 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/gestures/SuspendingGestureTestUtil.kt
@@ -28,19 +28,8 @@
 import androidx.compose.runtime.dispatch.MonotonicFrameClock
 import androidx.compose.runtime.withRunningRecomposer
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.TransformOrigin
-import androidx.compose.ui.autofill.Autofill
-import androidx.compose.ui.autofill.AutofillTree
-import androidx.compose.ui.focus.ExperimentalFocus
-import androidx.compose.ui.focus.FocusManager
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.gesture.ExperimentalPointerInput
-import androidx.compose.ui.graphics.Canvas
-import androidx.compose.ui.graphics.Matrix
-import androidx.compose.ui.graphics.Shape
-import androidx.compose.ui.hapticfeedback.HapticFeedback
-import androidx.compose.ui.input.key.ExperimentalKeyInput
-import androidx.compose.ui.input.key.KeyEvent
 import androidx.compose.ui.input.pointer.ConsumedData
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
@@ -50,33 +39,15 @@
 import androidx.compose.ui.input.pointer.PointerInputFilter
 import androidx.compose.ui.input.pointer.PointerInputScope
 import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.materialize
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.node.InternalCoreApi
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.OwnedLayer
-import androidx.compose.ui.node.Owner
-import androidx.compose.ui.node.OwnerSnapshotObserver
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientViewConfiguration
-import androidx.compose.ui.platform.ClipboardManager
-import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
-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.Constraints
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Duration
-import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.Uptime
 import androidx.compose.ui.unit.milliseconds
-import androidx.compose.ui.platform.WindowManager
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.test.runBlockingTest
@@ -137,7 +108,6 @@
                     pointerInputFilter = currentComposer
                         .materialize(Modifier.pointerInput(gestureDetector)) as
                         PointerInputFilter
-                    LayoutNode(0, 0, width, height, pointerInputFilter!! as Modifier)
                 }
             }
             yield()
@@ -348,24 +318,6 @@
         }
     }
 
-    @Suppress("SameParameterValue")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    private fun LayoutNode(x: Int, y: Int, x2: Int, y2: Int, modifier: Modifier = Modifier) =
-        LayoutNode().apply {
-            this.modifier = modifier
-            measureBlocks = object : LayoutNode.NoIntrinsicsMeasureBlocks("not supported") {
-                override fun measure(
-                    measureScope: MeasureScope,
-                    measurables: List<Measurable>,
-                    constraints: Constraints
-                ): MeasureResult =
-                    measureScope.layout(x2 - x, y2 - y) {}
-            }
-            attach(MockOwner())
-            measure(Constraints.fixed(x2 - x, y2 - y))
-            place(x, y)
-        }
-
     internal class TestFrameClock : MonotonicFrameClock {
 
         private val frameCh = Channel<Long>()
@@ -379,130 +331,6 @@
             onFrame(frameCh.receive())
     }
 
-    @OptIn(
-        ExperimentalFocus::class,
-        ExperimentalLayoutNodeApi::class,
-        InternalCoreApi::class
-    )
-    private class MockOwner(
-        val position: IntOffset = IntOffset.Zero,
-        override val root: LayoutNode = LayoutNode()
-    ) : Owner {
-        val onRequestMeasureParams = mutableListOf<LayoutNode>()
-        val onAttachParams = mutableListOf<LayoutNode>()
-        val onDetachParams = mutableListOf<LayoutNode>()
-
-        override val hapticFeedBack: HapticFeedback
-            get() = TODO("Not yet implemented")
-        override val clipboardManager: ClipboardManager
-            get() = TODO("Not yet implemented")
-        override val textToolbar: TextToolbar
-            get() = TODO("Not yet implemented")
-        override val autofillTree: AutofillTree
-            get() = TODO("Not yet implemented")
-        override val autofill: Autofill?
-            get() = TODO("Not yet implemented")
-        override val density: Density
-            get() = Density(1f)
-        override val semanticsOwner: SemanticsOwner
-            get() = TODO("Not yet implemented")
-        override val textInputService: TextInputService
-            get() = TODO("Not yet implemented")
-        override val focusManager: FocusManager
-            get() = TODO("Not yet implemented")
-        override val windowManager: WindowManager
-            get() = TODO("Not yet implemented")
-        override val fontLoader: Font.ResourceLoader
-            get() = TODO("Not yet implemented")
-        override val layoutDirection: LayoutDirection
-            get() = LayoutDirection.Ltr
-        override var showLayoutBounds: Boolean = false
-        override val snapshotObserver = OwnerSnapshotObserver { it.invoke() }
-
-        override fun onRequestMeasure(layoutNode: LayoutNode) {
-            onRequestMeasureParams += layoutNode
-        }
-
-        override fun onRequestRelayout(layoutNode: LayoutNode) {
-        }
-
-        override val hasPendingMeasureOrLayout = false
-
-        override fun onAttach(node: LayoutNode) {
-            onAttachParams += node
-        }
-
-        override fun onDetach(node: LayoutNode) {
-            onDetachParams += node
-        }
-
-        override fun calculatePosition(): IntOffset = position
-
-        override fun requestFocus(): Boolean = false
-
-        @ExperimentalKeyInput
-        override fun sendKeyEvent(keyEvent: KeyEvent): Boolean = false
-
-        override fun measureAndLayout() {
-        }
-
-        override fun createLayer(
-            drawBlock: (Canvas) -> Unit,
-            invalidateParentLayer: () -> Unit
-        ): OwnedLayer {
-            return object : OwnedLayer {
-                override val layerId: Long
-                    get() = 0
-
-                override fun updateLayerProperties(
-                    scaleX: Float,
-                    scaleY: Float,
-                    alpha: Float,
-                    translationX: Float,
-                    translationY: Float,
-                    shadowElevation: Float,
-                    rotationX: Float,
-                    rotationY: Float,
-                    rotationZ: Float,
-                    cameraDistance: Float,
-                    transformOrigin: TransformOrigin,
-                    shape: Shape,
-                    clip: Boolean
-                ) {
-                }
-
-                override fun move(position: IntOffset) {
-                }
-
-                override fun resize(size: IntSize) {
-                }
-
-                override fun drawLayer(canvas: Canvas) {
-                    drawBlock(canvas)
-                }
-
-                override fun updateDisplayList() {
-                }
-
-                override fun invalidate() {
-                }
-
-                override fun destroy() {
-                }
-
-                override fun getMatrix(matrix: Matrix) {
-                }
-            }
-        }
-
-        override fun onSemanticsChange() {
-        }
-
-        override val measureIteration: Long = 0
-        override val viewConfiguration: ViewConfiguration
-            get() = TestViewConfiguration()
-    }
-
     @OptIn(ExperimentalComposeApi::class)
     class EmptyApplier : Applier<Unit> {
         override val current: Unit = Unit
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 1723a18..a10c5d0 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
@@ -43,7 +43,6 @@
 import androidx.compose.ui.text.input.buildTextFieldValue
 import androidx.compose.ui.text.style.TextDecoration
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.any
 import com.nhaarman.mockitokotlin2.eq
@@ -75,8 +74,6 @@
     private lateinit var multiParagraphIntrinsics: MultiParagraphIntrinsics
     private lateinit var textLayoutResult: TextLayoutResult
 
-    private val layoutDirection = LayoutDirection.Ltr
-
     /**
      * Test implementation of offset map which doubles the offset in transformed text.
      */
diff --git a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
index acb51e1..4c14493 100644
--- a/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
+++ b/compose/integration-tests/benchmark/src/androidTest/java/androidx/compose/ui/lazy/LazyListScrollingBenchmark.kt
@@ -41,7 +41,6 @@
 import androidx.compose.ui.layout.Remeasurement
 import androidx.compose.ui.layout.RemeasurementModifier
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.unit.dp
 import androidx.test.filters.LargeTest
 import org.junit.Rule
@@ -303,7 +302,7 @@
 
     @Composable
     fun RegularItem() {
-        Box(Modifier.graphicsLayer().size(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
+        Box(Modifier.size(20.dp).background(Color.Red, RoundedCornerShape(8.dp)))
     }
 
     fun prepareForToggle() {
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
index 72d901a..54eda68 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/DemoFilter.kt
@@ -44,8 +44,8 @@
 import androidx.compose.ui.focusRequester
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.dp
 
 /**
@@ -132,7 +132,7 @@
     onNavigate: (Demo) -> Unit
 ) {
     val primary = MaterialTheme.colors.primary
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         val title = demo.title
         var currentIndex = 0
         val pattern = filterText.toRegex(option = RegexOption.IGNORE_CASE)
diff --git a/compose/integration-tests/macrobenchmark/build.gradle b/compose/integration-tests/macrobenchmark/build.gradle
index 4f210a1..9a6d909a 100644
--- a/compose/integration-tests/macrobenchmark/build.gradle
+++ b/compose/integration-tests/macrobenchmark/build.gradle
@@ -30,6 +30,7 @@
 android.defaultConfig.minSdkVersion 28
 
 dependencies {
+    androidTestImplementation(project(":benchmark:benchmark-junit4"))
     androidTestImplementation(project(":benchmark:benchmark-macro"))
     androidTestImplementation(ANDROIDX_TEST_RULES)
     androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
index 8123740..a9c3920 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/ProcessSpeedProfileValidation.kt
@@ -19,9 +19,10 @@
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
 import androidx.benchmark.macro.MacrobenchmarkRule
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
 import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -31,41 +32,40 @@
  * Macrobenchmark used for local validation of performance numbers coming from MacrobenchmarkRule.
  */
 @LargeTest
+@SdkSuppress(minSdkVersion = 29)
 @RunWith(Parameterized::class)
 class ProcessSpeedProfileValidation(
     private val compilationMode: CompilationMode,
-    private val killProcess: Boolean
+    private val startupMode: StartupMode
 ) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @Test
-    @Ignore("Not running the test in CI")
-    fun start() {
-        val config = MacrobenchmarkConfig(
+    fun start() = benchmarkRule.measureStartupRepeated(
+        MacrobenchmarkConfig(
             packageName = PACKAGE_NAME,
             metrics = listOf(StartupTimingMetric()),
             compilationMode = compilationMode,
-            killProcessEachIteration = killProcess,
-            iterations = 10
-        )
-        benchmarkRule.measureRepeated(config) {
-            pressHome()
-            launchPackageAndWait()
-        }
+            iterations = 3
+        ),
+        startupMode
+    ) {
+        pressHome()
+        launchPackageAndWait()
     }
 
     companion object {
         private const val PACKAGE_NAME = "androidx.compose.integration.demos"
 
-        @Parameterized.Parameters(name = "compilation_mode={0}, kill_process={1}")
+        @Parameterized.Parameters(name = "compilation_mode={0}, startup_mode={1}")
         @JvmStatic
         fun kilProcessParameters(): List<Array<Any>> {
             val compilationModes = listOf(
                 CompilationMode.None,
                 CompilationMode.SpeedProfile(warmupIterations = 3)
             )
-            val processKillOptions = listOf(true, false)
+            val processKillOptions = listOf(StartupMode.WARM, StartupMode.COLD)
             return compilationModes.zip(processKillOptions).map {
                 arrayOf(it.first, it.second)
             }
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
index ae2d39a..ace7a4c 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupDemosMacrobenchmark.kt
@@ -17,30 +17,41 @@
 package androidx.compose.integration.macrobenchmark
 
 import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.benchmark.macro.StartupMode
 import androidx.test.filters.LargeTest
-import org.junit.Ignore
+import androidx.test.filters.SdkSuppress
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 @LargeTest
-@RunWith(AndroidJUnit4::class)
-class StartupDemosMacrobenchmark {
+@SdkSuppress(minSdkVersion = 29)
+@RunWith(Parameterized::class) // Parameterized to work around timeouts (b/174175784)
+class StartupDemosMacrobenchmark(
+    @Suppress("unused") private val ignored: Boolean
+) {
+
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
-    @Ignore("Not running the test in CI")
     @Test
     fun compiledColdStartup() = benchmarkRule.measureStartup(
         profileCompiled = true,
-        coldLaunch = true
+        startupMode = StartupMode.COLD
     )
 
-    @Ignore("Not running the test in CI")
     @Test
     fun uncompiledColdStartup() = benchmarkRule.measureStartup(
         profileCompiled = false,
-        coldLaunch = true
+        startupMode = StartupMode.COLD
     )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters
+        fun startupDemosParameters(): List<Array<Any>> {
+            return listOf(arrayOf(false))
+        }
+    }
 }
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
index 2a9b90d94..fe30427 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/StartupUtils.kt
@@ -16,36 +16,40 @@
 
 package androidx.compose.integration.macrobenchmark
 
+import android.content.Intent
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.MacrobenchmarkConfig
 import androidx.benchmark.macro.MacrobenchmarkRule
-import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingMetric
 
+const val TargetPackage = "androidx.compose.integration.demos"
+
 /**
  * Simplified interface for standardizing e.g. package,
  * compilation types, and iteration count across project
  */
 fun MacrobenchmarkRule.measureStartup(
     profileCompiled: Boolean,
-    coldLaunch: Boolean,
-    setupBlock: MacrobenchmarkScope.() -> Unit = {},
-    measureBlock: MacrobenchmarkScope.() -> Unit = {
-        pressHome()
-        launchPackageAndWait()
-    }
-) = measureRepeated(
+    startupMode: StartupMode,
+    iterations: Int = 5,
+    setupIntent: Intent.() -> Unit = {}
+) = measureStartupRepeated(
     MacrobenchmarkConfig(
-        packageName = "androidx.compose.integration.demos",
+        packageName = TargetPackage,
         metrics = listOf(StartupTimingMetric()),
         compilationMode = if (profileCompiled) {
             CompilationMode.SpeedProfile(warmupIterations = 3)
         } else {
             CompilationMode.None
         },
-        killProcessEachIteration = coldLaunch,
-        iterations = 10
+        iterations = iterations
     ),
-    setupBlock,
-    measureBlock
-)
+    startupMode = startupMode
+) {
+    pressHome()
+    val intent = Intent()
+    intent.setPackage(TargetPackage)
+    setupIntent(intent)
+    launchIntentAndWait(intent)
+}
\ No newline at end of file
diff --git a/compose/material/material/OWNERS b/compose/material/OWNERS
similarity index 100%
rename from compose/material/material/OWNERS
rename to compose/material/OWNERS
diff --git a/compose/material/material-ripple/api/current.txt b/compose/material/material-ripple/api/current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  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 {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/material/material-ripple/api/public_plus_experimental_current.txt b/compose/material/material-ripple/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/public_plus_experimental_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  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 {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/material/material-ripple/api/res-current.txt b/compose/material/material-ripple/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material/material-ripple/api/res-current.txt
diff --git a/compose/material/material-ripple/api/restricted_current.txt b/compose/material/material-ripple/api/restricted_current.txt
new file mode 100644
index 0000000..8d21928
--- /dev/null
+++ b/compose/material/material-ripple/api/restricted_current.txt
@@ -0,0 +1,35 @@
+// Signature format: 4.0
+package androidx.compose.material.ripple {
+
+  @kotlin.RequiresOptIn(message="This ripple API is experimental and is likely to change or to be removed in" + " the future.") public @interface ExperimentalRippleApi {
+  }
+
+  @androidx.compose.material.ripple.ExperimentalRippleApi public fun interface RippleAlpha {
+    method public float alphaForInteraction(androidx.compose.foundation.Interaction interaction);
+  }
+
+  public final class RippleAnimationKt {
+  }
+
+  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 {
+    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
+    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleAlpha rippleAlpha();
+    field public static final androidx.compose.material.ripple.RippleTheme.Companion Companion;
+  }
+
+  public static final class RippleTheme.Companion {
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public androidx.compose.material.ripple.RippleAlpha defaultRippleAlpha-QZCes2I(long contentColor, boolean lightTheme);
+    method @androidx.compose.material.ripple.ExperimentalRippleApi public long defaultRippleColor-QZCes2I(long contentColor, boolean lightTheme);
+  }
+
+  public final class RippleThemeKt {
+    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
+  }
+
+}
+
diff --git a/compose/material/material-ripple/build.gradle b/compose/material/material-ripple/build.gradle
new file mode 100644
index 0000000..296fd0d
--- /dev/null
+++ b/compose/material/material-ripple/build.gradle
@@ -0,0 +1,105 @@
+/*
+ * 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.
+ */
+
+import androidx.build.AndroidXUiPlugin
+import androidx.build.LibraryGroups
+import androidx.build.Publish
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXUiPlugin")
+}
+
+AndroidXUiPlugin.applyAndConfigureKotlinPlugin(project)
+
+dependencies {
+    kotlinPlugin project(":compose:compiler:compiler")
+
+    if(!AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block below
+         */
+        api project(":compose:foundation:foundation")
+        api project(":compose:runtime:runtime")
+
+        implementation(KOTLIN_STDLIB_COMMON)
+        implementation project(":compose:animation:animation")
+        implementation project(":compose:ui:ui-util")
+
+        testImplementation(ANDROIDX_TEST_RULES)
+        testImplementation(ANDROIDX_TEST_RUNNER)
+        testImplementation(JUNIT)
+        testImplementation(TRUTH)
+    }
+}
+
+if(AndroidXUiPlugin.isMultiplatformEnabled(project)) {
+    kotlin {
+        android()
+        jvm("desktop")
+
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+                implementation(KOTLIN_STDLIB_COMMON)
+                api project(":compose:foundation:foundation")
+                api project(":compose:runtime:runtime")
+
+                implementation project(":compose:animation:animation")
+                implementation project(":compose:ui:ui-util")
+            }
+
+            desktopMain.dependencies {
+                implementation(KOTLIN_STDLIB)
+            }
+
+            test.dependencies {
+                implementation(ANDROIDX_TEST_RULES)
+                implementation(ANDROIDX_TEST_RUNNER)
+                implementation(JUNIT)
+                implementation(TRUTH)
+            }
+        }
+    }
+}
+
+androidx {
+    name = "Compose Material Ripple"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    mavenGroup = LibraryGroups.Compose.MATERIAL
+    inceptionYear = "2020"
+    description = "Material ripple used to build interactive components"
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        useIR = true
+    }
+}
+
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += ["-Xallow-jvm-ir-dependencies"]
+    }
+}
diff --git a/compose/material/material-ripple/src/androidMain/AndroidManifest.xml b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
new file mode 100644
index 0000000..0c2bb3c
--- /dev/null
+++ b/compose/material/material-ripple/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright 2018 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest package="androidx.compose.material.ripple" />
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
similarity index 67%
copy from compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
copy to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
index 8a6dfd3..a4942eb 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/ComposeInit.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/ExperimentalRippleApi.kt
@@ -13,17 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui.test
 
-import org.jetbrains.skiko.Library
+package androidx.compose.material.ripple
 
-fun initCompose() {
-    ComposeInit
-}
-
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+@RequiresOptIn(
+    "This ripple API is experimental and is likely to change or to be removed in" +
+        " the future."
+)
+public annotation class ExperimentalRippleApi
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
similarity index 64%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
index f2a65ca..59d79ee 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleIndication.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/Ripple.kt
@@ -26,8 +26,6 @@
 import androidx.compose.foundation.IndicationInstance
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.State
@@ -35,7 +33,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.structuralEqualityPolicy
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
@@ -48,120 +46,134 @@
 import androidx.compose.ui.util.nativeClass
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * 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 Effects grow up to this size. If null is provided the size would be calculated
+ * @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 Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @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.
  */
-@Suppress("ComposableNaming")
 @Deprecated(
-    "Replaced with rememberRippleIndication",
+    "Replaced with rememberRipple",
     ReplaceWith(
-        "rememberRippleIndication(bounded, radius, color)",
-        "androidx.compose.material.ripple.rememberRippleIndication"
+        "rememberRipple(bounded, radius, color)",
+        "androidx.compose.material.ripple.rememberRipple"
     )
 )
 @Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun RippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRippleIndication(
     bounded: Boolean = true,
     radius: Dp? = null,
     color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
     val theme = AmbientRippleTheme.current
     val clock = AmbientAnimationClock.current.asDisposableClock()
     val resolvedColor = color.useOrElse { theme.defaultColor() }
     val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
     colorState.value = resolvedColor
-    val interactionOpacity = theme.rippleOpacity()
+    val rippleAlpha = theme.rippleAlpha()
     return remember(bounded, radius, theme, clock) {
-        RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+        Ripple(bounded, radius ?: Dp.Unspecified, colorState, rippleAlpha, clock)
     }
 }
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * Creates and [remember]s a Ripple using values provided by [RippleTheme].
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * 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 Effects grow up to this size. If null is provided the size would be calculated
- * based on the target layout size.
- * @param color The Ripple color is usually the same color used by the text or iconography in the
- * component. If [Color.Unspecified] is provided the color will be calculated by
- * [RippleTheme.defaultColor]. This color will then have [RippleTheme.rippleOpacity] applied
+ * @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
+ * 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.
  */
 @Composable
-@OptIn(ExperimentalMaterialApi::class)
-fun rememberRippleIndication(
+@OptIn(ExperimentalRippleApi::class)
+public fun rememberRipple(
     bounded: Boolean = true,
-    radius: Dp? = null,
+    radius: Dp = Dp.Unspecified,
     color: Color = Color.Unspecified
-): RippleIndication {
+): Indication {
     val theme = AmbientRippleTheme.current
     val clock = AmbientAnimationClock.current.asDisposableClock()
     val resolvedColor = color.useOrElse { theme.defaultColor() }
     val colorState = remember { mutableStateOf(resolvedColor, structuralEqualityPolicy()) }
     colorState.value = resolvedColor
-    val interactionOpacity = theme.rippleOpacity()
+    val rippleAlpha = theme.rippleAlpha()
     return remember(bounded, radius, theme, clock) {
-        RippleIndication(bounded, radius, colorState, interactionOpacity, clock)
+        Ripple(bounded, radius, colorState, rippleAlpha, clock)
     }
 }
 
 /**
- * Material implementation of [IndicationInstance] that expresses indication via ripples. This
- * [IndicationInstance] will be used by default in Modifier.indication() if you have a
- * [MaterialTheme] set in your hierarchy.
+ * A Ripple is a Material implementation of [Indication] that expresses different [Interaction]s
+ * by drawing ripple animations and state layers.
  *
- * RippleIndication responds to [Interaction.Pressed] by starting a new [RippleAnimation], and
- * responds to other interactions by showing a fixed state layer.
+ * 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].
  *
- * By default this [Indication] with default parameters will be provided by [MaterialTheme]
- * through [androidx.compose.foundation.AmbientIndication], and hence used in interactions such as
- * [androidx.compose.foundation.clickable] out of the box. You can also manually create a
- * [RippleIndication] and provide it to [androidx.compose.foundation.indication] in order to
- * customize its appearance.
+ * 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.
  */
 @Stable
-@OptIn(ExperimentalMaterialApi::class)
-class RippleIndication internal constructor(
+@ExperimentalRippleApi
+private class Ripple(
     private val bounded: Boolean,
-    private val radius: Dp? = null,
-    private var color: State<Color>,
-    private val rippleOpacity: RippleOpacity,
+    private val radius: Dp,
+    private val color: State<Color>,
+    private val rippleAlpha: RippleAlpha,
     private val clock: AnimationClockObservable
 ) : Indication {
     override fun createInstance(): IndicationInstance {
-        return RippleIndicationInstance(bounded, radius, color, rippleOpacity, clock)
+        return RippleIndicationInstance(bounded, radius, color, rippleAlpha, clock)
     }
 
     // to force stability on this indication we need equals and hashcode, there's no value in
@@ -170,12 +182,12 @@
         if (this === other) return true
         if (this.nativeClass() != other?.nativeClass()) return false
 
-        other as RippleIndication
+        other as Ripple
 
         if (bounded != other.bounded) return false
         if (radius != other.radius) return false
         if (color != other.color) return false
-        if (rippleOpacity != other.rippleOpacity) return false
+        if (rippleAlpha != other.rippleAlpha) return false
         if (clock != other.clock) return false
 
         return true
@@ -183,24 +195,24 @@
 
     override fun hashCode(): Int {
         var result = bounded.hashCode()
-        result = 31 * result + (radius?.hashCode() ?: 0)
+        result = 31 * result + radius.hashCode()
         result = 31 * result + color.hashCode()
-        result = 31 * result + rippleOpacity.hashCode()
+        result = 31 * result + rippleAlpha.hashCode()
         result = 31 * result + clock.hashCode()
         return result
     }
 }
 
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
 private class RippleIndicationInstance constructor(
     private val bounded: Boolean,
-    private val radius: Dp? = null,
-    private var color: State<Color>,
-    private val rippleOpacity: RippleOpacity,
+    private val radius: Dp,
+    private val color: State<Color>,
+    private val rippleAlpha: RippleAlpha,
     private val clock: AnimationClockObservable
 ) : IndicationInstance {
 
-    private val stateLayer = StateLayer(clock, bounded, rippleOpacity)
+    private val stateLayer = StateLayer(clock, bounded, rippleAlpha)
 
     private val ripples = mutableStateListOf<RippleAnimation>()
     private var currentPressPosition: Offset? = null
@@ -208,8 +220,12 @@
 
     override fun ContentDrawScope.drawIndication(interactionState: InteractionState) {
         val color = color.value
-        val targetRadius =
-            radius?.toPx() ?: getRippleEndRadius(bounded, size)
+        // TODO: b/174310811 use library function instead of manual logic here
+        val targetRadius = if (radius == Dp.Unspecified) {
+            getRippleEndRadius(bounded, size)
+        } else {
+            radius.toPx()
+        }
         drawContent()
         with(stateLayer) {
             drawStateLayer(interactionState, targetRadius, color)
@@ -250,7 +266,7 @@
     private fun DrawScope.drawRipples(color: Color) {
         ripples.fastForEach {
             with(it) {
-                val alpha = rippleOpacity.opacityForInteraction(Interaction.Pressed)
+                val alpha = rippleAlpha.alphaForInteraction(Interaction.Pressed)
                 if (alpha != 0f) {
                     draw(color.copy(alpha = alpha))
                 }
@@ -287,13 +303,13 @@
  * @see IncomingStateLayerAnimationSpecs
  * @see OutgoingStateLayerAnimationSpecs
  */
-@OptIn(ExperimentalMaterialApi::class)
+@ExperimentalRippleApi
 private class StateLayer(
     clock: AnimationClockObservable,
     private val bounded: Boolean,
-    private val rippleOpacity: RippleOpacity
+    private val rippleAlpha: RippleAlpha
 ) {
-    private val animatedOpacity = AnimatedFloatModel(0f, clock)
+    private val animatedAlpha = AnimatedFloatModel(0f, clock)
     private var previousInteractions: Set<Interaction> = emptySet()
     private var lastDrawnInteraction: Interaction? = null
 
@@ -318,15 +334,15 @@
             if (interaction in previousInteractions) continue
 
             // Move to the next interaction if this is not an interaction we show a state layer for
-            val targetOpacity = rippleOpacity.opacityForInteraction(interaction)
-            if (targetOpacity == 0f) continue
+            val targetAlpha = rippleAlpha.alphaForInteraction(interaction)
+            if (targetAlpha == 0f) continue
 
             // TODO: consider defaults - these will be used for a custom Interaction that we are
             // not aware of, but has an alpha that should be shown because of a custom RippleTheme.
             val incomingAnimationSpec = IncomingStateLayerAnimationSpecs[interaction]
                 ?: TweenSpec(durationMillis = 15, easing = LinearEasing)
 
-            animatedOpacity.animateTo(targetOpacity, incomingAnimationSpec)
+            animatedAlpha.animateTo(targetAlpha, incomingAnimationSpec)
 
             lastDrawnInteraction = interaction
             handled = true
@@ -342,7 +358,7 @@
                 val outgoingAnimationSpec = OutgoingStateLayerAnimationSpecs[previousInteraction]
                     ?: TweenSpec(durationMillis = 15, easing = LinearEasing)
 
-                animatedOpacity.animateTo(0f, outgoingAnimationSpec)
+                animatedAlpha.animateTo(0f, outgoingAnimationSpec)
 
                 lastDrawnInteraction = null
             }
@@ -350,10 +366,10 @@
 
         previousInteractions = currentInteractions
 
-        val opacity = animatedOpacity.value
+        val alpha = animatedAlpha.value
 
-        if (opacity > 0f) {
-            val modulatedColor = color.copy(alpha = opacity)
+        if (alpha > 0f) {
+            val modulatedColor = color.copy(alpha = alpha)
 
             if (bounded) {
                 clipRect {
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
similarity index 93%
rename from compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
rename to compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
index d1f06c2..18c5b76 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleAnimation.kt
@@ -41,19 +41,18 @@
 import kotlin.math.max
 
 /**
- * [RippleAnimation]s are drawn as part of [RippleIndication] as a visual indicator for an
- * [androidx.compose.foundation.Interaction.Pressed] state.
+ * [RippleAnimation]s are drawn as part of [Ripple] as a visual indicator for an
+ * different [androidx.compose.foundation.Interaction]s.
  *
  * Use [androidx.compose.foundation.clickable] or [androidx.compose.foundation.indication] to add a
- * [RippleIndication] to your component, which contains a RippleAnimation for pressed states, and
+ * ripple to your component, which contains a RippleAnimation for pressed states, and
  * a state layer for other states.
  *
  * This is a default implementation based on the Material Design specification.
  *
- * A circular ripple effect whose origin starts at the input touch point and
- * whose radius expands from 60% of the final value. The ripple origin
- * animates to the center of its target layout for the bounded version
- * and stays in the center for the unbounded one.
+ * Draws a circular ripple effect with an origin starting at the input touch point and with a
+ * radius expanding from 60% of the final value. The ripple origin animates to the center of its
+ * target layout for the bounded version and stays in the center for the unbounded one.
  *
  * @param size The size of the target layout.
  * @param startPosition The position the animation will start from.
diff --git a/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
new file mode 100644
index 0000000..d637ef9
--- /dev/null
+++ b/compose/material/material-ripple/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
@@ -0,0 +1,198 @@
+/*
+ * 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.
+ */
+
+package androidx.compose.material.ripple
+
+import androidx.compose.foundation.Interaction
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.ProvidableAmbient
+import androidx.compose.runtime.staticAmbientOf
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.luminance
+
+/**
+ * Defines the appearance for Ripples. You can define a new theme and apply it using
+ * [AmbientRippleTheme]. See [defaultRippleColor] and [defaultRippleAlpha] for default values
+ * that can be used when creating your own [RippleTheme].
+ *
+ * @see rememberRipple
+ */
+@ExperimentalRippleApi
+public interface RippleTheme {
+    /**
+     * @return the default ripple color at the call site's position in the hierarchy.
+     * This color will be used when a color is not explicitly set in the ripple itself.
+     * @see defaultRippleColor
+     */
+    @Composable
+    public fun defaultColor(): Color
+
+    /**
+     * @return the [RippleAlpha] used to calculate the alpha for the ripple depending on the
+     * [Interaction] for a given component. This will be set as the alpha channel for
+     * [defaultColor] or the color explicitly provided to the ripple.
+     * @see defaultRippleAlpha
+     */
+    @Composable
+    public fun rippleAlpha(): RippleAlpha
+
+    public companion object {
+        /**
+         * Represents the default color that will be used for a ripple if a color has not been
+         * explicitly set on the ripple instance.
+         *
+         * @param contentColor the color of content (text or iconography) in the component that
+         * contains the ripple.
+         * @param lightTheme whether the theme is light or not
+         */
+        @ExperimentalRippleApi
+        public fun defaultRippleColor(
+            contentColor: Color,
+            lightTheme: Boolean
+        ): Color {
+            val contentLuminance = contentColor.luminance()
+            // If we are on a colored surface (typically indicated by low luminance content), the
+            // ripple color should be white.
+            return if (!lightTheme && contentLuminance < 0.5) {
+                Color.White
+                // Otherwise use contentColor
+            } else {
+                contentColor
+            }
+        }
+
+        /**
+         * Represents the default [RippleAlpha] that will be used for a ripple to indicate different
+         * states.
+         *
+         * @param contentColor the color of content (text or iconography) in the component that
+         * contains the ripple.
+         * @param lightTheme whether the theme is light or not
+         */
+        @ExperimentalRippleApi
+        public fun defaultRippleAlpha(contentColor: Color, lightTheme: Boolean): RippleAlpha {
+            return when {
+                lightTheme -> {
+                    if (contentColor.luminance() > 0.5) {
+                        LightThemeHighContrastRippleAlpha
+                    } else {
+                        LightThemeLowContrastRippleAlpha
+                    }
+                }
+                else -> {
+                    DarkThemeRippleAlpha
+                }
+            }
+        }
+    }
+}
+
+/**
+ * RippleAlpha defines the alpha of the ripple / state layer for a given [Interaction].
+ */
+@ExperimentalRippleApi
+public fun interface RippleAlpha {
+    /**
+     * @return the alpha of the ripple for the given [interaction]. Return `0f` if this
+     * particular interaction should not show a corresponding ripple / state layer.
+     */
+    public fun alphaForInteraction(interaction: Interaction): Float
+}
+
+/**
+ * Ambient used for providing [RippleTheme] down the tree.
+ *
+ * See [RippleTheme.defaultRippleColor] and [RippleTheme.defaultRippleAlpha] functions for the
+ * default implementations for color and alpha.
+ */
+@ExperimentalRippleApi
+public val AmbientRippleTheme: ProvidableAmbient<RippleTheme> = staticAmbientOf { DebugRippleTheme }
+
+@Suppress("unused")
+@OptIn(ExperimentalRippleApi::class)
+private sealed class DefaultRippleAlpha(
+    val pressed: Float,
+    val focused: Float,
+    val dragged: Float,
+    val hovered: Float
+) : RippleAlpha {
+    override fun alphaForInteraction(interaction: Interaction): Float = when (interaction) {
+        Interaction.Pressed -> pressed
+        Interaction.Dragged -> dragged
+        else -> 0f
+    }
+}
+
+/**
+ * Alpha values for high luminance content in a light 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 LightThemeHighContrastRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.24f,
+    focused = 0.24f,
+    dragged = 0.16f,
+    hovered = 0.08f
+)
+
+/**
+ * Alpha levels for low luminance content in a light 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 LightThemeLowContrastRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.12f,
+    focused = 0.12f,
+    dragged = 0.08f,
+    hovered = 0.04f
+)
+
+/**
+ * Alpha levels for all content in a dark theme.
+ */
+private object DarkThemeRippleAlpha : DefaultRippleAlpha(
+    pressed = 0.10f,
+    focused = 0.12f,
+    dragged = 0.08f,
+    hovered = 0.04f
+)
+
+/**
+ * Simple debug indication that will assume black content color and light theme. You should
+ * instead provide your own theme with meaningful values - this exists as an alternative to
+ * crashing if no theme is provided.
+ */
+@ExperimentalRippleApi
+@Immutable
+private object DebugRippleTheme : RippleTheme {
+    @Composable
+    override fun defaultColor() = RippleTheme.defaultRippleColor(Color.Black, lightTheme = true)
+
+    @Composable
+    override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha(
+        Color.Black,
+        lightTheme = true
+    )
+}
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt b/compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
similarity index 100%
rename from compose/material/material/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
rename to compose/material/material-ripple/src/test/kotlin/androidx/compose/material/ripple/RippleAnimationTest.kt
diff --git a/compose/material/material/api/current.txt b/compose/material/material/api/current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/current.txt
+++ b/compose/material/material/api/current.txt
@@ -765,32 +765,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -765,32 +765,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/api/restricted_current.txt b/compose/material/material/api/restricted_current.txt
index 2596cb5..a589728 100644
--- a/compose/material/material/api/restricted_current.txt
+++ b/compose/material/material/api/restricted_current.txt
@@ -765,32 +765,3 @@
 
 }
 
-package androidx.compose.material.ripple {
-
-  public final class RippleAnimationKt {
-  }
-
-  @androidx.compose.runtime.Stable public final class RippleIndication implements androidx.compose.foundation.Indication {
-    method public androidx.compose.foundation.IndicationInstance createInstance();
-  }
-
-  public final class RippleIndicationKt {
-    method @Deprecated @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication RippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-    method @androidx.compose.runtime.Composable public static androidx.compose.material.ripple.RippleIndication rememberRippleIndication-DudYJDg(optional boolean bounded, optional androidx.compose.ui.unit.Dp? radius, optional long color);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleOpacity {
-    method public float opacityForInteraction(androidx.compose.foundation.Interaction interaction);
-  }
-
-  @androidx.compose.material.ExperimentalMaterialApi public interface RippleTheme {
-    method @androidx.compose.runtime.Composable public long defaultColor-0d7_KjU();
-    method @androidx.compose.runtime.Composable public androidx.compose.material.ripple.RippleOpacity rippleOpacity();
-  }
-
-  public final class RippleThemeKt {
-    method public static androidx.compose.runtime.ProvidableAmbient<androidx.compose.material.ripple.RippleTheme> getAmbientRippleTheme();
-  }
-
-}
-
diff --git a/compose/material/material/build.gradle b/compose/material/material/build.gradle
index 2053ed0..96b2108 100644
--- a/compose/material/material/build.gradle
+++ b/compose/material/material/build.gradle
@@ -17,7 +17,6 @@
 
 import androidx.build.AndroidXUiPlugin
 import androidx.build.LibraryGroups
-import androidx.build.LibraryVersions
 import androidx.build.Publish
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -42,6 +41,7 @@
         api project(":compose:animation:animation-core")
         api project(":compose:foundation:foundation")
         api project(":compose:material:material-icons-core")
+        api project(":compose:material:material-ripple")
         api project(":compose:runtime:runtime")
         api project(":compose:ui:ui")
         api project(":compose:ui:ui-text")
@@ -86,6 +86,7 @@
                 api project(":compose:animation:animation-core")
                 api project(":compose:foundation:foundation")
                 api project(":compose:material:material-icons-core")
+                api project(":compose:material:material-ripple")
                 api project(":compose:runtime:runtime")
                 api project(":compose:ui:ui")
                 api project(":compose:ui:ui-text")
diff --git a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
index 35c4da7..077d3a1 100644
--- a/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
+++ b/compose/material/material/integration-tests/material-studies/src/main/java/androidx/compose/material/studies/rally/TopAppBar.kt
@@ -32,7 +32,7 @@
 import androidx.compose.material.MaterialTheme
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
@@ -76,7 +76,7 @@
                 .selectable(
                     selected = selected,
                     onClick = onSelected,
-                    indication = rememberRippleIndication(bounded = false)
+                    indication = rememberRipple(bounded = false)
                 )
         ) {
             Icon(icon, tint = tabTintColor)
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
index 4cc00ec..05b8283 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/AppBarTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
@@ -58,7 +59,7 @@
                 TopAppBar(title = { Text("Title") })
             }
             .assertHeightIsEqualTo(appBarHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -169,7 +170,7 @@
                 BottomAppBar {}
             }
             .assertHeightIsEqualTo(appBarHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
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 d18dde2..c219aec 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
@@ -32,6 +32,7 @@
 import androidx.compose.ui.test.assertIsSelected
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.isSelectable
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -63,7 +64,7 @@
         rule.setMaterialContentForSizeAssertions {
             BottomNavigationSample()
         }
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
             .assertHeightIsEqualTo(height)
     }
 
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
index a938532..1d4aa77 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DividerUiTest.kt
@@ -17,6 +17,7 @@
 package androidx.compose.material
 
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -41,7 +42,7 @@
                 Divider()
             }
             .assertHeightIsEqualTo(defaultHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -51,7 +52,7 @@
             .setMaterialContentForSizeAssertions {
                 Divider(thickness = height)
             }
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
             .assertHeightIsEqualTo(height)
     }
 
@@ -65,6 +66,6 @@
                 Divider(startIndent = indent, thickness = height)
             }
             .assertHeightIsEqualTo(height)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 }
\ No newline at end of file
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
index b8766c6..c3c6fcd 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/DrawerTest.kt
@@ -27,6 +27,9 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.AmbientLayoutDirection
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.SemanticsActions
+import androidx.compose.ui.test.SemanticsMatcher
+import androidx.compose.ui.test.assert
 import androidx.compose.ui.test.assertLeftPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertTopPositionInRootIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
@@ -35,7 +38,9 @@
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onParent
 import androidx.compose.ui.test.performGesture
+import androidx.compose.ui.test.performSemanticsAction
 import androidx.compose.ui.test.swipeDown
 import androidx.compose.ui.test.swipeLeft
 import androidx.compose.ui.test.swipeRight
@@ -412,4 +417,82 @@
             assertThat(drawerState.value).isEqualTo(BottomDrawerValue.Closed)
         }
     }
+
+    @Test
+    @LargeTest
+    fun modalDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen() {
+        lateinit var drawerState: DrawerState
+        rule.setMaterialContent {
+            drawerState = rememberDrawerState(DrawerValue.Closed)
+            ModalDrawerLayout(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                },
+                bodyContent = emptyContent()
+            )
+        }
+
+        // Drawer should start in closed state and have no dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+
+        // When the drawer state is set to Opened
+        rule.runOnIdle {
+            drawerState.open()
+        }
+        // Then the drawer should be opened and have dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+
+        // When the drawer state is set to Closed using dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .performSemanticsAction(SemanticsActions.Dismiss)
+        // Then the drawer should be closed and have no dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+    }
+
+    @Test
+    @LargeTest
+    fun bottomDrawer_noDismissActionWhenClosed_hasDissmissActionWhenOpen() {
+        lateinit var drawerState: BottomDrawerState
+        rule.setMaterialContent {
+            drawerState = rememberBottomDrawerState(BottomDrawerValue.Closed)
+            BottomDrawerLayout(
+                drawerState = drawerState,
+                drawerContent = {
+                    Box(Modifier.fillMaxSize().testTag("drawer"))
+                },
+                bodyContent = emptyContent()
+            )
+        }
+
+        // Drawer should start in closed state and have no dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+
+        // When the drawer state is set to Opened
+        rule.runOnIdle {
+            drawerState.open()
+        }
+        // Then the drawer should be opened and have dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsActions.Dismiss))
+
+        // When the drawer state is set to Closed using dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .performSemanticsAction(SemanticsActions.Dismiss)
+        // Then the drawer should be closed and have no dismiss action
+        rule.onNodeWithTag("drawer", useUnmergedTree = true)
+            .onParent()
+            .assert(SemanticsMatcher.keyNotDefined(SemanticsActions.Dismiss))
+    }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
index fe7511d..809ca2f 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ListItemTest.kt
@@ -26,6 +26,7 @@
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.node.Ref
 import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.IntSize
@@ -57,7 +58,7 @@
                 ListItem(text = { Text("Primary text") })
             }
             .assertHeightIsEqualTo(expectedHeightNoIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -71,7 +72,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightSmallIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -85,7 +86,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightLargeIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -99,7 +100,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightNoIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -115,7 +116,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeightWithIcon)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -130,7 +131,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -145,7 +146,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -161,7 +162,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
@@ -177,7 +178,7 @@
                 )
             }
             .assertHeightIsEqualTo(expectedHeight)
-            .assertWidthFillsRoot()
+            .assertWidthIsEqualTo(rule.rootWidth())
     }
 
     @Test
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
similarity index 86%
rename from compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
rename to compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
index 6e5dc86..2bd7a2d 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/ripple/RippleIndicationTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialRippleThemeTest.kt
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package androidx.compose.material.ripple
+package androidx.compose.material
 
 import android.os.Build
+import androidx.compose.foundation.Indication
 import androidx.compose.foundation.Interaction
 import androidx.compose.foundation.InteractionState
 import androidx.compose.foundation.indication
@@ -26,12 +27,11 @@
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.GOLDEN_MATERIAL
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Surface
-import androidx.compose.material.darkColors
-import androidx.compose.material.lightColors
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleAlpha
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.getValue
@@ -61,11 +61,15 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 
+/**
+ * Test for the [RippleTheme] provided by [MaterialTheme], to verify colors and opacity in
+ * different configurations.
+ */
 @LargeTest
 @RunWith(AndroidJUnit4::class)
 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
-@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class)
-class RippleIndicationTest {
+@OptIn(ExperimentalMaterialApi::class, ExperimentalTesting::class, ExperimentalRippleApi::class)
+class MaterialRippleThemeTest {
 
     @get:Rule
     val rule = createComposeRule()
@@ -89,7 +93,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_light_highluminance_pressed",
+            "ripple_bounded_light_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
         )
     }
@@ -110,7 +114,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_light_highluminance_dragged",
+            "ripple_bounded_light_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
         )
     }
@@ -131,7 +135,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_light_lowluminance_pressed",
+            "ripple_bounded_light_lowluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
         )
     }
@@ -152,7 +156,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_light_lowluminance_dragged",
+            "ripple_bounded_light_lowluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -173,7 +177,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_dark_highluminance_pressed",
+            "ripple_bounded_dark_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
         )
     }
@@ -194,7 +198,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_dark_highluminance_dragged",
+            "ripple_bounded_dark_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -215,7 +219,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_bounded_dark_lowluminance_pressed",
+            "ripple_bounded_dark_lowluminance_pressed",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
         )
@@ -237,7 +241,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_bounded_dark_lowluminance_dragged",
+            "ripple_bounded_dark_lowluminance_dragged",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
         )
@@ -259,7 +263,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_light_highluminance_pressed",
+            "ripple_unbounded_light_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.24f)
         )
     }
@@ -280,7 +284,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_light_highluminance_dragged",
+            "ripple_unbounded_light_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.16f)
         )
     }
@@ -301,7 +305,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_light_lowluminance_pressed",
+            "ripple_unbounded_light_lowluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.12f)
         )
     }
@@ -322,7 +326,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_light_lowluminance_dragged",
+            "ripple_unbounded_light_lowluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -343,7 +347,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_dark_highluminance_pressed",
+            "ripple_unbounded_dark_highluminance_pressed",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.10f)
         )
     }
@@ -364,7 +368,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_dark_highluminance_dragged",
+            "ripple_unbounded_dark_highluminance_dragged",
             calculateResultingRippleColor(contentColor, rippleOpacity = 0.08f)
         )
     }
@@ -385,7 +389,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_unbounded_dark_lowluminance_pressed",
+            "ripple_unbounded_dark_lowluminance_pressed",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.10f)
         )
@@ -407,7 +411,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_unbounded_dark_lowluminance_dragged",
+            "ripple_unbounded_dark_lowluminance_dragged",
             // Low luminance content in dark theme should use a white ripple by default
             calculateResultingRippleColor(Color.White, rippleOpacity = 0.08f)
         )
@@ -427,17 +431,15 @@
             override fun defaultColor() = rippleColor
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
-            }
+            override fun rippleAlpha() = RippleAlpha { rippleAlpha }
         }
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = contentColor) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -449,7 +451,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Pressed,
-            "rippleindication_customtheme_pressed",
+            "ripple_customtheme_pressed",
             expectedColor
         )
     }
@@ -468,17 +470,15 @@
             override fun defaultColor() = rippleColor
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = rippleAlpha
-            }
+            override fun rippleAlpha() = RippleAlpha { rippleAlpha }
         }
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = contentColor) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -490,7 +490,7 @@
         assertRippleMatches(
             interactionState,
             Interaction.Dragged,
-            "rippleindication_customtheme_dragged",
+            "ripple_customtheme_dragged",
             expectedColor
         )
     }
@@ -504,9 +504,7 @@
             override fun defaultColor() = color
 
             @Composable
-            override fun rippleOpacity() = object : RippleOpacity {
-                override fun opacityForInteraction(interaction: Interaction) = alpha
-            }
+            override fun rippleAlpha() = RippleAlpha { alpha }
         }
 
         val initialColor = Color.Red
@@ -515,11 +513,11 @@
         var rippleTheme by mutableStateOf(createRippleTheme(initialColor, initialAlpha))
 
         rule.setContent {
-            Providers(AmbientRippleTheme provides rippleTheme) {
-                MaterialTheme {
+            MaterialTheme {
+                Providers(AmbientRippleTheme provides rippleTheme) {
                     Surface(contentColor = Color.Black) {
                         Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                            RippleBox(interactionState, rememberRippleIndication())
+                            RippleBox(interactionState, rememberRipple())
                         }
                     }
                 }
@@ -616,14 +614,14 @@
 }
 
 /**
- * Generic Button like component that allows injecting a [RippleIndication] and also includes
+ * Generic Button like component that allows injecting an [Indication] and also includes
  * padding around the rippled surface, so screenshots will include some dead space for clarity.
  *
  * @param interactionState the [InteractionState] that is used to drive the ripple state
- * @param rippleIndication [RippleIndication] placed inside the surface
+ * @param ripple ripple [Indication] placed inside the surface
  */
 @Composable
-private fun RippleBox(interactionState: InteractionState, rippleIndication: RippleIndication) {
+private fun RippleBox(interactionState: InteractionState, ripple: Indication) {
     Box(Modifier.semantics(mergeDescendants = true) {}.testTag(Tag)) {
         Surface(
             Modifier.padding(25.dp),
@@ -632,7 +630,7 @@
             Box(
                 Modifier.preferredWidth(80.dp).preferredHeight(50.dp).indication(
                     interactionState = interactionState,
-                    indication = rippleIndication
+                    indication = ripple
                 )
             )
         }
@@ -659,7 +657,7 @@
         MaterialTheme(colors) {
             Surface(contentColor = contentColor) {
                 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
-                    RippleBox(interactionState, rememberRippleIndication(bounded))
+                    RippleBox(interactionState, rememberRipple(bounded))
                 }
             }
         }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
index fcd18b4..d55c5dc 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTest.kt
@@ -22,20 +22,20 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.FirstBaseline
 import androidx.compose.ui.layout.LastBaseline
-import androidx.compose.ui.node.ExperimentalLayoutNodeApi
-import androidx.compose.ui.platform.AndroidOwner
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.assertHeightIsEqualTo
-import androidx.compose.ui.test.assertIsEqualTo
 import androidx.compose.ui.test.assertWidthIsEqualTo
 import androidx.compose.ui.test.getAlignmentLinePosition
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
 import androidx.compose.ui.test.junit4.ComposeTestRule
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.test.onRoot
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.compose.ui.unit.width
 
 fun ComposeTestRule.setMaterialContent(
     modifier: Modifier = Modifier,
@@ -61,39 +61,9 @@
 fun SemanticsNodeInteraction.assertIsSquareWithSize(expectedSize: Dp) =
     assertWidthIsEqualTo(expectedSize).assertHeightIsEqualTo(expectedSize)
 
-fun SemanticsNodeInteraction.assertWidthFillsRoot(): SemanticsNodeInteraction {
-    val node = fetchSemanticsNode("Failed to assertWidthFillsScreen")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-    val rootViewWidth = owner.view.width
+fun ComposeTestRule.rootWidth(): Dp = onRoot().getUnclippedBoundsInRoot().width
 
-    with(owner.density) {
-        node.boundsInRoot.width.toDp().assertIsEqualTo(rootViewWidth.toDp())
-    }
-    return this
-}
-
-fun ComposeTestRule.rootWidth(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen width")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.width.toDp()
-    }
-}
-
-fun ComposeTestRule.rootHeight(): Dp {
-    val nodeInteraction = onRoot()
-    val node = nodeInteraction.fetchSemanticsNode("Failed to get screen height")
-    @OptIn(ExperimentalLayoutNodeApi::class)
-    val owner = node.componentNode.owner as AndroidOwner
-
-    return with(owner.density) {
-        owner.view.height.toDp()
-    }
-}
+fun ComposeTestRule.rootHeight(): Dp = onRoot().getUnclippedBoundsInRoot().height
 
 /**
  * Constant to emulate very big but finite constraints
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
index f33a0a9..6cbca52 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MaterialTextSelectionColorsScreenshotTest.kt
@@ -21,8 +21,10 @@
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.selection.SelectionContainer
+import androidx.compose.ui.selection.TextSelectionColors
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.click
 import androidx.compose.ui.test.height
@@ -36,6 +38,7 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
+import com.google.common.truth.Truth
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -59,6 +62,29 @@
     val screenshotRule = AndroidXScreenshotTestRule(GOLDEN_MATERIAL)
 
     @Test
+    fun rememberTextSelectionColors() {
+        var lightTextSelectionColors: TextSelectionColors? = null
+        var darkTextSelectionColors: TextSelectionColors? = null
+        var lightPrimary: Color = Color.Unspecified
+        var darkPrimary: Color = Color.Unspecified
+        rule.setContent {
+            val lightColors = lightColors()
+            val darkColors = darkColors()
+            lightPrimary = lightColors.primary
+            darkPrimary = darkColors.primary
+            lightTextSelectionColors = rememberTextSelectionColors(lightColors)
+            darkTextSelectionColors = rememberTextSelectionColors(darkColors)
+        }
+
+        Truth.assertThat(lightTextSelectionColors!!.handleColor).isEqualTo(lightPrimary)
+        Truth.assertThat(darkTextSelectionColors!!.handleColor).isEqualTo(darkPrimary)
+        Truth.assertThat(lightTextSelectionColors!!.backgroundColor)
+            .isEqualTo(lightPrimary.copy(alpha = 0.325f))
+        Truth.assertThat(darkTextSelectionColors!!.backgroundColor)
+            .isEqualTo(darkPrimary.copy(alpha = 0.375f))
+    }
+
+    @Test
     fun text_lightThemeSelectionColors() {
         rule.setContent {
             TextTestContent(lightColors())
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
index 29fdbcd..64e7730 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/MenuTest.kt
@@ -217,6 +217,35 @@
     }
 
     @Test
+    fun menu_positioning_top() {
+        val screenWidth = 500
+        val screenHeight = 1000
+        val density = Density(1f)
+        val windowBounds = IntBounds(0, 0, screenWidth, screenHeight)
+        val anchorPosition = IntOffset(0, 0)
+        val anchorSize = IntSize(50, 20)
+        val popupSize = IntSize(150, 500)
+
+        // The min margin above and below the menu, relative to the screen.
+        val MenuVerticalMargin = 32.dp
+        val verticalMargin = with(density) { MenuVerticalMargin.toIntPx() }
+
+        val position = DropdownMenuPositionProvider(
+            Position(0.dp, 0.dp),
+            density
+        ).calculatePosition(
+            IntBounds(anchorPosition, anchorSize),
+            windowBounds,
+            LayoutDirection.Ltr,
+            popupSize
+        )
+
+        assertThat(position.y).isEqualTo(
+            verticalMargin
+        )
+    }
+
+    @Test
     fun menu_positioning_callback() {
         val screenWidth = 500
         val screenHeight = 1000
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
index 7f909009..e32b647 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/SliderTest.kt
@@ -159,6 +159,16 @@
     }
 
     @Test
+    fun slider_semantics_focusable() {
+        rule.setMaterialContent {
+            Slider(value = 0f, onValueChange = {}, modifier = Modifier.testTag(tag))
+        }
+
+        rule.onNodeWithTag(tag)
+            .assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.Focused))
+    }
+
+    @Test
     fun slider_drag() {
         val state = mutableStateOf(0f)
 
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
index 5cfd7af..30d12b7 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomNavigation.kt
@@ -30,7 +30,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredHeight
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.emptyContent
@@ -153,7 +153,7 @@
     // The color of the Ripple should always the selected color, as we want to show the color
     // before the item is considered selected, and hence before the new contentColor is
     // provided by BottomNavigationTransition.
-    val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+    val ripple = rememberRipple(bounded = false, color = selectedContentColor)
 
     // TODO This composable has magic behavior within a Row; reconsider this behavior later
     Box(
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 1ae6d72..6b8cfc3 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
@@ -35,7 +35,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.triStateToggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
@@ -141,7 +141,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(
+                indication = rememberRipple(
                     bounded = false,
                     radius = CheckboxRippleRadius
                 )
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 1162f52..5cb78fb 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
@@ -42,6 +42,8 @@
 import androidx.compose.ui.platform.AmbientAnimationClock
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.AmbientLayoutDirection
+import androidx.compose.ui.semantics.dismiss
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
@@ -386,7 +388,14 @@
                         maxWidth = constraints.maxWidth.toDp(),
                         maxHeight = constraints.maxHeight.toDp()
                     )
-                }.offset(x = { drawerState.offset.value }).padding(end = VerticalDrawerPadding),
+                }
+                    .semantics {
+                        if (drawerState.isOpen) {
+                            dismiss(action = { drawerState.close(); true })
+                        }
+                    }
+                    .offset(x = { drawerState.offset.value })
+                    .padding(end = VerticalDrawerPadding),
                 shape = drawerShape,
                 color = drawerBackgroundColor,
                 contentColor = drawerContentColor,
@@ -500,7 +509,13 @@
                         maxWidth = constraints.maxWidth.toDp(),
                         maxHeight = constraints.maxHeight.toDp()
                     )
-                }.offset(y = { drawerState.offset.value }),
+                }
+                    .semantics {
+                        if (drawerState.isOpen) {
+                            dismiss(action = { drawerState.close(); true })
+                        }
+                    }
+                    .offset(y = { drawerState.offset.value }),
                 shape = drawerShape,
                 color = drawerBackgroundColor,
                 contentColor = drawerContentColor,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
index c79d3b5..c2b3ad0 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/IconButton.kt
@@ -22,7 +22,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.preferredSize
 import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -68,7 +68,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+                indication = rememberRipple(bounded = false, radius = RippleRadius)
             )
             .then(IconButtonSizeModifier),
         contentAlignment = Alignment.Center
@@ -108,7 +108,7 @@
             onValueChange = onCheckedChange,
             enabled = enabled,
             interactionState = interactionState,
-            indication = rememberRippleIndication(bounded = false, radius = RippleRadius)
+            indication = rememberRipple(bounded = false, radius = RippleRadius)
         ).then(IconButtonSizeModifier),
         contentAlignment = Alignment.Center
     ) { content() }
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
index 5cb947f..4cd516e 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTextSelectionColors.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.compositeOver
 import androidx.compose.ui.graphics.luminance
+import androidx.compose.ui.graphics.useOrElse
 import androidx.compose.ui.selection.TextSelectionColors
 import androidx.compose.ui.util.annotation.VisibleForTesting
 import kotlin.math.max
@@ -39,9 +40,12 @@
     // Test with ContentAlpha.medium to ensure that the selection background is accessible in the
     // 'worst case' scenario. We explicitly don't test with ContentAlpha.disabled, as disabled
     // text shouldn't be selectable / is noted as disabled for accessibility purposes.
-    val textColorWithLowestAlpha = contentColorFor(backgroundColor).copy(
-        alpha = ContentAlpha.medium
-    )
+    val textColorWithLowestAlpha = colors.contentColorFor(backgroundColor)
+        .useOrElse {
+            AmbientContentColor.current
+        }.copy(
+            alpha = ContentAlpha.medium
+        )
     return remember(primaryColor, backgroundColor, textColorWithLowestAlpha) {
         TextSelectionColors(
             handleColor = colors.primary,
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
index e65309a..0a1c321 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/MaterialTheme.kt
@@ -18,9 +18,13 @@
 
 import androidx.compose.foundation.AmbientIndication
 import androidx.compose.foundation.Indication
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.AmbientRippleTheme
+import androidx.compose.material.ripple.ExperimentalRippleApi
+import androidx.compose.material.ripple.RippleTheme
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.ComposableContract
+import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.remember
 import androidx.compose.ui.selection.AmbientTextSelectionColors
@@ -50,6 +54,7 @@
  * @param typography A set of text styles to be used as this hierarchy's typography system
  * @param shapes A set of shapes to be used by the components in this hierarchy
  */
+@OptIn(ExperimentalRippleApi::class)
 @Composable
 fun MaterialTheme(
     colors: Colors = MaterialTheme.colors,
@@ -63,15 +68,16 @@
         colors.copy()
     }.apply { updateColorsFrom(colors) }
     val indicationFactory: @Composable () -> Indication = remember {
-        { rememberRippleIndication() }
+        @Composable { rememberRipple() }
     }
     val selectionColors = rememberTextSelectionColors(rememberedColors)
     Providers(
         AmbientColors provides rememberedColors,
         AmbientContentAlpha provides ContentAlpha.high,
         AmbientIndication provides indicationFactory,
-        AmbientTextSelectionColors provides selectionColors,
+        AmbientRippleTheme provides MaterialRippleTheme,
         AmbientShapes provides shapes,
+        AmbientTextSelectionColors provides selectionColors,
         AmbientTypography provides typography
     ) {
         ProvideTextStyle(value = typography.body1, content = content)
@@ -111,3 +117,19 @@
     val shapes: Shapes
         get() = AmbientShapes.current
 }
+
+@OptIn(ExperimentalRippleApi::class)
+@Immutable
+private object MaterialRippleTheme : RippleTheme {
+    @Composable
+    override fun defaultColor() = RippleTheme.defaultRippleColor(
+        contentColor = AmbientContentColor.current,
+        lightTheme = MaterialTheme.colors.isLight
+    )
+
+    @Composable
+    override fun rippleAlpha() = RippleTheme.defaultRippleAlpha(
+        contentColor = AmbientContentColor.current,
+        lightTheme = MaterialTheme.colors.isLight
+    )
+}
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
index 2a09612..f075e67 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Menu.kt
@@ -33,7 +33,7 @@
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.preferredSizeIn
 import androidx.compose.foundation.layout.preferredWidth
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Providers
@@ -179,7 +179,7 @@
                 enabled = enabled,
                 onClick = onClick,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(true)
+                indication = rememberRipple(true)
             )
             .fillMaxWidth()
             // Preferred min and max width used during the intrinsic measurement.
@@ -318,7 +318,7 @@
         } ?: toLeft
 
         // Compute vertical position.
-        val toBottom = parentGlobalBounds.bottom + contentOffsetY
+        val toBottom = maxOf(parentGlobalBounds.bottom + contentOffsetY, verticalMargin)
         val toTop = parentGlobalBounds.top - contentOffsetY - popupContentSize.height
         val toCenter = parentGlobalBounds.top - popupContentSize.height / 2
         val toDisplayBottom = windowGlobalBounds.height - popupContentSize.height - verticalMargin
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 8a8a6eb..6e9d2ed 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
@@ -32,7 +32,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.remember
@@ -89,7 +89,7 @@
                 onClick = onClick,
                 enabled = enabled,
                 interactionState = interactionState,
-                indication = rememberRippleIndication(
+                indication = rememberRipple(
                     bounded = false,
                     radius = RadioButtonRippleRadius
                 )
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 05b57ec..31d5c18 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
@@ -29,6 +29,7 @@
 import androidx.compose.foundation.animation.FlingConfig
 import androidx.compose.foundation.animation.defaultFlingConfig
 import androidx.compose.foundation.animation.fling
+import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.indication
 import androidx.compose.foundation.layout.Box
@@ -41,7 +42,7 @@
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.SliderConstants.InactiveTrackColorAlpha
 import androidx.compose.material.SliderConstants.TickColorAlpha
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
@@ -257,13 +258,15 @@
                 shape = CircleShape,
                 color = thumbColor,
                 elevation = elevation,
-                modifier = Modifier.indication(
-                    interactionState = interactionState,
-                    indication = rememberRippleIndication(
-                        bounded = false,
-                        radius = ThumbRippleRadius
+                modifier = Modifier
+                    .focusable(interactionState = interactionState)
+                    .indication(
+                        interactionState = interactionState,
+                        indication = rememberRipple(
+                            bounded = false,
+                            radius = ThumbRippleRadius
+                        )
                     )
-                )
             ) {
                 Spacer(Modifier.preferredSize(thumbSize, thumbSize))
             }
@@ -357,7 +360,7 @@
         1f -> 100
         else -> (fraction * 100).roundToInt().coerceIn(1, 99)
     }
-    return semantics {
+    return semantics(mergeDescendants = true) {
         accessibilityValue = Strings.TemplatePercent.format(percent)
         accessibilityValueRange = AccessibilityRangeInfo(coerced, valueRange, steps)
         setProgress(
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 4ad2c29..9881050 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
@@ -30,7 +30,7 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.toggleable
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Stable
@@ -171,7 +171,7 @@
             .offset(x = { thumbValue.value })
             .indication(
                 interactionState = interactionState,
-                indication = rememberRippleIndication(bounded = false, radius = ThumbRippleRadius)
+                indication = rememberRipple(bounded = false, radius = ThumbRippleRadius)
             )
             .size(ThumbDiameter),
         content = emptyContent()
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 63d8945..175501d 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
@@ -37,7 +37,7 @@
 import androidx.compose.foundation.layout.preferredWidth
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.foundation.selection.selectable
-import androidx.compose.material.ripple.rememberRippleIndication
+import androidx.compose.material.ripple.rememberRipple
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Providers
 import androidx.compose.runtime.emptyContent
@@ -143,7 +143,7 @@
     // The color of the Ripple should always the selected color, as we want to show the color
     // before the item is considered selected, and hence before the new contentColor is
     // provided by TabTransition.
-    val ripple = rememberRippleIndication(bounded = false, color = selectedContentColor)
+    val ripple = rememberRipple(bounded = false, color = selectedContentColor)
 
     TabTransition(selectedContentColor, unselectedContentColor, selected) {
         Column(
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
index 0cd8808..4486035 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/TabRow.kt
@@ -16,6 +16,9 @@
 
 package androidx.compose.material
 
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.horizontalScroll
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -305,7 +308,7 @@
                 // Scrolls to the tab with [tabPosition], trying to place it in the center of the
                 // screen or as close to the center as possible.
                 val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions)
-                scrollState.smoothScrollTo(calculatedOffset)
+                scrollState.smoothScrollTo(calculatedOffset, spec = ScrollableTabRowScrollSpec)
             }
         }
     }
@@ -334,3 +337,11 @@
 }
 
 private val ScrollableTabRowMinimumTabWidth = 90.dp
+
+/**
+ * [AnimationSpec] used when scrolling to a tab that is not fully visible.
+ */
+private val ScrollableTabRowScrollSpec: AnimationSpec<Float> = tween(
+    durationMillis = 250,
+    easing = FastOutSlowInEasing
+)
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
deleted file mode 100644
index 1dfa397..0000000
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ripple/RippleTheme.kt
+++ /dev/null
@@ -1,160 +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:OptIn(ExperimentalMaterialApi::class)
-
-package androidx.compose.material.ripple
-
-import androidx.compose.foundation.Interaction
-import androidx.compose.material.AmbientContentColor
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.staticAmbientOf
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.luminance
-
-/**
- * Defines the appearance and the behavior for [RippleIndication]s.
- *
- * You can define a new theme and apply it via [AmbientRippleTheme].
- */
-@ExperimentalMaterialApi
-interface RippleTheme {
-    /**
-     * @return the default [RippleIndication] color at the call site's position in the hierarchy.
-     * This color will be used when a color is not explicitly set in the [RippleIndication] itself.
-     */
-    @Composable
-    fun defaultColor(): Color
-
-    /**
-     * @return the [RippleOpacity] used to calculate the opacity for the ripple depending on the
-     * [Interaction] for a given component. This will be set as the alpha channel for
-     * [defaultColor] or the color explicitly provided to the [RippleIndication].
-     */
-    @Composable
-    fun rippleOpacity(): RippleOpacity
-}
-
-// TODO: can be a fun interface when we rebase to use Kotlin 1.4
-/**
- * RippleOpacity defines the opacity of the ripple / state layer for a given [Interaction].
- */
-@ExperimentalMaterialApi
-interface RippleOpacity {
-    /**
-     * @return the opacity of the ripple for the given [interaction]. Return `0f` if this
-     * particular interaction should not show a corresponding ripple / state layer.
-     */
-    fun opacityForInteraction(interaction: Interaction): Float
-}
-
-/**
- * Ambient used for providing [RippleTheme] down the tree.
- */
-@ExperimentalMaterialApi
-val AmbientRippleTheme = staticAmbientOf<RippleTheme> { DefaultRippleTheme }
-
-private object DefaultRippleTheme : RippleTheme {
-    @Composable
-    override fun defaultColor(): Color {
-        val contentColor = AmbientContentColor.current
-        val lightTheme = MaterialTheme.colors.isLight
-        val contentLuminance = contentColor.luminance()
-        // If we are on a colored surface (typically indicated by low luminance content), the
-        // ripple color should be white.
-        return if (!lightTheme && contentLuminance < 0.5) {
-            Color.White
-            // Otherwise use contentColor
-        } else {
-            contentColor
-        }
-    }
-
-    @Composable
-    override fun rippleOpacity(): RippleOpacity {
-        val lightTheme = MaterialTheme.colors.isLight
-        val contentLuminance = AmbientContentColor.current.luminance()
-        return when {
-            lightTheme -> {
-                if (contentLuminance > 0.5) {
-                    LightThemeHighContrastRippleOpacity
-                } else {
-                    LightThemeReducedContrastRippleOpacity
-                }
-            }
-            else -> {
-                DarkThemeRippleOpacity
-            }
-        }
-    }
-}
-
-@Suppress("unused")
-private sealed class DefaultRippleOpacity(
-    val pressed: Float,
-    val focused: Float,
-    val dragged: Float,
-    val hovered: Float
-) : RippleOpacity {
-    override fun opacityForInteraction(interaction: Interaction): Float = when (interaction) {
-        Interaction.Pressed -> pressed
-        Interaction.Dragged -> dragged
-        else -> 0f
-    }
-}
-
-/**
- * Opacity values for high luminance content in a light 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 LightThemeHighContrastRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.24f,
-    focused = 0.24f,
-    dragged = 0.16f,
-    hovered = 0.08f
-)
-
-/**
- * Alpha levels for low luminance content in a light 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 LightThemeReducedContrastRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.12f,
-    focused = 0.12f,
-    dragged = 0.08f,
-    hovered = 0.04f
-)
-
-/**
- * Alpha levels for all content in a dark theme.
- */
-private object DarkThemeRippleOpacity : DefaultRippleOpacity(
-    pressed = 0.10f,
-    focused = 0.12f,
-    dragged = 0.08f,
-    hovered = 0.04f
-)
diff --git a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
index 764c108..853e0ac 100644
--- a/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
+++ b/compose/material/material/src/test/kotlin/androidx/compose/material/TextSelectionBackgroundColorTest.kt
@@ -66,38 +66,50 @@
         }.toTypedArray()
     }
 
+    /**
+     * In light theme the default background color is white, with black text and a medium content
+     * alpha of 74%.
+     */
     @Test
-    fun assertContrast_blackTextOnWhiteBackground() {
+    fun assertContrast_lightTheme_backgroundBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.Black,
+            textAlpha = 0.74f,
             backgroundColor = Color.White
         )
     }
 
+    /**
+     * In dark theme the default background color is #121212, with white text and a medium content
+     * alpha of 60%.
+     */
     @Test
-    fun assertContrast_whiteTextOnBlackBackground() {
+    fun assertContrast_darkTheme_backgroundBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.White,
-            backgroundColor = Color.Black
+            textAlpha = 0.6f,
+            backgroundColor = Color(0xFF121212)
         )
     }
 
     @Test
-    fun assertContrast_blackTextOnPrimaryBackground() {
+    fun assertContrast_lightTheme_primaryBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.Black,
+            textAlpha = 0.74f,
             backgroundColor = color
         )
     }
 
     @Test
-    fun assertContrast_whiteTextOnPrimaryBackground() {
+    fun assertContrast_darkTheme_primaryBackground() {
         assertContrastRatio(
             selectionColor = color,
             textColor = Color.White,
+            textAlpha = 0.6f,
             backgroundColor = color
         )
     }
@@ -106,11 +118,13 @@
 private fun assertContrastRatio(
     selectionColor: Color,
     textColor: Color,
+    textAlpha: Float,
     backgroundColor: Color
 ) {
+    val textColorWithAlpha = textColor.copy(alpha = textAlpha)
     val selectionBackgroundColor = calculateSelectionBackgroundColor(
         selectionColor = selectionColor,
-        textColor = textColor,
+        textColor = textColorWithAlpha,
         backgroundColor = backgroundColor
     )
 
@@ -118,7 +132,7 @@
     // using the default alpha for consistency with the spec.
     val minimumCompositeBackground = selectionBackgroundColor.copy(alpha = MinimumSelectionAlpha)
         .compositeOver(backgroundColor)
-    val minimumCompositeTextColor = textColor.compositeOver(minimumCompositeBackground)
+    val minimumCompositeTextColor = textColorWithAlpha.compositeOver(minimumCompositeBackground)
     val minimumContrastRatio = calculateContrastRatio(
         foreground = minimumCompositeTextColor,
         background = minimumCompositeBackground
@@ -131,7 +145,7 @@
 
     // Otherwise, ensure that the value we choose is accessible
     val compositeBackground = selectionBackgroundColor.compositeOver(backgroundColor)
-    val compositeTextColor = textColor.compositeOver(compositeBackground)
+    val compositeTextColor = textColorWithAlpha.compositeOver(compositeBackground)
     val contrastRatio = calculateContrastRatio(
         foreground = compositeTextColor,
         background = compositeBackground
@@ -143,7 +157,7 @@
         // If we searched for a new alpha, this alpha should be the maximal alpha that meets
         // contrast requirements, so the contrast ratio should be just above 4.5f in order to
         // maximize alpha
-        assertThat(contrastRatio).isWithin(0.05f).of(RequiredContrastRatio)
+        assertThat(contrastRatio).isWithin(0.1f).of(RequiredContrastRatio)
     }
 }
 
diff --git a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
index 8cb5760..711bd90 100644
--- a/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
+++ b/compose/runtime/runtime-dispatch/src/desktopMain/kotlin/androidx/compose/runtime/dispatch/ActualDesktop.kt
@@ -18,6 +18,7 @@
 
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.yield
+import java.awt.DisplayMode
 import java.awt.GraphicsEnvironment
 
 // TODO implement local Recomposer in each Window, so each Window can have own MonotonicFrameClock.
@@ -32,14 +33,17 @@
             if (GraphicsEnvironment.isHeadless()) {
                 yield()
             } else {
-                val defaultRefreshRate = GraphicsEnvironment
-                    .getLocalGraphicsEnvironment()
-                    .defaultScreenDevice
-                    .displayMode
-                    .refreshRate
-                delay(1000L / defaultRefreshRate)
+                delay(1000L / getFramesPerSecond())
             }
             return onFrame(System.nanoTime())
         }
+
+        private fun getFramesPerSecond(): Int {
+            val refreshRate = GraphicsEnvironment
+                .getLocalGraphicsEnvironment()
+                .screenDevices.maxOfOrNull { it.displayMode.refreshRate }
+                ?: DisplayMode.REFRESH_RATE_UNKNOWN
+            return if (refreshRate != DisplayMode.REFRESH_RATE_UNKNOWN) refreshRate else 60
+        }
     }
 }
\ No newline at end of file
diff --git a/compose/runtime/runtime-rxjava3/samples/build.gradle b/compose/runtime/runtime-rxjava3/samples/build.gradle
index ac887e3..1711449 100644
--- a/compose/runtime/runtime-rxjava3/samples/build.gradle
+++ b/compose/runtime/runtime-rxjava3/samples/build.gradle
@@ -32,7 +32,7 @@
     kotlinPlugin project(":compose:compiler:compiler")
 
     implementation(KOTLIN_STDLIB)
-    implementation project(":annotation:annotation-sampled")
+    compileOnly project(":annotation:annotation-sampled")
     implementation project(":compose:foundation:foundation")
     implementation project(":compose:material:material")
     implementation project(":compose:runtime:runtime-rxjava3")
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 835bdc3..61b92204 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -84,7 +84,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,22 +98,16 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
     method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,17 +117,13 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
     property public final androidx.compose.runtime.SlotTable slotTable;
   }
@@ -273,19 +263,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   public final class ProduceStateKt {
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,12 +336,7 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
   @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -545,16 +517,11 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
diff --git a/compose/runtime/runtime/api/public_plus_experimental_current.txt b/compose/runtime/runtime/api/public_plus_experimental_current.txt
index 835bdc3..61b92204 100644
--- a/compose/runtime/runtime/api/public_plus_experimental_current.txt
+++ b/compose/runtime/runtime/api/public_plus_experimental_current.txt
@@ -84,7 +84,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,22 +98,16 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
     method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -123,17 +117,13 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
     property public final androidx.compose.runtime.SlotTable slotTable;
   }
@@ -273,19 +263,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   public final class ProduceStateKt {
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
     method @androidx.compose.runtime.Composable public static <T> androidx.compose.runtime.State<T> produceState(T? initialValue, Object? subject, @kotlin.BuilderInference kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer);
@@ -359,12 +336,7 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
   @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -545,16 +517,11 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 3d9fadf..a649fe2 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -84,7 +84,7 @@
   }
 
   public final class Composer<N> {
-    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @androidx.compose.runtime.ComposeCompilerApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
+    ctor public Composer(androidx.compose.runtime.SlotTable slotTable, @kotlin.PublishedApi androidx.compose.runtime.Applier<N> applier, androidx.compose.runtime.CompositionReference parentReference);
     method @androidx.compose.runtime.InternalComposeApi public void applyChanges();
     method @androidx.compose.runtime.ComposeCompilerApi public inline <T> T! cache(boolean invalid, kotlin.jvm.functions.Function0<? extends T> block);
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(Object? value);
@@ -98,23 +98,20 @@
     method @androidx.compose.runtime.ComposeCompilerApi public boolean changed(int value);
     method @androidx.compose.runtime.InternalComposeApi public void collectKeySourceInformation();
     method @androidx.compose.runtime.InternalComposeApi public void composeInitial(kotlin.jvm.functions.Function0<kotlin.Unit> block);
-    method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
-    method @androidx.compose.runtime.ComposeCompilerApi public <T extends N> void createNode(kotlin.jvm.functions.Function0<? extends T> factory);
-    method @androidx.compose.runtime.ComposeCompilerApi public void emitNode(Object? node);
+    method @kotlin.PublishedApi internal <T> T! consume(androidx.compose.runtime.Ambient<T> key);
+    method @kotlin.PublishedApi internal void emitNode(Object? node);
     method @androidx.compose.runtime.ComposeCompilerApi public void endDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void endMovableGroup();
-    method @androidx.compose.runtime.ComposeCompilerApi public void endNode();
+    method @kotlin.PublishedApi internal void endNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void endReplaceableGroup();
     method @androidx.compose.runtime.ComposeCompilerApi public androidx.compose.runtime.ScopeUpdateScope? endRestartGroup();
-    method public androidx.compose.runtime.Applier<N> getApplier();
     method @org.jetbrains.annotations.TestOnly public kotlin.coroutines.CoroutineContext getApplyCoroutineContext();
     method public int getCurrentCompoundKeyHash();
     method public boolean getDefaultsInvalid();
-    method public boolean getInserting();
     method public boolean getSkipping();
     method public androidx.compose.runtime.SlotTable getSlotTable();
     method @androidx.compose.runtime.ComposeCompilerApi public Object joinKey(Object? left, Object? right);
-    method @androidx.compose.runtime.ComposeCompilerApi public Object? nextSlot();
+    method @kotlin.PublishedApi internal Object? nextSlot();
     method @androidx.compose.runtime.InternalComposeApi public boolean recompose();
     method @androidx.compose.runtime.InternalComposeApi public void recordModificationsOf(java.util.Set<?> values);
     method @androidx.compose.runtime.InternalComposeApi public void recordReadOf(Object value);
@@ -124,20 +121,19 @@
     method @androidx.compose.runtime.ComposeCompilerApi public void startDefaults();
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey);
     method @androidx.compose.runtime.ComposeCompilerApi public void startMovableGroup(int key, Object? dataKey, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi public void startNode();
+    method @kotlin.PublishedApi internal void startNode();
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startReplaceableGroup(int key, String? sourceInformation);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key);
     method @androidx.compose.runtime.ComposeCompilerApi public void startRestartGroup(int key, String? sourceInformation);
-    method @androidx.compose.runtime.ComposeCompilerApi @kotlin.PublishedApi internal void updateValue(Object? value);
-    method @androidx.compose.runtime.ComposeCompilerApi public N! useNode();
-    property public final androidx.compose.runtime.Applier<N> applier;
+    method @kotlin.PublishedApi internal void updateValue(Object? value);
+    method @kotlin.PublishedApi internal N! useNode();
     property @org.jetbrains.annotations.TestOnly public final kotlin.coroutines.CoroutineContext applyCoroutineContext;
     property public final int currentCompoundKeyHash;
     property public final boolean defaultsInvalid;
-    property public final boolean inserting;
     property public final boolean skipping;
     property public final androidx.compose.runtime.SlotTable slotTable;
+    field @kotlin.PublishedApi internal boolean inserting;
   }
 
   public final class ComposerKt {
@@ -294,19 +290,6 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface NoLiveLiterals {
   }
 
-  public final class ObserverMap<K, V> {
-    ctor public ObserverMap();
-    method public void add(K key, V value);
-    method public void clear();
-    method public void clearValues(kotlin.jvm.functions.Function1<? super V,java.lang.Boolean> predicate);
-    method public boolean contains(K key, V value);
-    method public operator java.util.List<V> get(Iterable<? extends K> keys);
-    method public java.util.List<V> getValueOf(K key);
-    method public void remove(K key);
-    method public void remove(K key, V value);
-    method public void removeValue(V value);
-  }
-
   @kotlin.PublishedApi internal final class PreCommitScopeImpl implements androidx.compose.runtime.CommitScope androidx.compose.runtime.CompositionLifecycleObserver {
     ctor public PreCommitScopeImpl(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.CommitScope,kotlin.Unit> onCommit);
     method public void onDispose(kotlin.jvm.functions.Function0<kotlin.Unit> callback);
@@ -385,12 +368,8 @@
   }
 
   public final class SkippableUpdater<T> {
-    ctor public SkippableUpdater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
+    ctor @kotlin.PublishedApi internal SkippableUpdater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
     method public inline void update(kotlin.jvm.functions.Function1<? super androidx.compose.runtime.Updater<T>,kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
   @androidx.compose.runtime.InternalComposeApi public final class SlotReader {
@@ -573,16 +552,12 @@
   }
 
   public final class Updater<T> {
-    ctor public Updater(androidx.compose.runtime.Composer<?> composer, T? node);
-    method public androidx.compose.runtime.Composer<?> getComposer();
-    method public T! getNode();
+    ctor @kotlin.PublishedApi internal Updater(@kotlin.PublishedApi androidx.compose.runtime.Composer<?> composer, @kotlin.PublishedApi T? node);
     method public inline void reconcile(kotlin.jvm.functions.Function1<? super T,kotlin.Unit> block);
     method public inline void set(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void set(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
     method public inline void update(int value, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> block);
     method public inline <reified V> void update(V? value, kotlin.jvm.functions.Function2<? super T,? super V,? extends kotlin.Unit> block);
-    property public final androidx.compose.runtime.Composer<?> composer;
-    property public final T! node;
   }
 
 }
diff --git a/compose/runtime/runtime/build.gradle b/compose/runtime/runtime/build.gradle
index 57fdeed..5a834fb 100644
--- a/compose/runtime/runtime/build.gradle
+++ b/compose/runtime/runtime/build.gradle
@@ -46,7 +46,6 @@
         implementation "androidx.annotation:annotation:1.1.0"
         implementation "org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm:0.3"
         implementation("androidx.core:core-ktx:1.1.0")
-        implementation(KOTLIN_REFLECT)
         implementation(KOTLIN_STDLIB)
 
         testImplementation KOTLIN_TEST_JUNIT
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
index 563fabe0..3cc12dd 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt
@@ -28,5 +28,5 @@
      * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and
      * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler.
      */
-    const val version: Int = 1900
+    const val version: Int = 2000
 }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index b3ea2bc..206e8f4 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -61,7 +61,6 @@
  * changed. It is used to determine how to update the nodes and the slot table when changes to the
  * structure of the tree is detected.
  */
-@OptIn(InternalComposeApi::class)
 private class Pending(
     val keyInfos: MutableList<KeyInfo>,
     val startIndex: Int
@@ -351,8 +350,7 @@
     /**
      * An adapter that applies changes to the tree using the Applier abstraction.
      */
-    @ComposeCompilerApi
-    val applier: Applier<N>,
+    @PublishedApi internal val applier: Applier<N>,
 
     /**
      * Parent of this composition; a [Recomposer] for root-level compositions.
@@ -395,7 +393,6 @@
 
     private var reader: SlotReader = slotTable.openReader().also { it.close() }
 
-    @OptIn(InternalComposeApi::class)
     internal val insertTable = SlotTable()
 
     private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
@@ -538,7 +535,6 @@
      * Start the composition. This should be called, and only be called, as the first group in
      * the composition.
      */
-    @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
     private fun startRoot() {
         reader = slotTable.openReader()
         startGroup(rootKey)
@@ -560,8 +556,6 @@
      * End the composition. This should be called, and only be called, to end the first group in
      * the composition.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun endRoot() {
         endGroup()
         parentReference.doneComposing()
@@ -575,7 +569,6 @@
     /**
      * Discard a pending composition because an error was encountered during composition
      */
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun abortRoot() {
         cleanUpCompose()
         pendingStack.clear()
@@ -596,8 +589,8 @@
      * first composition this is always true. During recomposition this is true when new nodes
      * are being scheduled to be added to the tree.
      */
-    @ComposeCompilerApi
-    var inserting: Boolean = false
+    @PublishedApi
+    internal var inserting: Boolean = false
         private set
 
     /**
@@ -763,7 +756,6 @@
      * Apply the changes to the tree that were collected during the last composition.
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
     fun applyChanges() {
         trace("Compose:applyChanges") {
             invalidateStack.clear()
@@ -812,7 +804,6 @@
     }
 
     @ExperimentalComposeApi
-    @OptIn(ComposeCompilerApi::class, InternalComposeApi::class)
     internal fun dispose() {
         trace("Compose:Composer.dispose") {
             parentReference.unregisterComposer(this)
@@ -845,19 +836,15 @@
      *
      *  @param key The key for the group
      */
-    @ComposeCompilerApi
     internal fun startGroup(key: Int) = start(key, null, false, null)
 
-    @ComposeCompilerApi
     internal fun startGroup(key: Int, dataKey: Any?) = start(key, dataKey, false, null)
 
     /**
      * End the current group.
      */
-    @ComposeCompilerApi
     internal fun endGroup() = end(false)
 
-    @OptIn(InternalComposeApi::class)
     private fun skipGroup() {
         groupNodeCount += reader.skipGroup()
     }
@@ -869,8 +856,8 @@
      * current position if found, if no such node is found the composition switches into insert
      * mode and a the node is scheduled to be inserted at the current location.
      */
-    @ComposeCompilerApi
-    fun startNode() {
+    @PublishedApi
+    internal fun startNode() {
         start(nodeKey, null, true, null)
         nodeExpected = true
     }
@@ -880,9 +867,7 @@
      * call when the composer is inserting.
      */
     @Suppress("UNUSED")
-    @ComposeCompilerApi
-    @OptIn(ExperimentalComposeApi::class, InternalComposeApi::class)
-    fun <T : N> createNode(factory: () -> T) {
+    internal fun <T : N> createNode(factory: () -> T) {
         validateNodeExpected()
         check(inserting) { "createNode() can only be called when inserting" }
         val insertIndex = nodeIndexStack.peek()
@@ -900,9 +885,8 @@
      * Schedule the given node to be inserted. This is only valid to call when the composer is
      * inserting.
      */
-    @ComposeCompilerApi
-    @OptIn(ExperimentalComposeApi::class)
-    fun emitNode(node: Any?) {
+    @PublishedApi
+    internal fun emitNode(node: Any?) {
         validateNodeExpected()
         check(inserting) { "emitNode() called when not inserting" }
         val insertIndex = nodeIndexStack.peek()
@@ -921,8 +905,8 @@
      * valid to call when the composition is not inserting. This must be called at the same
      * location as [emitNode] or [createNode] as called even if the value is unused.
      */
-    @ComposeCompilerApi
-    fun useNode(): N {
+    @PublishedApi
+    internal fun useNode(): N {
         validateNodeExpected()
         check(!inserting) { "useNode() called while inserting" }
         val result = reader.node
@@ -933,15 +917,14 @@
     /**
      * Called to end the node group.
      */
-    @ComposeCompilerApi
-    fun endNode() = end(true)
+    @PublishedApi
+    internal fun endNode() = end(true)
 
     /**
      * Schedule a change to be applied to a node's property. This change will be applied to the
      * node that is the current node in the tree which was either created by [createNode],
      * emitted by [emitNode] or returned by [useNode].
      */
-    @OptIn(ExperimentalComposeApi::class)
     internal fun <V, T> apply(value: V, block: T.(V) -> Unit) {
         recordApplierOperation { applier, _, _ ->
             @Suppress("UNCHECKED_CAST")
@@ -954,16 +937,14 @@
      * use the key stored at the current location in the slot table to avoid allocating a new key.
      */
     @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     fun joinKey(left: Any?, right: Any?): Any =
         getKey(reader.groupObjectKey, left, right) ?: JoinedKey(left, right)
 
     /**
      * Return the next value in the slot table and advance the current location.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
-    fun nextSlot(): Any? = if (inserting) {
+    @PublishedApi
+    internal fun nextSlot(): Any? = if (inserting) {
         validateNodeNotExpected()
         EMPTY
     } else reader.next()
@@ -1099,9 +1080,7 @@
      *
      * @param value the value to schedule to be written to the slot table.
      */
-    @OptIn(InternalComposeApi::class)
     @PublishedApi
-    @ComposeCompilerApi
     internal fun updateValue(value: Any?) {
         if (inserting) {
             writer.update(value)
@@ -1137,8 +1116,6 @@
     /**
      * Return the current ambient scope which was provided by a parent group.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun currentAmbientScope(): AmbientMap {
         if (inserting && hasProvider) {
             var current = writer.parent
@@ -1173,8 +1150,6 @@
      * compose which might be inserting the sub-composition. In that case the current scope
      * is the correct scope.
      */
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun ambientScopeAt(location: Int): AmbientMap {
         if (isComposing) {
             // The sub-composer is being composed as part of a nested composition then use the
@@ -1204,7 +1179,6 @@
      * scope followed by the map used to augment the parent scope. Both are needed to detect
      * inserts, updates and deletes to the providers.
      */
-    @ComposeCompilerApi
     private fun updateProviderMapGroup(
         parentScope: AmbientMap,
         currentProviders: AmbientMap
@@ -1217,8 +1191,6 @@
         return providerScope
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     internal fun startProviders(values: Array<out ProvidedValue<*>>) {
         val parentScope = currentAmbientScope()
         startGroup(providerKey, provider)
@@ -1268,22 +1240,18 @@
         start(ambientMapKey, ambientMap, false, providers)
     }
 
-    @ComposeCompilerApi
     internal fun endProviders() {
         endGroup()
         endGroup()
         providersInvalid = providersInvalidStack.pop().asBool()
     }
 
-    @ComposeCompilerApi
     @PublishedApi
     internal fun <T> consume(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
 
     /**
      * Create or use a memoized `CompositionReference` instance at this position in the slot table.
      */
-    @ComposeCompilerApi
-    @OptIn(ExperimentalComposeApi::class)
     internal fun buildReference(): CompositionReference {
         startGroup(referenceKey, reference)
 
@@ -1304,10 +1272,8 @@
     private fun <T> resolveAmbient(key: Ambient<T>, scope: AmbientMap): T =
         if (scope.contains(key)) scope.getValueOf(key) else parentReference.getAmbient(key)
 
-    @ComposeCompilerApi
     internal fun <T> parentAmbient(key: Ambient<T>): T = resolveAmbient(key, currentAmbientScope())
 
-    @ComposeCompilerApi
     private fun <T> parentAmbient(key: Ambient<T>, location: Int): T =
         resolveAmbient(key, ambientScopeAt(location))
 
@@ -1324,7 +1290,6 @@
             if (childrenComposing == 0 && it.isNotEmpty()) it.peek() else null
         }
 
-    @OptIn(InternalComposeApi::class)
     private fun ensureWriter() {
         if (writer.closed) {
             writer = insertTable.openWriter()
@@ -1337,7 +1302,6 @@
     /**
      * Start the reader group updating the data of the group if necessary
      */
-    @OptIn(InternalComposeApi::class)
     private fun startReaderGroup(isNode: Boolean, data: Any?) {
         if (isNode) {
             reader.startNode()
@@ -1351,8 +1315,6 @@
         }
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
         validateNodeNotExpected()
 
@@ -1478,7 +1440,6 @@
         groupNodeCount = 0
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun exitGroup(expectedNodeCount: Int, inserting: Boolean) {
         // Restore the parent's state updating them if they have changed based on changes in the
         // children. For example, if a group generates nodes then the number of generated nodes will
@@ -1493,8 +1454,6 @@
         this.groupNodeCount = this.groupNodeCountStack.pop() + expectedNodeCount
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun end(isNode: Boolean) {
         // All the changes to the group (or node) have been recorded. All new nodes have been
         // inserted but it has yet to determine which need to be removed or moved. Note that the
@@ -1649,7 +1608,6 @@
      * called instead of [skipReaderToGroupEnd] if any child groups are invalid. If no children
      * are invalid it will call [skipReaderToGroupEnd].
      */
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     private fun recomposeToGroupEnd() {
         val wasComposing = isComposing
         isComposing = true
@@ -1738,7 +1696,6 @@
      * updates that count and then updates any parent groups that include the nodes this group
      * emits.
      */
-    @OptIn(InternalComposeApi::class)
     private fun updateNodeCountOverrides(group: Int, newCount: Int) {
         // The value of group can be negative which indicates it is tracking an inserted group
         // instead of an existing group. The index is a virtual index calculated by
@@ -1778,7 +1735,6 @@
      * [recomposeIndex] allows the calculation to exit early if there is no node group between
      * [group] and [recomposeGroup].
      */
-    @OptIn(InternalComposeApi::class)
     private fun nodeIndexOf(
         groupLocation: Int,
         group: Int,
@@ -1814,7 +1770,6 @@
         return index
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun updatedNodeCount(group: Int): Int {
         if (group < 0) return nodeCountVirtualOverrides?.let { it[group] } ?: 0
         val nodeCounts = nodeCountOverrides
@@ -1825,7 +1780,6 @@
         return reader.nodeCount(group)
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun updateNodeCount(group: Int, count: Int) {
         if (updatedNodeCount(group) != count) {
             if (group < 0) {
@@ -1856,7 +1810,6 @@
      * Records the operations necessary to move the applier the node affected by the previous
      * group to the new group.
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordUpsAndDowns(oldGroup: Int, newGroup: Int, commonRoot: Int) {
         val reader = reader
         val nearestCommonRoot = reader.nearestCommonRootOf(
@@ -1876,7 +1829,6 @@
         doRecordDownsFor(newGroup, nearestCommonRoot)
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun doRecordDownsFor(group: Int, nearestCommonRoot: Int) {
         if (group > 0 && group != nearestCommonRoot) {
             doRecordDownsFor(reader.parent(group), nearestCommonRoot)
@@ -1889,7 +1841,6 @@
      * for [group]. Passing in the [recomposeGroup] and [recomposeKey] allows this method to exit
      * early.
      */
-    @OptIn(InternalComposeApi::class)
     private fun compoundKeyOf(group: Int, recomposeGroup: Int, recomposeKey: Int): Int {
         return if (group == recomposeGroup) recomposeKey else (
             compoundKeyOf(
@@ -1909,12 +1860,10 @@
      * back into a known good state after a period of time when snapshot changes were not
      * being observed.
      */
-    @OptIn(InternalComposeApi::class)
     internal fun invalidateAll() {
         slotTable.slots.forEach { (it as? RecomposeScope)?.invalidate() }
     }
 
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     internal fun invalidate(scope: RecomposeScope): InvalidationResult {
         if (scope.defaultsInScope) {
             scope.defaultsInvalid = true
@@ -1940,7 +1889,6 @@
      * composition is not inserting.
      */
     @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class, ExperimentalComposeApi::class)
     fun skipCurrentGroup() {
         if (invalidations.isEmpty()) {
             skipGroup()
@@ -1956,7 +1904,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun skipReaderToGroupEnd() {
         groupNodeCount = reader.parentNodes
         reader.skipToGroupEnd()
@@ -1994,8 +1941,6 @@
         addRecomposeScope()
     }
 
-    @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     private fun addRecomposeScope() {
         if (inserting) {
             val scope = RecomposeScope(this)
@@ -2016,7 +1961,6 @@
      * [endRestartGroup]).
      */
     @ComposeCompilerApi
-    @OptIn(InternalComposeApi::class)
     fun endRestartGroup(): ScopeUpdateScope? {
         // This allows for the invalidate stack to be out of sync since this might be called during exception stack
         // unwinding that might have not called the doneJoin/endRestartGroup in the wrong order.
@@ -2045,7 +1989,6 @@
      * which must be applied by [applyChanges] to build the tree implied by [block].
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
     fun composeInitial(block: @Composable () -> Unit) {
         trace("Compose:recompose") {
             var complete = false
@@ -2070,7 +2013,6 @@
      * applied by [applyChanges] to have an effect.
      */
     @InternalComposeApi
-    @OptIn(ComposeCompilerApi::class)
     fun recompose(): Boolean {
         if (invalidations.isNotEmpty()) {
             trace("Compose:recompose") {
@@ -2095,15 +2037,12 @@
 
     internal fun hasInvalidations() = invalidations.isNotEmpty()
 
-    @OptIn(InternalComposeApi::class)
     @Suppress("UNCHECKED_CAST")
     private var SlotWriter.node
         get() = node(currentGroup) as N
         set(value) { updateParentNode(value) }
-    @OptIn(InternalComposeApi::class)
     @Suppress("UNCHECKED_CAST")
     private val SlotReader.node get() = node(parent) as N
-    @OptIn(InternalComposeApi::class)
     @Suppress("UNCHECKED_CAST")
     private fun SlotReader.nodeAt(index: Int) = node(index) as N
 
@@ -2169,7 +2108,6 @@
     private var pendingUps = 0
     private var downNodes = Stack<N>()
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeUps() {
         val count = pendingUps
         if (count > 0) {
@@ -2178,7 +2116,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeDowns(nodes: Array<N>) {
         record { applier, _, _ ->
             for (index in nodes.indices) {
@@ -2214,7 +2151,6 @@
         pendingInsertUps++
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeInsertUps() {
         if (pendingInsertUps > 0) {
             val count = pendingInsertUps
@@ -2259,7 +2195,6 @@
      */
     private val startedGroups = IntStack()
 
-    @OptIn(InternalComposeApi::class)
     private fun realizeOperationLocation(forParent: Boolean = false) {
         val location = if (forParent) reader.parent else reader.currentGroup
         val distance = location - writersReaderDelta
@@ -2270,7 +2205,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordInsert(anchor: Anchor) {
         if (insertFixups.isEmpty()) {
             recordSlotEditingOperation { _, slots, _ ->
@@ -2294,7 +2228,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordFixup(change: Change<N>) {
         realizeInsertUps()
         val anchor = insertAnchor
@@ -2312,7 +2245,6 @@
      * writer and reader are tracking the same slot we advance the [writersReaderDelta] to
      * account for the removal.
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordDelete() {
         recordSlotEditingOperation(change = removeCurrentGroupInstance)
         writersReaderDelta += reader.groupSize
@@ -2321,7 +2253,6 @@
     /**
      * Called when reader current is moved directly, such as when a group moves, to [location].
      */
-    @OptIn(InternalComposeApi::class)
     private fun recordReaderMoving(location: Int) {
         val distance = reader.currentGroup - writersReaderDelta
 
@@ -2329,7 +2260,6 @@
         writersReaderDelta = location - distance
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordSlotEditing() {
         // During initial composition (when the slot table is empty), no group needs
         // to be started.
@@ -2350,7 +2280,6 @@
         }
     }
 
-    @OptIn(InternalComposeApi::class)
     private fun recordEndGroup() {
         val location = reader.parent
         val currentStartedGroup = startedGroups.peekOr(-1)
@@ -2376,7 +2305,6 @@
         cleanUpCompose()
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun cleanUpCompose() {
         pending = null
         nodeIndex = 0
@@ -2421,7 +2349,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun realizeMovement() {
         val count = previousCount
         previousCount = 0
@@ -2504,8 +2431,6 @@
             invalidate(scope)
         }
 
-        @ComposeCompilerApi
-        @OptIn(InternalComposeApi::class)
         override fun <T> getAmbient(key: Ambient<T>): T {
             val anchor = scope.anchor
             return if (anchor != null && anchor.valid) {
@@ -2517,8 +2442,6 @@
             }
         }
 
-        @ComposeCompilerApi
-        @OptIn(InternalComposeApi::class)
         override fun getAmbientScope(): AmbientMap {
             return ambientScopeAt(scope.anchor?.toIndexFor(slotTable) ?: 0)
         }
@@ -2540,7 +2463,6 @@
         }
     }
 
-    @OptIn(ExperimentalComposeApi::class)
     private fun updateCompoundKeyWhenWeEnterGroup(groupKey: Int, dataKey: Any?) {
         if (dataKey == null)
             updateCompoundKeyWhenWeEnterGroupKeyHash(groupKey)
@@ -2568,8 +2490,10 @@
 }
 
 @Suppress("UNCHECKED_CAST")
-/*inline */ class Updater<T>(val composer: Composer<*>, val node: T) {
-    @OptIn(ComposeCompilerApi::class)
+/*inline */ class Updater<T> @PublishedApi internal constructor(
+    @PublishedApi internal val composer: Composer<*>,
+    @PublishedApi internal val node: T
+) {
     inline fun set(
         value: Int,
         /*crossinline*/
@@ -2583,7 +2507,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun <reified V> set(
         value: V,
         /*crossinline*/
@@ -2597,7 +2520,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun update(
         value: Int,
         /*crossinline*/
@@ -2611,7 +2533,6 @@
         }
     }
 
-    @OptIn(ComposeCompilerApi::class)
     inline fun <reified V> update(
         value: V,
         /*crossinline*/
@@ -2632,8 +2553,10 @@
     }
 }
 
-class SkippableUpdater<T>(val composer: Composer<*>, val node: T) {
-    @OptIn(ComposeCompilerApi::class)
+class SkippableUpdater<T> @PublishedApi internal constructor(
+    @PublishedApi internal val composer: Composer<*>,
+    @PublishedApi internal val node: T
+) {
     inline fun update(block: Updater<T>.() -> Unit) {
         composer.startReplaceableGroup(0x1e65194f)
         Updater(composer, node).block()
@@ -2641,7 +2564,6 @@
     }
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotWriter.removeCurrentGroup(lifecycleManager: LifecycleManager) {
     // Notify the lifecycle manager of any observers leaving the slot table
     // The notification order should ensure that listeners are notified of leaving
@@ -2896,7 +2818,6 @@
     return realFn(composer, 1)
 }
 
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.distanceFrom(index: Int, root: Int): Int {
     var count = 0
     var current = index
@@ -2908,7 +2829,6 @@
 }
 
 // find the nearest common root
-@OptIn(InternalComposeApi::class)
 private fun SlotReader.nearestCommonRootOf(a: Int, b: Int, common: Int): Int {
     // Early outs, to avoid calling distanceFrom in trivial cases
     if (a == b) return a // A group is the nearest common root of itself
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
index d027d69..67ebb1b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Emit.kt
@@ -95,6 +95,32 @@
     currentComposer.endNode()
 }
 
+/**
+ * Emits a node into the composition of type [T]. Nodes emitted inside of [content] will become
+ * children of the emitted node.
+ *
+ * This function will throw a runtime exception if [E] is not a subtype of the applier of the
+ * [currentComposer].
+ *
+ * @sample androidx.compose.runtime.samples.CustomTreeComposition
+ *
+ * @param ctor A function which will create a new instance of [T]. This function is NOT
+ * guaranteed to be called in place.
+ * @param update A function to perform updates on the node. This will run every time emit is
+ * executed. This function is called in place and will be inlined.
+ * @param skippableUpdate A function to perform updates on the node. Unlike [update], this
+ * function is Composable and will therefore be skipped unless it has been invalidated by some
+ * other mechanism. This can be useful to perform expensive calculations for updating the node
+ * where the calculations are likely to have the same inputs over time, so the function's
+ * execution can be skipped.
+ * @param content the composable content that will emit the "children" of this node.
+ *
+ * @see Updater
+ * @see SkippableUpdater
+ * @see Applier
+ * @see emit
+ * @see compositionFor
+ */
 @Suppress("ComposableNaming")
 @OptIn(ComposeCompilerApi::class)
 @Composable @ComposableContract(readonly = true)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
index bfc4e99..31c52b0 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Expect.kt
@@ -32,19 +32,6 @@
 
 internal expect inline fun <R> synchronized(lock: Any, block: () -> R): R
 
-expect open class WeakReference<T> : Reference<T> {
-    constructor(referent: T)
-    constructor(referent: T, q: ReferenceQueue<in T>?)
-}
-
-expect abstract class Reference<T> {
-    open fun get(): T?
-}
-
-expect open class ReferenceQueue<T>() {
-    open fun poll(): Reference<out T>?
-}
-
 expect class AtomicReference<V>(value: V) {
     fun get(): V
     fun set(value: V)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
deleted file mode 100644
index 1616cfc..0000000
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ObserverMap.kt
+++ /dev/null
@@ -1,195 +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.
- */
-
-package androidx.compose.runtime
-
-/**
- * A map from a key to a set of values used for keeping the relation between some
- * entities and a models changes of which this entities are observing.
- *
- * Two main differences from a regular Map<K, Set<V>>:
- * 1) Object.hashCode is not used, so the values can be mutable and change their hashCode value
- * 2) Objects are stored with WeakReference to prevent leaking them.
-*/
-class ObserverMap<K : Any, V : Any> {
-    private val keyToValue =
-        hashMapOf<IdentityWeakReference<K>, MutableSet<IdentityWeakReference<V>>>()
-    private val valueToKey =
-        hashMapOf<IdentityWeakReference<V>, MutableSet<IdentityWeakReference<K>>>()
-    private val keyQueue = ReferenceQueue<K>()
-    private val valueQueue = ReferenceQueue<V>()
-
-    /**
-     * Adds a [value] into a set associated with this [key].
-     */
-    fun add(key: K, value: V) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key, keyQueue)
-        val weakValue = IdentityWeakReference(value, valueQueue)
-        addToSet(keyToValue, weakKey, weakValue)
-        addToSet(valueToKey, weakValue, weakKey)
-    }
-
-    /**
-     * Removes all the values associated with this [key].
-     *
-     * @return the list of values removed from the set as a result of this operation.
-     */
-    fun remove(key: K) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        removeFromSet(keyToValue, valueToKey, weakKey)
-    }
-
-    /**
-     * Removes exact [value] from the set associated with this [key].
-     */
-    fun remove(key: K, value: V) {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        val weakValue = IdentityWeakReference(value)
-        keyToValue[weakKey]?.remove(weakValue)
-        valueToKey[weakValue]?.remove(weakKey)
-    }
-
-    /**
-     * Returns `true` when the map contains the given key and value
-     */
-    fun contains(key: K, value: V): Boolean {
-        clearReferences()
-        val set = keyToValue[IdentityWeakReference(key)]
-        return set?.contains(IdentityWeakReference(value)) ?: false
-    }
-
-    /**
-     * Clears all the keys and values from the map.
-     */
-    fun clear() {
-        keyToValue.clear()
-        valueToKey.clear()
-        clearReferences()
-    }
-
-    /**
-     * @return a list of values associated with the provided [keys].
-     */
-    operator fun get(keys: Iterable<K>): List<V> {
-        clearReferences()
-        val set = mutableSetOf<IdentityWeakReference<V>>()
-        keys.forEach { key ->
-            val weakKey = IdentityWeakReference(key)
-            keyToValue[weakKey]?.let(set::addAll)
-        }
-        return set.mapNotNull { it.get() }
-    }
-
-    /**
-     * @return a list of values associated with the provided [key]
-     */
-    fun getValueOf(key: K): List<V> {
-        clearReferences()
-        val weakKey = IdentityWeakReference(key)
-        return keyToValue[weakKey]?.mapNotNull { it.get() }?.toList() ?: emptyList<V>()
-    }
-
-    /**
-     * Clears all the values that match the given [predicate] from all the sets.
-     */
-    @Suppress("UNCHECKED_CAST")
-    fun clearValues(predicate: (V) -> Boolean) {
-        clearReferences()
-        val matching = mutableListOf<V>()
-        valueToKey.keys.forEach { value ->
-            val v = value.get()
-            if (v != null && predicate(v)) {
-                matching.add(v)
-            }
-        }
-        matching.forEach { removeValue(it) }
-    }
-
-    /**
-     * Removes all values matching [value].
-     */
-    fun removeValue(value: V) {
-        clearReferences()
-        val weakValue = IdentityWeakReference(value)
-        valueToKey.remove(weakValue)?.forEach { key ->
-            val valueSet = keyToValue[key]!!
-            valueSet.remove(weakValue)
-            if (valueSet.isEmpty()) {
-                keyToValue.remove(key)
-            }
-        }
-    }
-
-    private fun clearReferences() {
-        pollQueue(keyQueue, keyToValue, valueToKey)
-        pollQueue(valueQueue, valueToKey, keyToValue)
-    }
-
-    private fun <T, U> pollQueue(
-        queue: ReferenceQueue<T>,
-        keyMap: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        valueMap: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>
-    ) {
-        do {
-            val ref = queue.poll()
-            if (ref != null) {
-                @Suppress("UNCHECKED_CAST")
-                val weakKey = ref as IdentityWeakReference<T>
-                removeFromSet(keyMap, valueMap, weakKey)
-            }
-        } while (ref != null)
-    }
-
-    private fun <T, U> addToSet(
-        map: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        key: IdentityWeakReference<T>,
-        value: IdentityWeakReference<U>
-    ) {
-        var set = map[key]
-        if (set == null) {
-            set = hashSetOf()
-            map.put(key, set)
-        }
-        set.add(value)
-    }
-
-    private fun <T, U> removeFromSet(
-        mapFromKey: MutableMap<IdentityWeakReference<T>, MutableSet<IdentityWeakReference<U>>>,
-        mapToKey: MutableMap<IdentityWeakReference<U>, MutableSet<IdentityWeakReference<T>>>,
-        key: IdentityWeakReference<T>
-    ) {
-        mapFromKey.remove(key)?.forEach { value ->
-            mapToKey[value]?.remove(key)
-        }
-    }
-}
-
-private class IdentityWeakReference<T>(value: T, queue: ReferenceQueue<T>? = null) :
-    WeakReference<T>(value, queue) {
-    val hash = identityHashCode(value)
-
-    override fun equals(other: Any?): Boolean {
-        if (other !is IdentityWeakReference<*>) {
-            return false
-        }
-        return hash == other.hash && get() === other.get()
-    }
-
-    override fun hashCode(): Int = hash
-}
\ No newline at end of file
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
index 044bbab..e760c75 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/ComposableLambda.kt
@@ -1177,7 +1177,7 @@
 private fun RecomposeScope?.replacableWith(other: RecomposeScope) =
     this == null || !this.valid || this == other || this.anchor == other.anchor
 
-@ComposeCompilerApi
+@OptIn(ComposeCompilerApi::class)
 private typealias CLambda = ComposableLambda<Any, Any, Any, Any, Any, Any, Any, Any, Any, Any,
     Any, Any, Any, Any, Any, Any, Any, Any, Any>
 
diff --git a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
index 927fa46..830f020 100644
--- a/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
+++ b/compose/runtime/runtime/src/jvmMain/kotlin/androidx/compose/runtime/ActualJvm.kt
@@ -45,10 +45,4 @@
     }
 }
 
-internal actual typealias Reference<T> = java.lang.ref.Reference<T>
-
-internal actual typealias ReferenceQueue<T> = java.lang.ref.ReferenceQueue<T>
-
-internal actual typealias WeakReference<T> = java.lang.ref.WeakReference<T>
-
 internal actual typealias TestOnly = org.jetbrains.annotations.TestOnly
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
deleted file mode 100644
index 8828ff6..0000000
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/ObserverMapTests.kt
+++ /dev/null
@@ -1,164 +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.
- */
-
-package androidx.compose.runtime
-
-import kotlin.test.BeforeTest
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class ObserverMapTests {
-
-    private val node1 = 1
-    private val node2 = 2
-    private lateinit var map: ObserverMap<TestModel, Int>
-
-    @BeforeTest
-    fun setup() {
-        map = ObserverMap()
-    }
-
-    @Test
-    fun testMapContainsPreviouslyAddedModel() {
-        val model = TestModel()
-        map.add(model, node1)
-
-        map.assertNodes(model, node1)
-    }
-
-    @Test
-    fun testMapAssociateBothNodesWithTheModel() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.assertNodes(model, node1, node2)
-    }
-
-    @Test
-    fun testMapContainsModelWithChangedHashCode() {
-        val model = TestModel("Original")
-        map.add(model, node1)
-        model.content = "Changed"
-
-        map.assertNodes(model, node1)
-    }
-
-    @Test
-    fun testMapRemovesTheModel() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.remove(model)
-
-        map.assertNodes(model)
-    }
-
-    @Test
-    fun testMapRemovesTheNode() {
-        val model = TestModel()
-        map.add(model, node1)
-        map.add(model, node2)
-
-        map.remove(model, node1)
-
-        map.assertNodes(model, node2)
-    }
-
-    @Test
-    fun testMapClearsAllTheModels() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node2)
-
-        map.clear()
-
-        map.assertNodes(model1)
-        map.assertNodes(model2)
-    }
-
-    @Test
-    fun testMapClearsTheValuesByPredicate() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        val node3 = 3
-        map.add(model1, node1)
-        map.add(model2, node2)
-        map.add(model2, node3)
-
-        map.clearValues { it == node1 || it == node3 }
-
-        map.assertNodes(model1)
-        map.assertNodes(model2, node2)
-    }
-
-    @Test
-    fun testGetForMultipleModels() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        val model3 = TestModel("Test2")
-        val node3 = 3
-        val node4 = 4
-        map.add(model1, node1)
-        map.add(model1, node2)
-        map.add(model2, node3)
-        map.add(model3, node4)
-
-        map.assertNodes(listOf(model1, model2, model3), node1, node2, node3, node4)
-    }
-
-    @Test
-    fun testGetFiltersDuplicates() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node1)
-
-        map.assertNodes(listOf(model1, model2), node1)
-    }
-
-    @Test
-    fun testRemoveValue() {
-        val model1 = TestModel("Test1")
-        val model2 = TestModel("Test2")
-        map.add(model1, node1)
-        map.add(model2, node1)
-        map.add(model2, node2)
-
-        map.removeValue(node1)
-        map.assertNodes(listOf(model2), node2)
-    }
-
-    private data class TestModel(var content: String = "Test")
-
-    private fun ObserverMap<TestModel, Int>.assertNodes(
-        model: TestModel,
-        vararg nodes: Int
-    ) {
-        assertNodes(listOf(model), *nodes)
-    }
-
-    private fun ObserverMap<TestModel, Int>.assertNodes(
-        models: List<TestModel>,
-        vararg nodes: Int
-    ) {
-        val expected = nodes.toList().sorted()
-        val actual = get(models).sorted()
-        assertEquals(expected, actual)
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-graphics/api/current.txt b/compose/ui/ui-graphics/api/current.txt
index 50d3a44..2b56325 100644
--- a/compose/ui/ui-graphics/api/current.txt
+++ b/compose/ui/ui-graphics/api/current.txt
@@ -885,6 +885,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
index 50d3a44..2b56325 100644
--- a/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-graphics/api/public_plus_experimental_current.txt
@@ -885,6 +885,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/api/restricted_current.txt b/compose/ui/ui-graphics/api/restricted_current.txt
index f7b2958..f6b1064 100644
--- a/compose/ui/ui-graphics/api/restricted_current.txt
+++ b/compose/ui/ui-graphics/api/restricted_current.txt
@@ -941,6 +941,10 @@
   public final class CanvasDrawScopeKt {
   }
 
+  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
+    method public void drawContent();
+  }
+
   public interface DrawContext {
     method public androidx.compose.ui.graphics.Canvas getCanvas();
     method public long getSize-NH-jbRc();
diff --git a/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
new file mode 100644
index 0000000..3ce1cd9
--- /dev/null
+++ b/compose/ui/ui-graphics/src/commonMain/kotlin/androidx/compose/ui/graphics/drawscope/ContentDrawScope.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.ui.graphics.drawscope
+
+/**
+ * Receiver scope for drawing content into a layout, where the content can
+ * be drawn between other canvas operations. If [drawContent] is not called,
+ * the contents of the layout will not be drawn.
+ */
+interface ContentDrawScope : DrawScope {
+    /**
+     * Causes child drawing operations to run during the `onPaint` lambda.
+     */
+    fun drawContent()
+}
diff --git a/compose/ui/ui-test-junit4/api/current.txt b/compose/ui/ui-test-junit4/api/current.txt
index cc099459..e6bda1c2 100644
--- a/compose/ui/ui-test-junit4/api/current.txt
+++ b/compose/ui/ui-test-junit4/api/current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
index cc099459..e6bda1c2 100644
--- a/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-test-junit4/api/public_plus_experimental_current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/api/restricted_current.txt b/compose/ui/ui-test-junit4/api/restricted_current.txt
index cc099459..e6bda1c2 100644
--- a/compose/ui/ui-test-junit4/api/restricted_current.txt
+++ b/compose/ui/ui-test-junit4/api/restricted_current.txt
@@ -2,7 +2,7 @@
 package androidx.compose.ui.test.junit4 {
 
   public final class AndroidAnimationClockTestRuleKt {
-    method @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static androidx.compose.ui.test.junit4.AnimationClockTestRule createAnimationClockRule();
   }
 
   public final class AndroidComposeTestRule<R extends org.junit.rules.TestRule, A extends androidx.activity.ComponentActivity> implements androidx.compose.ui.test.junit4.ComposeTestRule {
@@ -82,11 +82,14 @@
 
 package androidx.compose.ui.test.junit4.android {
 
+  public final class AndroidOwnerRegistryKt {
+  }
+
   public final class ComposeIdlingResourceKt {
-    method public static void registerComposeWithEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
-    method public static void unregisterComposeFromEspresso();
-    method @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void registerComposeWithEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void registerTestClock(androidx.compose.ui.test.TestAnimationClock clock);
+    method @Deprecated public static void unregisterComposeFromEspresso();
+    method @Deprecated @androidx.compose.ui.test.ExperimentalTesting public static void unregisterTestClock(androidx.compose.ui.test.TestAnimationClock clock);
   }
 
   public final class ComposeNotIdleException extends java.lang.Throwable {
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
index 4a4d79e..2c038c3 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/AndroidOwnerRegistryTest.kt
@@ -26,16 +26,21 @@
 import androidx.test.ext.junit.rules.ActivityScenarioRule
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
-import org.junit.After
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
 
 @LargeTest
 class AndroidOwnerRegistryTest {
 
+    private val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+    private val androidOwnerRegistry = AndroidOwnerRegistry()
+
     @get:Rule
-    val activityRule = ActivityScenarioRule(ComponentActivity::class.java)
+    val testRule: RuleChain = RuleChain
+        .outerRule { base, _ -> androidOwnerRegistry.getStatementFor(base) }
+        .around(activityRule)
 
     private val onRegistrationChangedListener =
         object : AndroidOwnerRegistry.OnRegistrationChangedListener {
@@ -47,24 +52,14 @@
 
     @Before
     fun setUp() {
-        assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-        AndroidOwnerRegistry.setupRegistry()
-        AndroidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
-    }
-
-    @After
-    fun tearDown() {
-        AndroidOwnerRegistry.removeOnRegistrationChangedListener(onRegistrationChangedListener)
-        AndroidOwnerRegistry.tearDownRegistry()
-        assertThat(AndroidOwnerRegistry.isSetUp).isFalse()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        androidOwnerRegistry.addOnRegistrationChangedListener(onRegistrationChangedListener)
     }
 
     @Test
     fun registryIsSetUpAndEmpty() {
-        assertThat(AndroidOwnerRegistry.isSetUp).isTrue()
-        assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+        assertThat(androidOwnerRegistry.isSetUp).isTrue()
+        assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
     }
 
     @Test
@@ -75,8 +70,8 @@
             val owner = activity.findOwner()
 
             // Then it is registered
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
-            assertThat(AndroidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEqualTo(setOf(owner))
+            assertThat(androidOwnerRegistry.getOwners()).isEqualTo(setOf(owner))
             // And our listener was notified
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(Pair(owner, true))
@@ -95,8 +90,8 @@
             activity.setContentView(View(activity))
 
             // Then it is not registered now
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-            assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getOwners()).isEmpty()
             // But our listener was notified of addition and removal
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(
@@ -115,11 +110,11 @@
             val owner = activity.findOwner()
 
             // When we tear down the registry
-            AndroidOwnerRegistry.tearDownRegistry()
+            androidOwnerRegistry.tearDownRegistry()
 
             // Then the registry is empty
-            assertThat(AndroidOwnerRegistry.getUnfilteredOwners()).isEmpty()
-            assertThat(AndroidOwnerRegistry.getOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getUnfilteredOwners()).isEmpty()
+            assertThat(androidOwnerRegistry.getOwners()).isEmpty()
             // And our listener was notified of addition and removal
             assertThat(onRegistrationChangedListener.recordedChanges).isEqualTo(
                 listOf(
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
index 7bee9b6..0adcd1b 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/ComposeIdlingResourceTest.kt
@@ -16,21 +16,25 @@
 
 package androidx.compose.ui.test.junit4
 
-import android.os.Handler
 import android.os.Looper
+import androidx.activity.ComponentActivity
 import androidx.compose.animation.core.FloatPropKey
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.transitionDefinition
 import androidx.compose.animation.core.tween
 import androidx.compose.animation.transition
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.State
+import androidx.compose.runtime.dispatch.withFrameNanos
 import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
@@ -39,6 +43,12 @@
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.runBlocking
+import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 
@@ -51,14 +61,12 @@
         private val rectSize = Size(50.0f, 50.0f)
     }
 
-    private val handler = Handler(Looper.getMainLooper())
-
     private var animationRunning = false
     private val recordedAnimatedValues = mutableListOf<Float>()
-    private var hasRecomposed = false
 
     @get:Rule
-    val rule = createComposeRule()
+    val rule = createAndroidComposeRule<ComponentActivity>()
+    private val composeIdlingResource = rule.composeIdlingResource
 
     /**
      * High level test to only verify that [ComposeTestRule.runOnIdle] awaits animations.
@@ -110,82 +118,79 @@
      * key moments during the animation kick-off process.
      */
     @Test
+    @Ignore("b/173798666: Idleness not detected after Snapshot.sendApplyNotifications()")
     fun testAnimationIdle_detailed() {
-        var wasIdleAfterCommit = false
-        var wasIdleAfterRecompose = false
         var wasIdleBeforeKickOff = false
-        var wasIdleBeforeCommit = false
+        var wasIdleBeforeApplySnapshot = false
+        var wasIdleAfterApplySnapshot = false
 
         val animationState = mutableStateOf(AnimationStates.From)
-        rule.setContent { Ui(animationState) }
+        lateinit var scope: CoroutineScope
+        rule.setContent {
+            scope = rememberCoroutineScope()
+            Ui(animationState)
+        }
 
-        rule.runOnIdle {
-            // Record idleness after this frame is committed. The mutation we're about to make
-            // will trigger a commit of the frame, which is posted at the front of the handler's
-            // queue. By posting a message at the front of the queue here, it will be executed
-            // right after the frame commit.
-            handler.postAtFrontOfQueue {
-                wasIdleAfterCommit = ComposeIdlingResource.isIdle()
-            }
+        runBlocking(scope.coroutineContext) {
+            // Verify that we're on the main thread, which is important for isIdle() later
+            assertThat(Looper.myLooper()).isEqualTo(Looper.getMainLooper())
+        }
 
-            // Record idleness after the next recomposition. Since we can't get a signal from the
-            // recomposer, keep polling until we detect we have been recomposed.
-            hasRecomposed = false
-            handler.pollUntil({ hasRecomposed }) {
-                wasIdleAfterRecompose = ComposeIdlingResource.isIdle()
-            }
-
+        val wasIdleAfterRecompose = rule.runOnIdle {
             // Record idleness before kickoff of animation
-            wasIdleBeforeKickOff = ComposeIdlingResource.isIdle()
+            wasIdleBeforeKickOff = composeIdlingResource.isIdle()
 
             // Kick off the animation
             animationRunning = true
             animationState.value = AnimationStates.To
 
-            // Record idleness after kickoff of animation, but before the frame is committed
-            wasIdleBeforeCommit = ComposeIdlingResource.isIdle()
+            // Record idleness after kickoff of animation, but before the snapshot is applied
+            wasIdleBeforeApplySnapshot = composeIdlingResource.isIdle()
+
+            // Apply the snapshot
+            @OptIn(ExperimentalComposeApi::class)
+            Snapshot.sendApplyNotifications()
+
+            // Record idleness after this snapshot is applied
+            wasIdleAfterApplySnapshot = composeIdlingResource.isIdle()
+
+            // Record idleness after the first recomposition
+            @OptIn(ExperimentalCoroutinesApi::class)
+            scope.async(start = CoroutineStart.UNDISPATCHED) {
+                // Await a single recomposition
+                withFrameNanos {}
+                composeIdlingResource.isIdle()
+            }
+        }.let {
+            runBlocking {
+                it.await()
+            }
         }
 
-        // Verify that animation is kicked off
-        assertThat(animationRunning).isTrue()
         // Wait until it is finished
-        onIdle()
-        // Verify it was finished
-        assertThat(animationRunning).isFalse()
+        rule.runOnIdle {
+            // Verify it was finished
+            assertThat(animationRunning).isFalse()
 
-        // Before the animation is kicked off, it is still idle
-        assertThat(wasIdleBeforeKickOff).isTrue()
-        // After animation is kicked off, but before the frame is committed, it must be busy
-        assertThat(wasIdleBeforeCommit).isFalse()
-        // After the frame is committed, it must still be busy
-        assertThat(wasIdleAfterCommit).isFalse()
-        // After recomposition, it must still be busy
-        assertThat(wasIdleAfterRecompose).isFalse()
-    }
-
-    private fun Handler.pollUntil(condition: () -> Boolean, onDone: () -> Unit) {
-        object : Runnable {
-            override fun run() {
-                if (condition()) {
-                    onDone()
-                } else {
-                    this@pollUntil.post(this)
-                }
-            }
-        }.run()
+            // Before the animation is kicked off, it is still idle
+            assertThat(wasIdleBeforeKickOff).isTrue()
+            // After animation is kicked off, but before the frame is committed, it must be busy
+            assertThat(wasIdleBeforeApplySnapshot).isFalse()
+            // After the frame is committed, it must still be busy
+            assertThat(wasIdleAfterApplySnapshot).isFalse()
+            // After recomposition, it must still be busy
+            assertThat(wasIdleAfterRecompose).isFalse()
+        }
     }
 
     @Composable
     private fun Ui(animationState: State<AnimationStates>) {
-        hasRecomposed = true
         Box(modifier = Modifier.background(color = Color.Yellow).fillMaxSize()) {
-            hasRecomposed = true
             val state = transition(
                 definition = animationDefinition,
                 toState = animationState.value,
                 onStateChangeFinished = { animationRunning = false }
             )
-            hasRecomposed = true
             Canvas(modifier = Modifier.fillMaxSize()) {
                 recordedAnimatedValues.add(state[x])
                 drawRect(Color.Cyan, Offset(state[x], 0f), rectSize)
@@ -217,4 +222,4 @@
             x using snap()
         }
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
index 77901b8..2e199ea 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/SynchronizationMethodsTest.kt
@@ -16,26 +16,43 @@
 
 package androidx.compose.ui.test.junit4
 
+import androidx.activity.ComponentActivity
 import androidx.compose.testutils.expectError
 import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.LifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.doReturn
 import com.nhaarman.mockitokotlin2.mock
 import com.nhaarman.mockitokotlin2.whenever
+import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
 import org.junit.runner.RunWith
-import androidx.test.ext.junit.runners.AndroidJUnit4
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class SynchronizationMethodsTest {
 
-    val rule = createComposeRule()
+    // Note: don't add `@get:Rule` to avoid the Rule from being applied. Except for the
+    // AndroidOwnerRegistry, it doesn't need to be initialized in these tests.
+    private val rule = createAndroidComposeRule<ComponentActivity>()
+    private val androidOwnerRegistry = rule.composeIdlingResource.androidOwnerRegistry
+
+    @get:Rule
+    val registryRule: TestRule = RuleChain.outerRule { base, _ ->
+        androidOwnerRegistry.getStatementFor(base)
+    }
+
+    @Before
+    fun addMockResumedOwner() {
+        androidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
+    }
 
     @Test
     fun runOnUiThread() {
@@ -58,72 +75,50 @@
 
     @Test
     fun runOnIdle() {
-        withAndroidOwnerRegistry {
-            val result = rule.runOnIdle { "Hello" }
-            assertThat(result).isEqualTo("Hello")
-        }
+        val result = rule.runOnIdle { "Hello" }
+        assertThat(result).isEqualTo("Hello")
     }
 
     @Test
     fun runOnIdle_void() {
-        withAndroidOwnerRegistry {
-            var called = false
-            rule.runOnIdle { called = true }
-            assertThat(called).isTrue()
-        }
+        var called = false
+        rule.runOnIdle { called = true }
+        assertThat(called).isTrue()
     }
 
     @Test
     fun runOnIdle_nullable() {
-        withAndroidOwnerRegistry {
-            val result: String? = rule.runOnIdle { null }
-            assertThat(result).isEqualTo(null)
-        }
+        val result: String? = rule.runOnIdle { null }
+        assertThat(result).isEqualTo(null)
     }
 
     @Test
     fun runOnIdle_assert_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.onNodeWithTag("dummy").assertExists()
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.onNodeWithTag("dummy").assertExists()
             }
         }
     }
 
     @Test
     fun runOnIdle_waitForIdle_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.waitForIdle()
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.waitForIdle()
             }
         }
     }
 
     @Test
     fun runOnIdle_runOnIdle_fails() {
-        withAndroidOwnerRegistry {
-            rule.runOnIdle {
-                expectError<IllegalStateException> {
-                    rule.runOnIdle {}
-                }
+        rule.runOnIdle {
+            expectError<IllegalStateException> {
+                rule.runOnIdle {}
             }
         }
     }
 
-    private fun withAndroidOwnerRegistry(block: () -> Unit) {
-        try {
-            AndroidOwnerRegistry.setupRegistry()
-            AndroidOwnerRegistry.registerOwner(mockResumedAndroidOwner())
-            block()
-        } finally {
-            AndroidOwnerRegistry.tearDownRegistry()
-        }
-    }
-
     private fun mockResumedAndroidOwner(): AndroidOwner {
         val lifecycle = mock<Lifecycle>()
         doReturn(Lifecycle.State.RESUMED).whenever(lifecycle).currentState
diff --git a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
index f3ff936..a6fd566 100644
--- a/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
+++ b/compose/ui/ui-test-junit4/src/androidAndroidTest/kotlin/androidx/compose/ui/test/junit4/TestAnimationClockTest.kt
@@ -39,7 +39,6 @@
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.test.espresso.Espresso.onIdle
 import androidx.test.filters.LargeTest
 import com.google.common.truth.Truth.assertThat
@@ -67,6 +66,7 @@
     @get:Rule
     val rule = createAndroidComposeRule<ComponentActivity>()
     private val clockTestRule = rule.clockTestRule
+    private val composeIdlingResource = rule.composeIdlingResource
 
     /**
      * Tests if advancing the clock manually works when the clock is paused, and that idleness is
@@ -87,7 +87,7 @@
             animationState.value = AnimationStates.To
 
             // Changes need to trickle down the animation system, so compose should be non-idle
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdle()).isFalse()
         }
 
         // Await recomposition
@@ -101,7 +101,7 @@
         // Advance first half of the animation (.5 sec)
         rule.runOnIdle {
             clockTestRule.advanceClock(halfDuration)
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdle()).isFalse()
         }
 
         // Await next animation frame
@@ -115,7 +115,7 @@
         // Advance second half of the animation (.5 sec)
         rule.runOnIdle {
             clockTestRule.advanceClock(halfDuration)
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdle()).isFalse()
         }
 
         // Await next animation frame
@@ -150,7 +150,7 @@
             animationState.value = AnimationStates.To
 
             // Changes need to trickle down the animation system, so compose should be non-idle
-            assertThat(ComposeIdlingResource.isIdle()).isFalse()
+            assertThat(composeIdlingResource.isIdle()).isFalse()
         }
 
         // Perform a single recomposition by awaiting the same signal as the Recomposer
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
index 0f7da5e..c417e42 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidAnimationClockTestRule.kt
@@ -20,10 +20,8 @@
 import androidx.compose.animation.core.rootAnimationClockFactory
 import androidx.compose.ui.test.ExperimentalTesting
 import androidx.compose.ui.test.TestAnimationClock
-import androidx.test.espresso.IdlingResource
-import androidx.compose.ui.test.junit4.android.registerTestClock
-import androidx.compose.ui.test.junit4.android.unregisterTestClock
 import androidx.compose.ui.test.junit4.android.AndroidTestAnimationClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import org.junit.rules.TestRule
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
@@ -31,30 +29,15 @@
 /**
  * A [TestRule] to monitor and take over the animation clock in the composition. It substitutes
  * the ambient animation clock provided at the root of the composition tree with a
- * [TestAnimationClock] and registers it with [registerTestClock].
- *
- * Usually you don't need to create this rule by yourself, it is done for you in
- * [ComposeTestRule]. If you don't use [ComposeTestRule], use this rule in your test and make
- * sure it is run _before_ your activity is created.
- *
- * If your app provides a custom animation clock somewhere in your composition, make sure to have
- * it implement [TestAnimationClock] and register it with [registerTestClock]. Alternatively,
- * if you use Espresso you can create your own [IdlingResource] to let Espresso await your
- * animations. Otherwise, built in steps that make sure the UI is stable when performing actions
- * or assertions will fail to work.
+ * [TestAnimationClock].
  */
 @ExperimentalTesting
-internal class AndroidAnimationClockTestRule : AnimationClockTestRule {
+internal class AndroidAnimationClockTestRule(
+    private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
 
     /** Backing property for [clock] */
     private val _clock = AndroidTestAnimationClock()
-
-    /**
-     * The ambient animation clock that is provided at the root of the composition tree.
-     * Typically, apps will only use this clock. If your app provides another clock in the tree,
-     * make sure to let it implement [TestAnimationClock] and register it with
-     * [registerTestClock].
-     */
     override val clock: TestAnimationClock get() = _clock
 
     override fun apply(base: Statement, description: Description?): Statement {
@@ -65,7 +48,7 @@
     private inner class AnimationClockStatement(private val base: Statement) : Statement() {
         override fun evaluate() {
             val oldFactory = rootAnimationClockFactory
-            registerTestClock(clock)
+            composeIdlingResource.registerTestClock(clock)
             rootAnimationClockFactory = { clock }
             try {
                 base.evaluate()
@@ -74,13 +57,20 @@
                     _clock.dispose()
                 } finally {
                     rootAnimationClockFactory = oldFactory
-                    unregisterTestClock(clock)
+                    composeIdlingResource.unregisterTestClock(clock)
                 }
             }
         }
     }
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
+@Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
-    AndroidAnimationClockTestRule()
+    throw UnsupportedOperationException()
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
index 8f8fe80..7fcac78 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidComposeTestRule.kt
@@ -17,6 +17,7 @@
 package androidx.compose.ui.test.junit4
 
 import androidx.activity.ComponentActivity
+import androidx.annotation.VisibleForTesting
 import androidx.compose.foundation.text.blinkingCursorEnabled
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Recomposer
@@ -27,11 +28,7 @@
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.FirstDrawRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
-import androidx.compose.ui.test.junit4.android.registerComposeWithEspresso
-import androidx.compose.ui.test.junit4.android.unregisterComposeFromEspresso
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.input.textInputServiceFactory
 import androidx.compose.ui.unit.Density
@@ -141,18 +138,20 @@
         activityProvider: (R) -> A
     ) : this(activityRule, activityProvider, false)
 
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal val composeIdlingResource = ComposeIdlingResource()
+
     @ExperimentalTesting
     override val clockTestRule: AnimationClockTestRule =
         if (!driveClockByMonotonicFrameClock) {
-            AndroidAnimationClockTestRule()
+            AndroidAnimationClockTestRule(composeIdlingResource)
         } else {
-            MonotonicFrameClockTestRule()
+            MonotonicFrameClockTestRule(composeIdlingResource)
         }
 
     internal var disposeContentHook: (() -> Unit)? = null
 
-    private val idleAwaiter = IdleAwaiter()
-    private val testOwner = AndroidTestOwner(idleAwaiter)
+    private val testOwner = AndroidTestOwner(composeIdlingResource)
     private val testContext = createTestContext(testOwner)
 
     private var activity: A? = null
@@ -179,7 +178,8 @@
         @Suppress("NAME_SHADOWING")
         @OptIn(ExperimentalTesting::class)
         return RuleChain
-            .outerRule(clockTestRule)
+            .outerRule { base, _ -> composeIdlingResource.getStatementFor(base) }
+            .around(clockTestRule)
             .around { base, _ -> AndroidComposeStatement(base) }
             .around(activityRule)
             .apply(base, description)
@@ -216,12 +216,12 @@
     }
 
     override fun waitForIdle() {
-        idleAwaiter.waitForIdle()
+        composeIdlingResource.waitForIdle()
     }
 
     @ExperimentalTesting
     override suspend fun awaitIdle() {
-        idleAwaiter.awaitIdle()
+        composeIdlingResource.awaitIdle()
     }
 
     override fun <T> runOnUiThread(action: () -> T): T {
@@ -242,54 +242,38 @@
         override fun evaluate() {
             @Suppress("DEPRECATION_ERROR")
             val oldTextInputFactory = textInputServiceFactory
-            beforeEvaluate()
             try {
+                @Suppress("DEPRECATION_ERROR")
+                blinkingCursorEnabled = false
+                @Suppress("DEPRECATION_ERROR")
+                textInputServiceFactory = {
+                    TextInputServiceForTests(it)
+                }
                 base.evaluate()
             } finally {
-                afterEvaluate()
+                @Suppress("DEPRECATION_ERROR")
+                blinkingCursorEnabled = true
                 @Suppress("DEPRECATION_ERROR")
                 textInputServiceFactory = oldTextInputFactory
-            }
-        }
-
-        @OptIn(InternalTextApi::class)
-        private fun beforeEvaluate() {
-            @Suppress("DEPRECATION_ERROR")
-            blinkingCursorEnabled = false
-            AndroidOwnerRegistry.setupRegistry()
-            FirstDrawRegistry.setupRegistry()
-            registerComposeWithEspresso()
-            @Suppress("DEPRECATION_ERROR")
-            textInputServiceFactory = {
-                TextInputServiceForTests(it)
-            }
-        }
-
-        @OptIn(InternalTextApi::class)
-        private fun afterEvaluate() {
-            @Suppress("DEPRECATION_ERROR")
-            blinkingCursorEnabled = true
-            AndroidOwnerRegistry.tearDownRegistry()
-            FirstDrawRegistry.tearDownRegistry()
-            unregisterComposeFromEspresso()
-            // Dispose the content
-            if (disposeContentHook != null) {
-                runOnUiThread {
-                    // NOTE: currently, calling dispose after an exception that happened during
-                    // composition is not a safe call. Compose runtime should fix this, and then
-                    // this call will be okay. At the moment, however, calling this could
-                    // itself produce an exception which will then obscure the original
-                    // exception. To fix this, we will just wrap this call in a try/catch of
-                    // its own
-                    try {
-                        disposeContentHook!!()
-                    } catch (e: Exception) {
-                        // ignore
+                // Dispose the content
+                if (disposeContentHook != null) {
+                    runOnUiThread {
+                        // NOTE: currently, calling dispose after an exception that happened during
+                        // composition is not a safe call. Compose runtime should fix this, and then
+                        // this call will be okay. At the moment, however, calling this could
+                        // itself produce an exception which will then obscure the original
+                        // exception. To fix this, we will just wrap this call in a try/catch of
+                        // its own
+                        try {
+                            disposeContentHook!!()
+                        } catch (e: Exception) {
+                            // ignore
+                        }
+                        disposeContentHook = null
                     }
-                    disposeContentHook = null
                 }
+                activity = null
             }
-            activity = null
         }
     }
 
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
index 19e890a..404b507 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/AndroidTestOwner.kt
@@ -23,13 +23,14 @@
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.test.InternalTestingApi
 import androidx.compose.ui.test.TestOwner
-import androidx.compose.ui.test.junit4.android.AndroidOwnerRegistry
-import androidx.compose.ui.test.junit4.android.IdleAwaiter
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 
 @OptIn(InternalTestingApi::class)
-internal class AndroidTestOwner(private val idleAwaiter: IdleAwaiter) : TestOwner {
+internal class AndroidTestOwner(
+    private val composeIdlingResource: ComposeIdlingResource
+) : TestOwner {
 
     @SuppressLint("DocumentExceptions")
     override fun sendTextInputCommand(node: SemanticsNode, command: List<EditOperation>) {
@@ -65,16 +66,7 @@
     }
 
     override fun getOwners(): Set<Owner> {
-        // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
-        //  structure. In case of multiple AndroidOwners, add a fake root
-        idleAwaiter.waitForIdle()
-
-        return AndroidOwnerRegistry.getOwners().also {
-            // TODO(b/153632210): This check should be done by callers of collectOwners
-            check(it.isNotEmpty()) {
-                "No compose views found in the app. Is your Activity resumed?"
-            }
-        }
+        return composeIdlingResource.getOwners()
     }
 
     private fun AndroidOwner.getTextInputServiceOrDie(): TextInputServiceForTests {
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
index ac434d4..8aa2f09 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/MonotonicFrameClockTestRule.kt
@@ -22,14 +22,15 @@
 import androidx.compose.animation.core.rootAnimationClockFactory
 import androidx.compose.ui.test.ExperimentalTesting
 import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.registerTestClock
-import androidx.compose.ui.test.junit4.android.ComposeIdlingResource.unregisterTestClock
+import androidx.compose.ui.test.junit4.android.ComposeIdlingResource
 import kotlinx.coroutines.CoroutineScope
 import org.junit.runner.Description
 import org.junit.runners.model.Statement
 
 @ExperimentalTesting
-internal class MonotonicFrameClockTestRule : AnimationClockTestRule {
+internal class MonotonicFrameClockTestRule(
+    private val composeIdlingResource: ComposeIdlingResource
+) : AnimationClockTestRule {
 
     private lateinit var _clock: InternalClock
     override val clock: TestAnimationClock get() = _clock
@@ -41,7 +42,7 @@
     private fun getOrCreateClock(scope: CoroutineScope): TestAnimationClock {
         if (!this::_clock.isInitialized) {
             _clock = InternalClock(MonotonicFrameAnimationClock(scope))
-            registerTestClock(_clock)
+            composeIdlingResource.registerTestClock(_clock)
         }
         return _clock
     }
@@ -55,7 +56,7 @@
                 base.evaluate()
             } finally {
                 rootAnimationClockFactory = oldFactory
-                unregisterTestClock(clock)
+                composeIdlingResource.unregisterTestClock(clock)
             }
         }
     }
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
index ae51818..0138a73 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/AndroidOwnerRegistry.kt
@@ -17,16 +17,25 @@
 package androidx.compose.ui.test.junit4.android
 
 import android.view.View
+import androidx.annotation.VisibleForTesting
 import androidx.compose.ui.platform.AndroidOwner
+import androidx.compose.ui.test.ExperimentalTesting
 import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.suspendCancellableCoroutine
+import org.junit.runners.model.Statement
 import java.util.Collections
 import java.util.WeakHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.resume
+import kotlin.time.ExperimentalTime
 
 /**
  * Registry where all [AndroidOwner]s should be registered while they are attached to the window.
  * This registry is used by the testing library to query the owners's state.
  */
-internal object AndroidOwnerRegistry {
+internal class AndroidOwnerRegistry {
     private val owners = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
     private val registryListeners = mutableSetOf<OnRegistrationChangedListener>()
 
@@ -39,13 +48,14 @@
     /**
      * Sets up this registry to be notified of any [AndroidOwner] created
      */
-    internal fun setupRegistry() {
+    private fun setupRegistry() {
         AndroidOwner.onAndroidOwnerCreatedCallback = ::onAndroidOwnerCreated
     }
 
     /**
      * Cleans up the changes made by [setupRegistry]. Call this after your test has run.
      */
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
     internal fun tearDownRegistry() {
         AndroidOwner.onAndroidOwnerCreatedCallback = null
         synchronized(owners) {
@@ -124,6 +134,19 @@
         }
     }
 
+    fun getStatementFor(base: Statement): Statement {
+        return object : Statement() {
+            override fun evaluate() {
+                try {
+                    setupRegistry()
+                    base.evaluate()
+                } finally {
+                    tearDownRegistry()
+                }
+            }
+        }
+    }
+
     /**
      * Interface to be implemented by components that want to be notified when an [AndroidOwner]
      * registers or unregisters at this registry.
@@ -132,7 +155,7 @@
         fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean)
     }
 
-    private class OwnerAttachedListener(
+    private inner class OwnerAttachedListener(
         private val owner: AndroidOwner
     ) : View.OnAttachStateChangeListener {
 
@@ -147,4 +170,75 @@
             unregisterOwner(owner)
         }
     }
-}
\ No newline at end of file
+}
+
+private val AndroidOwnerRegistry.hasAndroidOwners: Boolean get() = getOwners().isNotEmpty()
+
+private fun AndroidOwnerRegistry.ensureAndroidOwnerRegistryIsSetUp() {
+    check(isSetUp) {
+        "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
+            "with composables"
+    }
+}
+
+internal fun AndroidOwnerRegistry.waitForAndroidOwners() {
+    ensureAndroidOwnerRegistryIsSetUp()
+
+    if (!hasAndroidOwners) {
+        val latch = CountDownLatch(1)
+        val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+            override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+                if (hasAndroidOwners) {
+                    latch.countDown()
+                }
+            }
+        }
+        try {
+            addOnRegistrationChangedListener(listener)
+            if (!hasAndroidOwners) {
+                latch.await(2, TimeUnit.SECONDS)
+            }
+        } finally {
+            removeOnRegistrationChangedListener(listener)
+        }
+    }
+}
+
+@ExperimentalTesting
+@OptIn(ExperimentalTime::class)
+internal suspend fun AndroidOwnerRegistry.awaitAndroidOwners() {
+    ensureAndroidOwnerRegistryIsSetUp()
+
+    if (!hasAndroidOwners) {
+        suspendCancellableCoroutine<Unit> { continuation ->
+            // Make sure we only resume once
+            val didResume = AtomicBoolean(false)
+            fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
+                if (didResume.compareAndSet(false, true)) {
+                    removeOnRegistrationChangedListener(listener)
+                    continuation.resume(Unit)
+                }
+            }
+
+            // Usually we resume if an AndroidOwner is registered while the listener is added
+            val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
+                override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
+                    if (hasAndroidOwners) {
+                        resume(this)
+                    }
+                }
+            }
+
+            addOnRegistrationChangedListener(listener)
+            continuation.invokeOnCancellation {
+                removeOnRegistrationChangedListener(listener)
+            }
+
+            // Sometimes the AndroidOwner was registered before we added
+            // the listener, in which case we missed our signal
+            if (hasAndroidOwners) {
+                resume(listener)
+            }
+        }
+    }
+}
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
index d792d7c..ee8879a 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/ComposeIdlingResource.kt
@@ -18,15 +18,24 @@
 
 import android.os.Handler
 import android.os.Looper
+import androidx.annotation.VisibleForTesting
 import androidx.compose.runtime.ExperimentalComposeApi
 import androidx.compose.runtime.Recomposer
 import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.ui.node.Owner
 import androidx.compose.ui.test.ExperimentalTesting
+import androidx.compose.ui.test.TestAnimationClock
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.junit4.isOnUiThread
+import androidx.compose.ui.test.junit4.runOnUiThread
+import androidx.test.espresso.AppNotIdleException
+import androidx.test.espresso.Espresso
 import androidx.test.espresso.IdlingRegistry
 import androidx.test.espresso.IdlingResource
-import androidx.compose.ui.test.TestAnimationClock
-import androidx.compose.ui.test.junit4.runOnUiThread
-import java.util.concurrent.atomic.AtomicBoolean
+import androidx.test.espresso.IdlingResourceTimeoutException
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.junit.runners.model.Statement
 import java.util.concurrent.atomic.AtomicInteger
 
 /**
@@ -46,48 +55,79 @@
  * [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
  * wait for Compose being idle you can use this function.
  */
-fun registerComposeWithEspresso() {
-    ComposeIdlingResource.registerSelfIntoEspresso()
-    FirstDrawIdlingResource.registerSelfIntoEspresso()
-}
+@Deprecated(
+    message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+        "createAndroidComposeRule() and registration will happen when needed",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun registerComposeWithEspresso(): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
 
 /**
  * Unregisters resource registered as part of [registerComposeWithEspresso].
  */
-fun unregisterComposeFromEspresso() {
-    ComposeIdlingResource.unregisterSelfFromEspresso()
-    FirstDrawIdlingResource.unregisterSelfFromEspresso()
-}
+@Deprecated(
+    message = "Global (un)registration of ComposeIdlingResource is no longer supported. Use " +
+        "createAndroidComposeRule() and registration will happen when needed",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("")
+)
+@Suppress("DocumentExceptions")
+fun unregisterComposeFromEspresso(): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of ComposeIdlingResource is no longer supported"
+)
 
 /**
  * Registers the given [clock] so Espresso can await the animations subscribed to that clock.
  */
+@Deprecated(
+    message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+        "member function ComposeIdlingResource.registerTestClock(clock) instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeIdlingResource.registerTestClock(clock)")
+)
 @ExperimentalTesting
-fun registerTestClock(clock: TestAnimationClock) {
-    ComposeIdlingResource.registerTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun registerTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+        "directly on an instance of ComposeIdlingResource instead"
+)
 
 /**
  * Unregisters the [clock] that was registered with [registerTestClock].
  */
+@Deprecated(
+    message = "Global (un)registration of TestAnimationClocks is no longer supported. Use the " +
+        "member function ComposeIdlingResource.unregisterTestClock(clock) instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeIdlingResource.unregisterTestClock(clock)")
+)
 @ExperimentalTesting
-fun unregisterTestClock(clock: TestAnimationClock) {
-    ComposeIdlingResource.unregisterTestClock(clock)
-}
+@Suppress("UNUSED_PARAMETER", "DocumentExceptions")
+fun unregisterTestClock(clock: TestAnimationClock): Unit = throw UnsupportedOperationException(
+    "Global (un)registration of TestAnimationClocks is no longer supported. Register clocks " +
+        "directly on an instance of ComposeIdlingResource instead"
+)
 
 /**
  * Provides an idle check to be registered into Espresso.
  *
  * This makes sure that Espresso is able to wait for any pending changes in Compose. This
  * resource is automatically registered when any compose testing APIs are used including
- * [createAndroidComposeRule]. If you for some reasons want to only use Espresso but still have it
- * wait for Compose being idle you can register this yourself via [registerSelfIntoEspresso].
+ * [createAndroidComposeRule].
  */
-internal object ComposeIdlingResource : BaseIdlingResource(), IdlingResourceWithDiagnostics {
+internal class ComposeIdlingResource : IdlingResource, IdlingResourceWithDiagnostics {
 
     override fun getName(): String = "ComposeIdlingResource"
 
     private var isIdleCheckScheduled = false
+    private var resourceCallback: IdlingResource.ResourceCallback? = null
+
+    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+    internal val androidOwnerRegistry = AndroidOwnerRegistry()
 
     @OptIn(ExperimentalTesting::class)
     private val clocks = mutableSetOf<TestAnimationClock>()
@@ -98,6 +138,9 @@
     private var hadNoSnapshotChanges = true
     private var hadNoRecomposerChanges = true
     private var lastCompositionAwaiters = 0
+    private var hadNoPendingMeasureLayout = true
+    // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//    private var hadNoPendingDraw = true
 
     private var compositionAwaiters = AtomicInteger(0)
 
@@ -112,13 +155,25 @@
             hadNoRecomposerChanges = !Recomposer.current().hasInvalidations()
             hadAnimationClocksIdle = areAllClocksIdle()
             lastCompositionAwaiters = compositionAwaiters.get()
+            val owners = androidOwnerRegistry.getUnfilteredOwners()
+            hadNoPendingMeasureLayout = !owners.any { it.hasPendingMeasureOrLayout }
+            // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//            hadNoPendingDraw = !owners.any {
+//                val hasContent = it.view.measuredWidth != 0 && it.view.measuredHeight != 0
+//                it.view.isDirty && (hasContent || it.view.isLayoutRequested)
+//            }
 
             check(lastCompositionAwaiters >= 0) {
                 "More CompositionAwaiters were removed then added ($lastCompositionAwaiters)"
             }
 
-            hadNoSnapshotChanges && hadNoRecomposerChanges && hadAnimationClocksIdle &&
-                lastCompositionAwaiters == 0
+            hadNoSnapshotChanges &&
+                hadNoRecomposerChanges &&
+                hadAnimationClocksIdle &&
+                lastCompositionAwaiters == 0 &&
+                // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+                hadNoPendingMeasureLayout /*&&
+                hadNoPendingDraw*/
         }
     }
 
@@ -151,6 +206,14 @@
         }
     }
 
+    private fun transitionToIdle() {
+        resourceCallback?.onTransitionToIdle()
+    }
+
+    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
+        resourceCallback = callback
+    }
+
     /**
      * Called by [CompositionAwaiter] to indicate that this [ComposeIdlingResource] should report
      * busy to Espresso while that [CompositionAwaiter] is checking idleness.
@@ -168,14 +231,14 @@
     }
 
     @OptIn(ExperimentalTesting::class)
-    internal fun registerTestClock(clock: TestAnimationClock) {
+    fun registerTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.add(clock)
         }
     }
 
     @OptIn(ExperimentalTesting::class)
-    internal fun unregisterTestClock(clock: TestAnimationClock) {
+    fun unregisterTestClock(clock: TestAnimationClock) {
         synchronized(clocks) {
             clocks.remove(clock)
         }
@@ -194,6 +257,9 @@
         val hadRunningAnimations = !hadAnimationClocksIdle
         val numCompositionAwaiters = lastCompositionAwaiters
         val wasAwaitingCompositions = numCompositionAwaiters > 0
+        val hadPendingMeasureLayout = !hadNoPendingMeasureLayout
+        // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//        val hadPendingDraw = !hadNoPendingDraw
 
         val wasIdle = !hadSnapshotChanges && !hadRecomposerChanges &&
             !hadRunningAnimations && !wasAwaitingCompositions
@@ -217,60 +283,159 @@
                 " infinite re-compositions happening in the tested code.\n"
             message += "- Debug: hadRecomposerChanges = $hadRecomposerChanges, "
             message += "hadSnapshotChanges = $hadSnapshotChanges, "
-            message += "numCompositionAwaiters = $numCompositionAwaiters"
+            message += "numCompositionAwaiters = $numCompositionAwaiters, "
+            message += "hadPendingMeasureLayout = $hadPendingMeasureLayout"
+            // TODO(b/174244530): Include hadNoPendingDraw when it is reliable
+//            message += ", hadPendingDraw = $hadPendingDraw"
         }
         return message
     }
-}
 
-private object FirstDrawIdlingResource : BaseIdlingResource() {
-    override fun getName(): String = "FirstDrawIdlingResource"
+    fun waitForIdle() {
+        check(!isOnUiThread()) {
+            "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
+                "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
+                "from the main thread. Did you nest such a function inside " +
+                "runOnIdle {}, runOnUiThread {} or setContent {}?"
+        }
 
-    override fun isIdleNow(): Boolean {
-        return FirstDrawRegistry.haveAllDrawn().also {
-            if (!it) {
-                FirstDrawRegistry.setOnDrawnCallback(::transitionToIdle)
+        // First wait until we have an AndroidOwner (in case an Activity is being started)
+        androidOwnerRegistry.waitForAndroidOwners()
+        // Then await composition(s)
+        runEspressoOnIdle()
+
+        // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
+        //  Activity that is about to be paused, in cases where a new Activity is being started.
+        //  That means that AndroidOwnerRegistry.getOwners() may still return an empty list
+        //  between now and when the new Activity has created its AndroidOwner, even though
+        //  waitForAndroidOwners() suggests that we are now guaranteed one.
+    }
+
+    @ExperimentalTesting
+    suspend fun awaitIdle() {
+        // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
+        //  runEspressoOnIdle() and replace it with a suspend fun that loops while the
+        //  snapshot or the recomposer has pending changes, clocks are busy or owners have
+        //  pending measures or layouts; and do the await on AndroidUiDispatcher.Main
+        // We use Espresso to wait for composition, measure, layout and draw,
+        // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
+        withContext(Dispatchers.IO) {
+            // First wait until we have an AndroidOwner (in case an Activity is being started)
+            androidOwnerRegistry.awaitAndroidOwners()
+            // Then await composition(s)
+            runEspressoOnIdle()
+        }
+    }
+
+    fun getOwners(): Set<Owner> {
+        // TODO(pavlis): Instead of returning a flatMap, let all consumers handle a tree
+        //  structure. In case of multiple AndroidOwners, add a fake root
+        waitForIdle()
+
+        return androidOwnerRegistry.getOwners().also {
+            // TODO(b/153632210): This check should be done by callers of collectOwners
+            check(it.isNotEmpty()) {
+                "No compose views found in the app. Is your Activity resumed?"
             }
         }
     }
 
-    override fun unregisterSelfFromEspresso() {
-        super.unregisterSelfFromEspresso()
-        FirstDrawRegistry.setOnDrawnCallback(null)
+    fun getStatementFor(base: Statement): Statement {
+        return androidOwnerRegistry.getStatementFor(
+            object : Statement() {
+                override fun evaluate() {
+                    try {
+                        IdlingRegistry.getInstance().register(this@ComposeIdlingResource)
+                        base.evaluate()
+                    } finally {
+                        IdlingRegistry.getInstance().unregister(this@ComposeIdlingResource)
+                    }
+                }
+            }
+        )
     }
 }
 
-internal sealed class BaseIdlingResource : IdlingResource {
-    private val isRegistered = AtomicBoolean(false)
-    private var resourceCallback: IdlingResource.ResourceCallback? = null
+// TODO(b/168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
+//  and blocking await Espresso.onIdle().
+internal fun ComposeIdlingResource.runEspressoOnIdle() {
+    val compositionAwaiter = CompositionAwaiter(this)
+    try {
+        compositionAwaiter.start()
+        Espresso.onIdle()
+    } catch (e: Throwable) {
+        compositionAwaiter.cancel()
 
-    final override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
-        resourceCallback = callback
-    }
-
-    protected fun transitionToIdle() {
-        resourceCallback?.onTransitionToIdle()
-    }
-
-    /**
-     * Registers this resource into Espresso.
-     *
-     * Can be called multiple times.
-     */
-    internal fun registerSelfIntoEspresso() {
-        if (isRegistered.compareAndSet(false, true)) {
-            IdlingRegistry.getInstance().register(this)
+        // Happens on the global time out, usually when global idling time out is less
+        // or equal to dynamic idling time out or when the timeout is not due to individual
+        // idling resource. This does not necessary mean that it can't be due to idling
+        // resource being busy. So we try to check if it failed due to compose being busy and
+        // add some extra information to the developer.
+        val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
+        if (appNotIdleMaybe != null) {
+            rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
         }
-    }
 
-    /**
-     * Unregisters this resource from Espresso.
-     *
-     * Can be called multiple times.
-     */
-    internal open fun unregisterSelfFromEspresso() {
-        if (isRegistered.compareAndSet(true, false)) {
-            IdlingRegistry.getInstance().unregister(this)
+        // Happens on idling resource taking too long. Espresso gives out which resources caused
+        // it but it won't allow us to give any extra information. So we check if it was our
+        // resource and give more info if we can.
+        val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
+        if (resourceNotIdleMaybe != null) {
+            rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
         }
+
+        // No match, rethrow
+        throw e
     }
 }
+
+private fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
+    var diagnosticInfo = ""
+    val listOfIdlingResources = mutableListOf<String>()
+    IdlingRegistry.getInstance().resources.forEach { resource ->
+        if (resource is IdlingResourceWithDiagnostics) {
+            val message = resource.getDiagnosticMessageIfBusy()
+            if (message != null) {
+                diagnosticInfo += "$message \n"
+            }
+        }
+        listOfIdlingResources.add(resource.name)
+    }
+    if (diagnosticInfo.isNotEmpty()) {
+        val prefix = if (wasGlobalTimeout) {
+            "Global time out"
+        } else {
+            "Idling resource timed out"
+        }
+        throw ComposeNotIdleException(
+            "$prefix: possibly due to compose being busy.\n" +
+                diagnosticInfo +
+                "All registered idling resources: " +
+                listOfIdlingResources.joinToString(", "),
+            e
+        )
+    }
+    // No extra info, re-throw the original exception
+    throw e
+}
+
+/**
+ * Tries to find if the given exception or any of its cause is of the type of the provided
+ * throwable T. Returns null if there is no match. This is required as some exceptions end up
+ * wrapped in Runtime or Concurrent exceptions.
+ */
+private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
+    var causeToCheck: Throwable? = e
+    while (causeToCheck != null) {
+        if (causeToCheck is T) {
+            return causeToCheck
+        }
+        causeToCheck = causeToCheck.cause
+    }
+    return null
+}
+
+/**
+ * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
+ */
+class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
index af9d0553a..dd380e9 100644
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
+++ b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/CompositionAwaiter.kt
@@ -24,7 +24,7 @@
 import androidx.compose.runtime.snapshots.Snapshot
 import androidx.compose.ui.test.junit4.runOnUiThread
 
-internal class CompositionAwaiter {
+internal class CompositionAwaiter(private val composeIdlingResource: ComposeIdlingResource) {
 
     private enum class State {
         Initialized, Running, Finished, Cancelled
@@ -62,11 +62,11 @@
     }
 
     private fun startIdlingResource() {
-        ComposeIdlingResource.addCompositionAwaiter()
+        composeIdlingResource.addCompositionAwaiter()
     }
 
     private fun stopIdlingResource() {
-        ComposeIdlingResource.removeCompositionAwaiter()
+        composeIdlingResource.removeCompositionAwaiter()
     }
 
     /**
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
deleted file mode 100644
index 4a81e55..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/FirstDrawRegistry.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test.junit4.android
-
-import android.view.ViewTreeObserver
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.lifecycle.Lifecycle
-import java.util.Collections
-import java.util.WeakHashMap
-
-internal object FirstDrawRegistry {
-    private val notYetDrawn = Collections.newSetFromMap(WeakHashMap<AndroidOwner, Boolean>())
-    private var onDrawnCallback: (() -> Unit)? = null
-
-    private val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-        override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-            if (registered) {
-                notYetDrawn.add(owner)
-                owner.view.viewTreeObserver.addOnDrawListener(FirstDrawListener(owner))
-            } else {
-                notYetDrawn.remove(owner)
-                dispatchOnDrawn()
-            }
-        }
-    }
-
-    /**
-     * Sets up this registry to listen to the [AndroidOwnerRegistry]. Call this method before
-     * [AndroidOwner]s are registered into that registry, which is before the test activity is
-     * created, or simply right after the [AndroidOwnerRegistry] is setup.
-     */
-    internal fun setupRegistry() {
-        AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-    }
-
-    /**
-     * Cleans up the changes made by [setupRegistry]. Call this after your test has run.
-     */
-    internal fun tearDownRegistry() {
-        AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-    }
-
-    /**
-     * Returns if all registered owners have finished at least one draw call.
-     */
-    fun haveAllDrawn(): Boolean {
-        return notYetDrawn.all {
-            val viewTreeOwners = it.viewTreeOwners
-            viewTreeOwners == null ||
-                viewTreeOwners.lifecycleOwner.lifecycle.currentState != Lifecycle.State.RESUMED
-        }
-    }
-
-    /**
-     * Adds a [callback] to be called when all registered [AndroidOwner]s have drawn at least
-     * once. The callback will be removed after it is called.
-     */
-    fun setOnDrawnCallback(callback: (() -> Unit)?) {
-        onDrawnCallback = callback
-    }
-
-    /**
-     * Should be called when a registered owner has drawn for the first time. Can be called after
-     * subsequent draws as well, but that is not required.
-     */
-    private fun notifyOwnerDrawn(owner: AndroidOwner) {
-        notYetDrawn.remove(owner)
-        dispatchOnDrawn()
-    }
-
-    private fun dispatchOnDrawn() {
-        if (haveAllDrawn()) {
-            onDrawnCallback?.invoke()
-            onDrawnCallback = null
-        }
-    }
-
-    private class FirstDrawListener(private val owner: AndroidOwner) :
-        ViewTreeObserver.OnDrawListener {
-        private var invoked = false
-
-        override fun onDraw() {
-            if (!invoked) {
-                invoked = true
-                owner.view.post {
-                    // The view was drawn
-                    notifyOwnerDrawn(owner)
-                    val viewTreeObserver = owner.view.viewTreeObserver
-                    if (viewTreeObserver.isAlive) {
-                        viewTreeObserver.removeOnDrawListener(this)
-                    }
-                }
-            }
-        }
-    }
-}
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt b/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
deleted file mode 100644
index 959aa19..0000000
--- a/compose/ui/ui-test-junit4/src/androidMain/kotlin/androidx/compose/ui/test/junit4/android/IdleAwaiter.kt
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test.junit4.android
-
-import androidx.compose.ui.platform.AndroidOwner
-import androidx.compose.ui.test.ExperimentalTesting
-import androidx.compose.ui.test.InternalTestingApi
-import androidx.compose.ui.test.junit4.isOnUiThread
-import androidx.test.espresso.AppNotIdleException
-import androidx.test.espresso.Espresso
-import androidx.test.espresso.IdlingRegistry
-import androidx.test.espresso.IdlingResourceTimeoutException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.suspendCancellableCoroutine
-import kotlinx.coroutines.withContext
-import java.util.concurrent.CountDownLatch
-import java.util.concurrent.TimeUnit
-import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.coroutines.resume
-import kotlin.time.ExperimentalTime
-
-@OptIn(InternalTestingApi::class)
-internal class IdleAwaiter {
-
-    fun waitForIdle() {
-        check(!isOnUiThread()) {
-            "Functions that involve synchronization (Assertions, Actions, Synchronization; " +
-                "e.g. assertIsSelected(), doClick(), runOnIdle()) cannot be run " +
-                "from the main thread. Did you nest such a function inside " +
-                "runOnIdle {}, runOnUiThread {} or setContent {}?"
-        }
-
-        // First wait until we have an AndroidOwner (in case an Activity is being started)
-        waitForAndroidOwners()
-        // Then await composition(s)
-        runEspressoOnIdle()
-
-        // TODO(b/155774664): waitForAndroidOwners() may be satisfied by an AndroidOwner from an
-        //  Activity that is about to be paused, in cases where a new Activity is being started.
-        //  That means that AndroidOwnerRegistry.getOwners() may still return an empty list
-        //  between now and when the new Activity has created its AndroidOwner, even though
-        //  waitForAndroidOwners() suggests that we are now guaranteed one.
-    }
-
-    @ExperimentalTesting
-    suspend fun awaitIdle() {
-        // TODO(b/169038516): when we can query AndroidOwners for measure or layout, remove
-        //  runEspressoOnIdle() and replace it with a suspend fun that loops while the
-        //  snapshot or the recomposer has pending changes, clocks are busy or owners have
-        //  pending measures or layouts; and do the await on AndroidUiDispatcher.Main
-        // We use Espresso to wait for composition, measure, layout and draw,
-        // and Espresso needs to be called from a non-ui thread; so use Dispatchers.IO
-        withContext(Dispatchers.IO) {
-            // First wait until we have an AndroidOwner (in case an Activity is being started)
-            awaitAndroidOwners()
-            // Then await composition(s)
-            runEspressoOnIdle()
-        }
-    }
-
-    // TODO(168223213): Make the CompositionAwaiter a suspend fun, remove ComposeIdlingResource
-    //  and blocking await Espresso.onIdle().
-    private fun runEspressoOnIdle() {
-        fun rethrowWithMoreInfo(e: Throwable, wasGlobalTimeout: Boolean) {
-            var diagnosticInfo = ""
-            val listOfIdlingResources = mutableListOf<String>()
-            IdlingRegistry.getInstance().resources.forEach { resource ->
-                if (resource is IdlingResourceWithDiagnostics) {
-                    val message = resource.getDiagnosticMessageIfBusy()
-                    if (message != null) {
-                        diagnosticInfo += "$message \n"
-                    }
-                }
-                listOfIdlingResources.add(resource.name)
-            }
-            if (diagnosticInfo.isNotEmpty()) {
-                val prefix = if (wasGlobalTimeout) {
-                    "Global time out"
-                } else {
-                    "Idling resource timed out"
-                }
-                throw ComposeNotIdleException(
-                    "$prefix: possibly due to compose being busy.\n" +
-                        diagnosticInfo +
-                        "All registered idling resources: " +
-                        listOfIdlingResources.joinToString(", "),
-                    e
-                )
-            }
-            // No extra info, re-throw the original exception
-            throw e
-        }
-
-        val compositionAwaiter = CompositionAwaiter()
-        try {
-            compositionAwaiter.start()
-            Espresso.onIdle()
-        } catch (e: Throwable) {
-            compositionAwaiter.cancel()
-
-            // Happens on the global time out, usually when global idling time out is less
-            // or equal to dynamic idling time out or when the timeout is not due to individual
-            // idling resource. This does not necessary mean that it can't be due to idling
-            // resource being busy. So we try to check if it failed due to compose being busy and
-            // add some extra information to the developer.
-            val appNotIdleMaybe = tryToFindCause<AppNotIdleException>(e)
-            if (appNotIdleMaybe != null) {
-                rethrowWithMoreInfo(appNotIdleMaybe, wasGlobalTimeout = true)
-            }
-
-            // Happens on idling resource taking too long. Espresso gives out which resources caused
-            // it but it won't allow us to give any extra information. So we check if it was our
-            // resource and give more info if we can.
-            val resourceNotIdleMaybe = tryToFindCause<IdlingResourceTimeoutException>(e)
-            if (resourceNotIdleMaybe != null) {
-                rethrowWithMoreInfo(resourceNotIdleMaybe, wasGlobalTimeout = false)
-            }
-
-            // No match, rethrow
-            throw e
-        }
-    }
-
-    /**
-     * Tries to find if the given exception or any of its cause is of the type of the provided
-     * throwable T. Returns null if there is no match. This is required as some exceptions end up
-     * wrapped in Runtime or Concurrent exceptions.
-     */
-    private inline fun <reified T : Throwable> tryToFindCause(e: Throwable): Throwable? {
-        var causeToCheck: Throwable? = e
-        while (causeToCheck != null) {
-            if (causeToCheck is T) {
-                return causeToCheck
-            }
-            causeToCheck = causeToCheck.cause
-        }
-        return null
-    }
-
-    private fun ensureAndroidOwnerRegistryIsSetUp() {
-        check(AndroidOwnerRegistry.isSetUp) {
-            "Test not setup properly. Use a ComposeTestRule in your test to be able to interact " +
-                "with composables"
-        }
-    }
-
-    private fun waitForAndroidOwners() {
-        ensureAndroidOwnerRegistryIsSetUp()
-
-        fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
-        if (!hasAndroidOwners()) {
-            val latch = CountDownLatch(1)
-            val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-                override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-                    if (hasAndroidOwners()) {
-                        latch.countDown()
-                    }
-                }
-            }
-            try {
-                AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-                if (!hasAndroidOwners()) {
-                    latch.await(2, TimeUnit.SECONDS)
-                }
-            } finally {
-                AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-            }
-        }
-    }
-
-    @ExperimentalTesting
-    @OptIn(ExperimentalTime::class)
-    private suspend fun awaitAndroidOwners() {
-        ensureAndroidOwnerRegistryIsSetUp()
-
-        fun hasAndroidOwners(): Boolean = AndroidOwnerRegistry.getOwners().isNotEmpty()
-
-        if (!hasAndroidOwners()) {
-            suspendCancellableCoroutine<Unit> { continuation ->
-                // Make sure we only resume once
-                val didResume = AtomicBoolean(false)
-                fun resume(listener: AndroidOwnerRegistry.OnRegistrationChangedListener) {
-                    if (didResume.compareAndSet(false, true)) {
-                        AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-                        continuation.resume(Unit)
-                    }
-                }
-
-                // Usually we resume if an AndroidOwner is registered while the listener is added
-                val listener = object : AndroidOwnerRegistry.OnRegistrationChangedListener {
-                    override fun onRegistrationChanged(owner: AndroidOwner, registered: Boolean) {
-                        if (hasAndroidOwners()) {
-                            resume(this)
-                        }
-                    }
-                }
-
-                AndroidOwnerRegistry.addOnRegistrationChangedListener(listener)
-                continuation.invokeOnCancellation {
-                    AndroidOwnerRegistry.removeOnRegistrationChangedListener(listener)
-                }
-
-                // Sometimes the AndroidOwner was registered before we added
-                // the listener, in which case we missed our signal
-                if (hasAndroidOwners()) {
-                    resume(listener)
-                }
-            }
-        }
-    }
-}
-
-/**
- * Thrown in cases where Compose can't get idle in Espresso's defined time limit.
- */
-class ComposeNotIdleException(message: String?, cause: Throwable?) : Throwable(message, cause)
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
index bacfaf2..20c2fb6 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopAnimationClockTestRule.kt
@@ -89,6 +89,13 @@
     }
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
+@Suppress("DocumentExceptions")
 actual fun createAnimationClockRule(): AnimationClockTestRule =
-    DesktopAnimationClockTestRule()
\ No newline at end of file
+    throw UnsupportedOperationException()
\ No newline at end of file
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
index 37b9c58..282ac47 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/DesktopComposeTestRule.kt
@@ -32,7 +32,6 @@
 import androidx.compose.ui.test.SemanticsNodeInteractionCollection
 import androidx.compose.ui.test.TestOwner
 import androidx.compose.ui.test.createTestContext
-import androidx.compose.ui.test.initCompose
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.unit.Density
@@ -53,10 +52,6 @@
 class DesktopComposeTestRule : ComposeTestRule {
 
     companion object {
-        init {
-            initCompose()
-        }
-
         var current: DesktopComposeTestRule? = null
     }
 
diff --git a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
index be5d66c..5a02b9c 100644
--- a/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
+++ b/compose/ui/ui-test-junit4/src/desktopMain/kotlin/androidx/compose/ui/test/junit4/SkijaTest.kt
@@ -15,7 +15,6 @@
  */
 package androidx.compose.ui.test.junit4
 
-import androidx.compose.ui.test.initCompose
 import org.jetbrains.skija.Surface
 import org.junit.rules.TestRule
 import org.junit.runner.Description
@@ -168,12 +167,6 @@
     private lateinit var testIdentifier: String
     private lateinit var album: SkijaTestAlbum
 
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     val executionQueue = LinkedList<() -> Unit>()
 
     override fun apply(base: Statement, description: Description?): Statement {
diff --git a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
index 08c2c7c..3fbde5c 100644
--- a/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
+++ b/compose/ui/ui-test-junit4/src/jvmMain/kotlin/androidx/compose/ui/test/junit4/AnimationClockTestRule.kt
@@ -48,5 +48,11 @@
     fun advanceClock(milliseconds: Long) = clock.advanceClock(milliseconds)
 }
 
+@Deprecated(
+    message = "AnimationClockTestRule is no longer supported as a standalone solution. Retrieve " +
+        "it from your ComposeTestRule instead",
+    level = DeprecationLevel.ERROR,
+    replaceWith = ReplaceWith("composeTestRule.clockTestRule")
+)
 @ExperimentalTesting
-expect fun createAnimationClockRule(): AnimationClockTestRule
\ No newline at end of file
+expect fun createAnimationClockRule(): AnimationClockTestRule
diff --git a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
index a708660..f94df4a 100644
--- a/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
+++ b/compose/ui/ui-test/src/androidMain/kotlin/androidx/compose/ui/test/AndroidInputDispatcher.kt
@@ -34,7 +34,7 @@
 import kotlinx.coroutines.withContext
 import kotlin.math.max
 
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
     require(owner is AndroidOwner) {
         "InputDispatcher currently only supports dispatching to AndroidOwner, not to " +
             owner::class.java.simpleName
@@ -47,7 +47,7 @@
     testContext: TestContext,
     owner: AndroidOwner?,
     private val sendEvent: (MotionEvent) -> Unit
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
 
     private val batchLock = Any()
     // Batched events are generated just-in-time, given the "lateness" of the dispatching (see
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 9f7e5e4..e3ab0ad 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
@@ -112,7 +112,7 @@
 
     // TODO(b/133217292): Better error: explain which gesture couldn't be performed
     private var _inputDispatcher: InputDispatcher? =
-        InputDispatcher(testContext, checkNotNull(owner))
+        createInputDispatcher(testContext, checkNotNull(owner))
     internal val inputDispatcher
         get() = checkNotNull(_inputDispatcher) {
             "Can't send gesture, (Partial)GestureScope has already been disposed"
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
index 7cbf710..01baa72 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/InputDispatcher.kt
@@ -24,7 +24,7 @@
 import kotlin.math.max
 import kotlin.math.roundToInt
 
-internal expect fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
+internal expect fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher
 
 /**
  * Dispatcher to inject full and partial gestures. An [InputDispatcher] is created at the
@@ -37,9 +37,6 @@
  * Clients of [InputDispatcher] should only call methods for the first stage listed below, the
  * second stage is handled by [performGesture].
  *
- * Implementations of [InputDispatcher] must derive from [PersistingInputDispatcher], which
- * handles state restoration.
- *
  * Full gestures:
  * * [enqueueClick]
  * * [enqueueSwipe]
@@ -56,7 +53,10 @@
  * Chaining methods:
  * * [enqueueDelay]
  */
-internal abstract class InputDispatcher {
+internal abstract class InputDispatcher(
+    private val testContext: TestContext,
+    private val owner: Owner?
+) {
     companion object {
         /**
          * Whether or not injection of events should be suspended in between events until [now]
@@ -124,6 +124,26 @@
      */
     protected abstract val now: Long
 
+    init {
+        val state = testContext.states.remove(owner)
+        if (state?.partialGesture != null) {
+            nextDownTime = state.nextDownTime
+            gestureLateness = state.gestureLateness
+            partialGesture = state.partialGesture
+        }
+    }
+
+    protected open fun saveState(owner: Owner?) {
+        if (owner != null) {
+            testContext.states[owner] =
+                InputDispatcherState(
+                    nextDownTime,
+                    gestureLateness,
+                    partialGesture
+                )
+        }
+    }
+
     /**
      * Generates the downTime of the next gesture with the given [duration]. The gesture's
      * [duration] is necessary to facilitate chaining of gestures: if another gesture is made
@@ -549,7 +569,12 @@
     /**
      * Called when this [InputDispatcher] is about to be discarded, from [GestureScope.dispose].
      */
-    abstract fun dispose()
+    fun dispose() {
+        saveState(owner)
+        onDispose()
+    }
+
+    protected open fun onDispose() {}
 }
 
 /**
@@ -565,3 +590,22 @@
     val lastPositions = mutableMapOf(Pair(pointerId, startPosition))
     var hasPointerUpdates: Boolean = false
 }
+
+/**
+ * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
+ * when the [GestureScope] is recreated.
+ *
+ * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
+ * This property will only be restored if an incomplete gesture was in progress when the
+ * state of the [InputDispatcher] was saved.
+ * @param gestureLateness The time difference in milliseconds between enqueuing the first
+ * event of the gesture and dispatching it. Depending on the implementation of
+ * [InputDispatcher], this may or may not be used.
+ * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
+ * when the state of the [InputDispatcher] was saved, this will be `null`.
+ */
+internal data class InputDispatcherState(
+    val nextDownTime: Long,
+    var gestureLateness: Long?,
+    val partialGesture: PartialGesture?
+)
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
deleted file mode 100644
index 0ef9f06..0000000
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/PersistingInputDispatcher.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.test
-
-import androidx.compose.ui.node.Owner
-
-internal abstract class PersistingInputDispatcher(
-    private val testContext: TestContext,
-    private val owner: Owner?
-) : InputDispatcher() {
-
-    init {
-        val state = testContext.states.remove(owner)
-        if (state?.partialGesture != null) {
-            nextDownTime = state.nextDownTime
-            gestureLateness = state.gestureLateness
-            partialGesture = state.partialGesture
-        }
-    }
-
-    protected open fun saveState(owner: Owner?) {
-        if (owner != null) {
-            testContext.states[owner] =
-                InputDispatcherState(nextDownTime, gestureLateness, partialGesture)
-        }
-    }
-
-    final override fun dispose() {
-        saveState(owner)
-        onDispose()
-    }
-
-    open fun onDispose() {}
-
-    /**
-     * The state of an [InputDispatcher], saved when the [GestureScope] is disposed and restored
-     * when the [GestureScope] is recreated.
-     *
-     * @param nextDownTime The downTime of the start of the next gesture, when chaining gestures.
-     * This property will only be restored if an incomplete gesture was in progress when the
-     * state of the [InputDispatcher] was saved.
-     * @param gestureLateness The time difference in milliseconds between enqueuing the first
-     * event of the gesture and dispatching it. Depending on the implementation of
-     * [InputDispatcher], this may or may not be used.
-     * @param partialGesture The state of an incomplete gesture. If no gesture was in progress
-     * when the state of the [InputDispatcher] was saved, this will be `null`.
-     */
-    internal data class InputDispatcherState(
-        val nextDownTime: Long,
-        var gestureLateness: Long?,
-        val partialGesture: PartialGesture?
-    )
-}
diff --git a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
index c4084d0..9624c69 100644
--- a/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
+++ b/compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/TestOwner.kt
@@ -19,7 +19,6 @@
 import androidx.compose.ui.node.Owner
 import androidx.compose.ui.semantics.SemanticsNode
 import androidx.compose.ui.semantics.getAllSemanticsNodes
-import androidx.compose.ui.test.PersistingInputDispatcher.InputDispatcherState
 import androidx.compose.ui.text.input.EditOperation
 import androidx.compose.ui.text.input.ImeAction
 
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
index 94671fd..3d9f038 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/DesktopInputDispatcher.kt
@@ -24,14 +24,14 @@
 import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.unit.Uptime
 
-internal actual fun InputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
+internal actual fun createInputDispatcher(testContext: TestContext, owner: Owner): InputDispatcher {
     return DesktopInputDispatcher(testContext, owner as DesktopOwner)
 }
 
 internal class DesktopInputDispatcher(
     testContext: TestContext,
     val owner: DesktopOwner
-) : PersistingInputDispatcher(testContext, owner) {
+) : InputDispatcher(testContext, owner) {
     companion object {
         var gesturePointerId = 0L
     }
diff --git a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
index 8720246..3c47455 100644
--- a/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
+++ b/compose/ui/ui-test/src/desktopMain/kotlin/androidx/compose/ui/test/TestComposeWindow.kt
@@ -50,10 +50,4 @@
         owner.draw(canvas)
         return owners
     }
-
-    companion object {
-        init {
-            initCompose()
-        }
-    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-text/api/current.txt b/compose/ui/ui-text/api/current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/current.txt
+++ b/compose/ui/ui-text/api/current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/public_plus_experimental_current.txt b/compose/ui/ui-text/api/public_plus_experimental_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui-text/api/public_plus_experimental_current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/api/restricted_current.txt b/compose/ui/ui-text/api/restricted_current.txt
index bff3058..1c0358a 100644
--- a/compose/ui/ui-text/api/restricted_current.txt
+++ b/compose/ui/ui-text/api/restricted_current.txt
@@ -62,8 +62,8 @@
   public final class AnnotatedStringKt {
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.SpanStyle spanStyle, optional androidx.compose.ui.text.ParagraphStyle? paragraphStyle);
     method public static androidx.compose.ui.text.AnnotatedString AnnotatedString(String text, androidx.compose.ui.text.ParagraphStyle paragraphStyle);
-    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString AnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
-    method public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method @Deprecated public static inline androidx.compose.ui.text.AnnotatedString annotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
+    method public static inline androidx.compose.ui.text.AnnotatedString buildAnnotatedString(kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.AnnotatedString.Builder,kotlin.Unit> builder);
     method public static androidx.compose.ui.text.AnnotatedString capitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static androidx.compose.ui.text.AnnotatedString decapitalize(androidx.compose.ui.text.AnnotatedString, optional androidx.compose.ui.text.intl.LocaleList localeList);
     method public static int getLength(androidx.compose.ui.text.AnnotatedString);
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
index 4b443f7..5eb4cee 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/AnnotatedStringBuilderSamples.kt
@@ -21,7 +21,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
@@ -124,7 +124,7 @@
 @Sampled
 fun AnnotatedStringBuilderLambdaSample() {
     // create an AnnotatedString using the lambda builder
-    annotatedString {
+    buildAnnotatedString {
         // append "Hello" with red text color
         withStyle(SpanStyle(color = Color.Red)) {
             append("Hello")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
index ba55a26..17c7b19 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/BaselineShiftSamples.kt
@@ -20,7 +20,7 @@
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.BaselineShift
 import androidx.compose.ui.text.withStyle
 import androidx.compose.ui.unit.sp
@@ -30,7 +30,7 @@
 fun BaselineShiftSample() {
     Text(
         fontSize = 20.sp,
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text = "Hello")
             withStyle(SpanStyle(baselineShift = BaselineShift.Superscript, fontSize = 16.sp)) {
                 append("superscript")
@@ -45,7 +45,7 @@
 @Sampled
 @Composable
 fun BaselineShiftAnnotatedStringSample() {
-    val annotatedString = annotatedString {
+    val annotatedString = buildAnnotatedString {
         append("Text ")
         withStyle(SpanStyle(baselineShift = BaselineShift.Superscript)) {
             append("Demo")
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
index 0fc3863..51f383c 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/ParagraphStyleSamples.kt
@@ -21,7 +21,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.text.ParagraphStyle
 import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.text.style.TextIndent
 import androidx.compose.ui.unit.sp
@@ -57,7 +57,7 @@
     )
 
     Text(
-        text = annotatedString {
+        text = buildAnnotatedString {
             append(text)
             addStyle(paragraphStyle1, 0, text.indexOf('\n') + 1)
             addStyle(paragraphStyle2, text.indexOf('\n') + 1, text.length)
diff --git a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
index e85c75f..451b611 100644
--- a/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
+++ b/compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/SpanStyleSamples.kt
@@ -21,8 +21,8 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.annotatedString
 import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.unit.sp
 
 @Sampled
@@ -30,7 +30,7 @@
 fun SpanStyleSample() {
     Text(
         fontSize = 16.sp,
-        text = annotatedString {
+        text = buildAnnotatedString {
             withStyle(style = SpanStyle(color = Color.Red)) {
                 append("Hello")
             }
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
index 11456e3..6f01560 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTest.kt
@@ -1431,7 +1431,7 @@
      * Helper function which creates an AnnotatedString where each input string becomes a paragraph.
      */
     private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
-        return annotatedString {
+        return buildAnnotatedString {
             for (paragraph in paragraphs) {
                 pushStyle(ParagraphStyle())
                 append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
index 97ec592..c14cc7a 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/MultiParagraphIntegrationTextDirectionTest.kt
@@ -242,7 +242,7 @@
      * Helper function which creates an AnnotatedString where each input string becomes a paragraph.
      */
     private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
-        return annotatedString {
+        return buildAnnotatedString {
             for (paragraph in paragraphs) {
                 pushStyle(ParagraphStyle())
                 append(paragraph)
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
index a88a311..2417156 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/AndroidAccessibilitySpannableStringTest.kt
@@ -33,7 +33,7 @@
 import androidx.compose.ui.text.InternalTextApi
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.TestFontResourceLoader
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
 import androidx.compose.ui.text.font.FontWeight
@@ -62,7 +62,7 @@
     @Test
     fun toAccessibilitySpannableString_with_locale() {
         val languageTag = "en-GB"
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(localeList = LocaleList(languageTag))) {
                 append("world")
@@ -84,7 +84,7 @@
     @Test
     fun toAccessibilitySpannableString_with_color() {
         val color = Color.Black
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(color = color)) {
                 append("world")
@@ -105,7 +105,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontSizeInSp() {
         val fontSize = 12.sp
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontSize = fontSize)) {
                 append("world")
@@ -126,7 +126,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontSizeInEm() {
         val fontSize = 2.em
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontSize = fontSize)) {
                 append("world")
@@ -146,7 +146,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_fontWeightBold() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) {
                 append("world")
@@ -166,7 +166,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_italic() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontStyle = FontStyle.Italic)) {
                 append("world")
@@ -187,7 +187,7 @@
     @Test
     fun toAccessibilitySpannableString_with_fontFamily() {
         val fontFamily = FontFamily.Monospace
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(fontFamily = fontFamily)) {
                 append("world")
@@ -207,7 +207,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_underline() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) {
                 append("world")
@@ -225,7 +225,7 @@
 
     @Test
     fun toAccessibilitySpannableString_with_lineThrough() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(textDecoration = TextDecoration.LineThrough)) {
                 append("world")
@@ -244,7 +244,7 @@
     @Test
     fun toAccessibilitySpannableString_with_scaleX() {
         val scaleX = 1.2f
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(
                 style = SpanStyle(textGeometricTransform = TextGeometricTransform(scaleX = scaleX))
@@ -267,7 +267,7 @@
     @Test
     fun toAccessibilitySpannableString_with_background() {
         val backgroundColor = Color.Red
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("hello")
             withStyle(style = SpanStyle(background = backgroundColor)) {
                 append("world")
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
index 189c1ad..732c765 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/AnnotatedString.kt
@@ -743,10 +743,10 @@
  * @param builder lambda to modify [AnnotatedString.Builder]
  */
 @Deprecated(
-    message = "Renamed to annotatedString.",
-    replaceWith = ReplaceWith("annotatedString")
+    message = "Renamed to buildAnnotatedString.",
+    replaceWith = ReplaceWith("buildAnnotatedString")
 )
-inline fun AnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
     Builder().apply(builder).toAnnotatedString()
 
 /**
@@ -757,7 +757,7 @@
  *
  * @param builder lambda to modify [AnnotatedString.Builder]
  */
-inline fun annotatedString(builder: (Builder).() -> Unit): AnnotatedString =
+inline fun buildAnnotatedString(builder: (Builder).() -> Unit): AnnotatedString =
     Builder().apply(builder).toAnnotatedString()
 
 /**
diff --git a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
index 0f1e350..42a8fbe 100644
--- a/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
+++ b/compose/ui/ui-text/src/commonMain/kotlin/androidx/compose/ui/text/MultiParagraphIntrinsics.kt
@@ -58,7 +58,7 @@
     }
 
     /**
-     * [ParagraphIntrinsics] for each paragraph included in the [annotatedString]. For empty string
+     * [ParagraphIntrinsics] for each paragraph included in the [buildAnnotatedString]. For empty string
      * there will be a single empty paragraph intrinsics info.
      */
     internal val infoList: List<ParagraphIntrinsicInfo>
diff --git a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
index ddf38bd3..efac85d 100644
--- a/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
+++ b/compose/ui/ui-text/src/desktopMain/kotlin/androidx/compose/ui/text/platform/DesktopParagraph.kt
@@ -278,11 +278,11 @@
         return null
     }
 
-    private fun getBoxBackwardByOffset(offset: Int): TextBox? {
+    private fun getBoxBackwardByOffset(offset: Int, end: Int = offset): TextBox? {
         var from = offset - 1
         while (from >= 0) {
             val box = para.getRectsForRange(
-                from, offset,
+                from, end,
                 RectHeightMode.STRUT, RectWidthMode.TIGHT
             ).firstOrNull()
             when {
@@ -314,8 +314,10 @@
         return para.getGlyphPositionAtCoordinate(position.x, position.y).position
     }
 
-    override fun getBoundingBox(offset: Int) =
-        getBoxForwardByOffset(offset)!!.rect.toComposeRect()
+    override fun getBoundingBox(offset: Int): Rect {
+        val box = getBoxForwardByOffset(offset) ?: getBoxBackwardByOffset(offset, text.length)!!
+        return box.rect.toComposeRect()
+    }
 
     override fun getWordBoundary(offset: Int) = para.getWordBoundary(offset).let {
         TextRange(it.start, it.end)
@@ -554,6 +556,8 @@
 
             when (op) {
                 is Op.StyleAdd -> {
+                    // cached SkTextStyled could was loaded with a different font loader
+                    ensureFontsAreRegistered(fontLoader, op.style)
                     pb.pushStyle(makeSkTextStyle(op.style))
                 }
                 is Op.PutPlaceholder -> {
@@ -585,6 +589,12 @@
         return pb.build()
     }
 
+    private fun ensureFontsAreRegistered(fontLoader: FontLoader, style: ComputedStyle) {
+        style.fontFamily?.let {
+            fontLoader.ensureRegistered(it)
+        }
+    }
+
     private sealed class Op {
         abstract val position: Int
 
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
index f81951e..641b23cd 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringBuilderTest.kt
@@ -491,7 +491,7 @@
         val paragraphStyle1 = ParagraphStyle(textAlign = TextAlign.Right)
         val paragraphStyle2 = ParagraphStyle(textAlign = TextAlign.Right)
 
-        val buildResult = annotatedString {
+        val buildResult = buildAnnotatedString {
             withStyle(paragraphStyle1) {
                 withStyle(spanStyle1) {
                     append(text1)
diff --git a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
index 909a52e..5a422e9 100644
--- a/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
+++ b/compose/ui/ui-text/src/test/java/androidx/compose/ui/text/AnnotatedStringTest.kt
@@ -324,7 +324,7 @@
 
     @Test
     fun subSequence_withAnnotations_noIntersection() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -338,7 +338,7 @@
 
     @Test
     fun subSequence_withAnnotations_collapsedRange() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -370,7 +370,7 @@
 
     @Test
     fun subSequence_withAnnotations_hasIntersection() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
@@ -398,7 +398,7 @@
 
     @Test
     fun subSequence_withAnnotations_containsRange() {
-        val annotatedString = annotatedString {
+        val annotatedString = buildAnnotatedString {
             append("ab")
             pushStringAnnotation("scope1", "annotation1")
             append("cd")
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index 4fc98e6c..db472c3 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -67,8 +67,5 @@
 tasks.withType(KotlinCompile).configureEach {
     kotlinOptions {
         useIR = true
-        freeCompilerArgs += [
-                "-P", "plugin:androidx.compose.compiler.plugins.kotlin:sourceInformation=true"
-        ]
     }
 }
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 cfcd2a2..73d6452 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
@@ -463,6 +463,7 @@
 
     @SdkSuppress(minSdkVersion = 29) // Render id is not returned for api < 29:  b/171519437
     @Test
+    @Ignore("b/174152464")
     fun testTextId() {
         val slotTableRecord = SlotTableRecord.create()
 
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
index 591a4fa..6c431b6 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/ComposeViewAdapterTest.kt
@@ -53,17 +53,30 @@
      * Asserts that the given Composable method executes correct and outputs some [ViewInfo]s.
      */
     private fun assertRendersCorrectly(className: String, methodName: String): List<ViewInfo> {
-        val committed = CountDownLatch(1)
+        val committedAndDrawn = CountDownLatch(1)
+        val committed = AtomicBoolean(false)
         activityTestRule.runOnUiThread {
             composeViewAdapter.init(
                 className, methodName, debugViewInfos = true,
                 onCommit = {
-                    committed.countDown()
+                    committed.set(true)
+                },
+                onDraw = {
+                    if (committed.get()) {
+                        committedAndDrawn.countDown()
+                    }
                 }
             )
         }
 
-        committed.await()
+        // Workaround for a problem described in b/174291742 where onLayout will not be called
+        // after composition for the first test in the test suite.
+        activityTestRule.runOnUiThread {
+            composeViewAdapter.requestLayout()
+        }
+
+        // Wait for the first draw after the Composable has been committed.
+        committedAndDrawn.await()
         activityTestRule.runOnUiThread {
             assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
         }
diff --git a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
index c74b740..e2ebdbd 100644
--- a/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
+++ b/compose/ui/ui-tooling/src/androidTest/java/androidx/compose/ui/tooling/preview/PreviewParameterTest.kt
@@ -22,7 +22,6 @@
 import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
 import androidx.compose.ui.tooling.preview.datasource.LoremIpsum
 import androidx.compose.ui.tooling.test.R
-import org.junit.Assert
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -53,10 +52,6 @@
                 debugViewInfos = true
             )
         }
-
-        activityTestRule.runOnUiThread {
-            Assert.assertTrue(composeViewAdapter.viewInfos.isNotEmpty())
-        }
     }
 
     private class MyListProvider : CollectionPreviewParameterProvider<Int>(listOf(1, 2, 3))
diff --git a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
index c72068b..7e6d2b6 100644
--- a/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
+++ b/compose/ui/ui-tooling/src/main/java/androidx/compose/ui/tooling/preview/ComposeViewAdapter.kt
@@ -255,6 +255,7 @@
             throw exception
         }
 
+        processViewInfos()
         if (composableName.isNotEmpty()) {
             // TODO(b/160126628): support other APIs, e.g. animate
             findAndSubscribeTransitions()
@@ -423,7 +424,6 @@
 
         previewComposition = @Composable {
             onCommit {
-                processViewInfos()
                 onCommit()
             }
 
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 5043c6d..a0f762a 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class FocusModifierKt {
@@ -279,7 +278,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +294,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -2402,6 +2401,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2571,6 +2574,7 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2582,6 +2586,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2710,6 +2715,7 @@
     method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 5043c6d..a0f762a 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class FocusModifierKt {
@@ -279,7 +278,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +294,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -2402,6 +2401,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2571,6 +2574,7 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2582,6 +2586,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2710,6 +2715,7 @@
     method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 6ab61a8..1dc8c01 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -119,8 +119,7 @@
     method public static androidx.compose.ui.Modifier materialize(androidx.compose.runtime.Composer<?>, androidx.compose.ui.Modifier modifier);
   }
 
-  public interface ContentDrawScope extends androidx.compose.ui.graphics.drawscope.DrawScope {
-    method public void drawContent();
+  public final class ContentDrawScopeKt {
   }
 
   public final class DrawLayerModifierKt {
@@ -130,7 +129,7 @@
   public final class DrawModifierKt {
     method @Deprecated public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method @Deprecated public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method @Deprecated public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class FocusModifierKt {
@@ -279,7 +278,7 @@
     method public float getFontScale();
     method public long getSize-NH-jbRc();
     method public androidx.compose.ui.draw.DrawResult onDrawBehind(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> block);
-    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> block);
+    method public androidx.compose.ui.draw.DrawResult onDrawWithContent(kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> block);
     property public float density;
     property public float fontScale;
     property public final long size;
@@ -295,13 +294,13 @@
   }
 
   public interface DrawModifier extends androidx.compose.ui.Modifier.Element {
-    method public void draw(androidx.compose.ui.ContentDrawScope);
+    method public void draw(androidx.compose.ui.graphics.drawscope.ContentDrawScope);
   }
 
   public final class DrawModifierKt {
     method public static androidx.compose.ui.Modifier drawBehind(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.DrawScope,kotlin.Unit> onDraw);
     method public static androidx.compose.ui.Modifier drawWithCache(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.draw.CacheDrawScope,androidx.compose.ui.draw.DrawResult> onBuildDrawCache);
-    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.ContentDrawScope,kotlin.Unit> onDraw);
+    method public static androidx.compose.ui.Modifier drawWithContent(androidx.compose.ui.Modifier, kotlin.jvm.functions.Function1<? super androidx.compose.ui.graphics.drawscope.ContentDrawScope,kotlin.Unit> onDraw);
   }
 
   public final class DrawResult {
@@ -2464,6 +2463,10 @@
     ctor public LoadedResource(T? resource);
   }
 
+  public final class PainterResourcesKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.ui.graphics.painter.Painter painterResource(@DrawableRes int id);
+  }
+
   public final class PendingResource<T> extends androidx.compose.ui.res.Resource<T> {
     ctor public PendingResource(T? resource);
   }
@@ -2633,6 +2636,7 @@
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCopyText();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> getCustomActions();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getCutText();
+    method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getDismiss();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> getGetTextLayoutResult();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnClick();
     method public androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> getOnLongClick();
@@ -2644,6 +2648,7 @@
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CopyText;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<java.util.List<androidx.compose.ui.semantics.CustomAccessibilityAction>> CustomActions;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> CutText;
+    property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> Dismiss;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function1<java.util.List<androidx.compose.ui.text.TextLayoutResult>,java.lang.Boolean>>> GetTextLayoutResult;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnClick;
     property public final androidx.compose.ui.semantics.SemanticsPropertyKey<androidx.compose.ui.semantics.AccessibilityAction<kotlin.jvm.functions.Function0<java.lang.Boolean>>> OnLongClick;
@@ -2772,6 +2777,7 @@
     method public static void cutText(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static void dialog(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static void disabled(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
+    method public static void dismiss(androidx.compose.ui.semantics.SemanticsPropertyReceiver, optional String? label, kotlin.jvm.functions.Function0<java.lang.Boolean> action);
     method public static String getAccessibilityLabel(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static String getAccessibilityValue(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
     method public static androidx.compose.ui.semantics.AccessibilityRangeInfo getAccessibilityValueRange(androidx.compose.ui.semantics.SemanticsPropertyReceiver);
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
index fd1f866..ba2d319 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/HorizontalScrollersInVerticalScrollerDemo.kt
@@ -27,7 +27,7 @@
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
index 1b6c95a..a7dab04 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/gestures/VerticalScrollerInDrawerLayoutDemo.kt
@@ -34,7 +34,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
index cbae372..5500315 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/AndroidViewSample.kt
@@ -22,14 +22,23 @@
 import androidx.annotation.Sampled
 import androidx.compose.foundation.background
 import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.platform.AmbientContext
+import androidx.compose.ui.unit.dp
 import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.content.ContextCompat
+import kotlin.math.roundToInt
 
 @Suppress("SetTextI18n")
 @Sampled
@@ -43,3 +52,20 @@
         view.layoutParams = ViewGroup.LayoutParams(size, size)
     }
 }
+
+@Sampled
+@Composable
+fun AndroidDrawableInDrawScopeSample() {
+    val drawable = ContextCompat.getDrawable(AmbientContext.current, R.drawable.sample_drawable)
+    Box(
+        modifier = Modifier.size(100.dp)
+            .drawBehind {
+                drawIntoCanvas { canvas ->
+                    drawable?.let {
+                        it.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
+                        it.draw(canvas.nativeCanvas)
+                    }
+                }
+            }
+    )
+}
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
index 04a0ab1..a53a638 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/DrawModifierSample.kt
@@ -26,7 +26,7 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.graphics.BlendMode
diff --git a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
index ef4205e..dfce282 100644
--- a/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
+++ b/compose/ui/ui/samples/src/main/java/androidx/compose/ui/samples/PainterSample.kt
@@ -17,9 +17,11 @@
 package androidx.compose.ui.samples
 
 import androidx.annotation.Sampled
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.paint
@@ -27,6 +29,7 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.dp
 
 @Sampled
@@ -53,4 +56,16 @@
                 .background(color = Color.Yellow)
                 .paint(CustomPainter())
     ) { /** intentionally empty **/ }
+}
+
+@Sampled
+@Composable
+fun PainterResourceSample() {
+    // Sample showing how to render a Painter based on a different resource (vector vs png)
+    // Here a Vector asset is used in the portrait orientation, however, a png is used instead
+    // in the landscape orientation based on the res/drawable and res/drawable-land-hdpi folders
+    Image(
+        painterResource(R.drawable.ic_vector_or_png),
+        modifier = Modifier.size(50.dp)
+    )
 }
\ No newline at end of file
diff --git a/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
new file mode 100755
index 0000000..4696f99
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable-land-hdpi/ic_vector_or_png.png
Binary files differ
diff --git a/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
new file mode 100644
index 0000000..64452ad
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable/ic_vector_or_png.xml
@@ -0,0 +1,36 @@
+<!--
+  ~ 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.
+  -->
+
+<vector android:height="200dp" android:viewportHeight="144"
+    android:viewportWidth="144" android:width="200dp"
+    xmlns:android="http://schemas.android.com/apk/res/android">
+    <path android:fillColor="#F79D80" android:pathData="M69.26,55.73m-53.39,0a53.39,53.39 0,1 1,106.78 0a53.39,53.39 0,1 1,-106.78 0"/>
+    <path android:fillColor="#37474F" android:pathData="M47.66,76.35h2.26v65.31h-2.26z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M61.47,22.88l7.59,0.63C69.06,23.51 65.07,22.59 61.47,22.88z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M78.86,44.71c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.11,-3.17 2.61,-4.26 5.6,-4.5l-11.02,-0.92c-3.05,9.16 1.58,18.08 14.69,24.18c10.96,5.1 6.86,12.67 3.64,16.47c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.43 86.05,50.53 78.86,44.71z"/>
+    <path android:fillColor="#434343" android:pathData="M68.8,62.41c-3.13,-3.32 -7.57,-5.4 -12.49,-5.4c-0.78,0 -1.54,0.06 -2.29,0.16c-6.08,0.37 -39.94,5.23 -35.41,67.19c0,0 1.34,-0.96 3.61,-2.58c-2.04,-56.41 29.85,-60.98 35.73,-61.34c0.75,-0.1 1.51,-0.16 2.29,-0.16C65.16,60.29 66.58,61.23 68.8,62.41"/>
+    <path android:fillColor="#FFD54F" android:pathData="M83.23,22.46l20.76,1.43c0.4,0.03 0.52,-0.52 0.16,-0.67l-19.1,-7.78c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.35C82.86,22.23 83.01,22.44 83.23,22.46z"/>
+    <path android:fillColor="#F9BF2C" android:pathData="M86.66,16.09l-1.62,-0.66c-0.2,-0.08 -0.42,0.03 -0.47,0.24l-1.66,6.36c-0.06,0.21 0.1,0.42 0.31,0.44l10.52,0.72L86.66,16.09z"/>
+    <path android:fillColor="#1B2428" android:pathData="M47.66,103.79l0,3.86l2.24,2.24l0,-7.7z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.52,-0.06c2.52,1.27 5.61,2.55 9.09,2.79c8.62,0.59 15.88,-2.89 16.22,-7.77C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+    <path android:fillColor="#881A51" android:pathData="M71.76,7.67c-5.16,-0.35 -9.83,0.75 -12.82,2.74l0,0c-7.5,4.2 -9.01,13.44 -9.18,14.36c-0.18,0.93 3.05,2.01 3.05,2.01l4.12,-4.16l4.67,-0.38c2.52,1.27 7.02,1.03 10.5,1.27c8.62,0.59 14.33,-1.05 14.66,-5.93C87.1,12.7 80.38,8.26 71.76,7.67z"/>
+    <path android:fillColor="#FFFFFF" android:pathData="M71.83,11.32c-5.16,-0.29 -9.83,0.61 -12.82,2.21l0,0c-7.5,3.39 -9.16,10.46 -9.25,11.23c-0.15,1.18 3.05,3.13 3.05,3.13l4.2,-4.48l4.47,-0.54c2.52,1.03 5.59,2.28 9.07,2.47c8.62,0.48 15.08,-2.54 15.41,-6.49C86.29,14.92 80.45,11.8 71.83,11.32z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M73.53,24.78c-3.48,-0.19 -6.55,-1.44 -9.07,-2.47c0,0 0,0 0,0l-1.08,0.13c-0.68,0.11 -1.33,0.28 -1.91,0.52l0.11,-0.01c0,0 0,0 0,0c2.52,1.03 5.59,2.28 9.07,2.47c3.37,0.19 6.4,-0.16 8.87,-0.89C77.7,24.8 75.68,24.9 73.53,24.78z"/>
+    <path android:fillColor="#EEEEEE" android:pathData="M82.13,48.13c1.75,4.57 2.05,9.63 0.33,14.43c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.33,0.24 -41.01,29.29 -53.34,38.1c0.03,2.98 0.16,6.12 0.41,9.41c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.13,62.01 86.99,53.98 82.13,48.13z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M78.86,44.91c-0.16,-0.13 -0.33,-0.26 -0.5,-0.39c5.68,5.93 8.39,14.63 5.53,22.58c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -43.58,31.16 -54.59,39.02c0.06,1.4 0.15,2.8 0.26,4.26c0,0 57.03,-40.74 57.11,-40.79c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.88,60.64 86.05,50.73 78.86,44.91z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M78.94,44.76c-6.8,-5.5 -22.66,-7.48 -22.99,-17.32c-0.04,-1.21 0.33,-2.11 0.97,-2.78c-1.81,0.6 -3.12,1.8 -3.04,4.02C54.2,38.52 70.07,40.5 76.86,46c7.19,5.82 11.02,15.73 7.81,24.68c-2.02,5.62 -6.26,11.01 -11.13,14.5c-0.07,0.05 -44.69,31.92 -54.89,39.21c0.01,0.08 0.01,0.16 0.02,0.24c0,0 56.87,-40.62 56.95,-40.68c4.87,-3.48 9.11,-8.88 11.13,-14.5C89.96,60.49 86.13,50.59 78.94,44.76z"/>
+    <path android:fillColor="#BDBDBD" android:pathData="M54.5,28.8c0,0 -0.52,0.61 -0.52,0.61c0.01,-0.01 -0.02,-0.11 -0.02,-0.12c-0.02,-0.11 -0.03,-0.23 -0.05,-0.34c-0.04,-0.4 -0.06,-0.79 -0.04,-1.19c0.02,-0.55 0.1,-1.1 0.24,-1.64c0.17,-0.63 0.43,-1.24 0.78,-1.79c0.42,-0.65 0.96,-1.21 1.58,-1.66c0.32,-0.23 0.66,-0.43 1.01,-0.61c0.47,-0.24 0.93,-0.43 1.46,-0.49c0.27,-0.03 0.55,-0.05 0.82,-0.05c0.89,-0.02 1.78,0.07 2.65,0.24c0.37,0.07 0.73,0.16 1.09,0.26c0.19,0.05 0.39,0.11 0.58,0.17c0.1,0.03 0.2,0.07 0.3,0.1c0.04,0.01 0.27,0.13 0.3,0.11c0,0 -1.79,1.07 -1.79,1.07s-5.78,-1.34 -6.91,3.25L54.5,28.8z"/>
+</vector>
diff --git a/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
new file mode 100644
index 0000000..68053bc
--- /dev/null
+++ b/compose/ui/ui/samples/src/main/res/drawable/sample_drawable.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+  <gradient
+      android:startColor="@android:color/holo_red_dark"
+      android:centerColor="@android:color/holo_orange_dark"
+      android:endColor="@android:color/holo_blue_dark" />
+</shape>
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index dfad721..0b258d2 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -37,6 +37,7 @@
 import androidx.compose.ui.semantics.SemanticsWrapper
 import androidx.compose.ui.semantics.accessibilityValue
 import androidx.compose.ui.semantics.accessibilityValueRange
+import androidx.compose.ui.semantics.dismiss
 import androidx.compose.ui.semantics.focused
 import androidx.compose.ui.semantics.getTextLayoutResult
 import androidx.compose.ui.semantics.onClick
@@ -108,15 +109,17 @@
     }
 
     @Test
-    fun testPopulateAccessibilityNodeInfoProperties() {
-        var info = AccessibilityNodeInfoCompat.obtain()
+    fun testPopulateAccessibilityNodeInfoProperties_general() {
+        val info = AccessibilityNodeInfoCompat.obtain()
         val clickActionLabel = "click"
+        val dismissActionLabel = "dismiss"
         val accessibilityValue = "checked"
-        var semanticsModifier = SemanticsModifierCore(1, true) {
+        val semanticsModifier = SemanticsModifierCore(1, true) {
             this.accessibilityValue = accessibilityValue
             onClick(clickActionLabel) { true }
+            dismiss(dismissActionLabel) { true }
         }
-        var semanticsNode = SemanticsNode(
+        val semanticsNode = SemanticsNode(
             SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
             true
         )
@@ -131,6 +134,15 @@
                 )
             )
         )
+        assertTrue(
+            containsAction(
+                info,
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                    AccessibilityNodeInfoCompat.ACTION_DISMISS,
+                    dismissActionLabel
+                )
+            )
+        )
         val stateDescription = when {
             BuildCompat.isAtLeastR() -> {
                 info.unwrap().stateDescription
@@ -147,14 +159,17 @@
         assertEquals(accessibilityValue, stateDescription)
         assertTrue(info.isClickable)
         assertTrue(info.isVisibleToUser)
+    }
 
-        info = AccessibilityNodeInfoCompat.obtain()
+    @Test
+    fun testPopulateAccessibilityNodeInfoProperties_SeekBar() {
+        val info = AccessibilityNodeInfoCompat.obtain()
         val setProgressActionLabel = "setProgress"
-        semanticsModifier = SemanticsModifierCore(1, true) {
+        val semanticsModifier = SemanticsModifierCore(1, true) {
             accessibilityValueRange = AccessibilityRangeInfo(0.5f, 0f..1f, 6)
             setProgress(setProgressActionLabel) { true }
         }
-        semanticsNode = SemanticsNode(
+        val semanticsNode = SemanticsNode(
             SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
             true
         )
@@ -178,12 +193,15 @@
                 )
             )
         }
+    }
 
-        info = AccessibilityNodeInfoCompat.obtain()
+    @Test
+    fun testPopulateAccessibilityNodeInfoProperties_EditText() {
+        val info = AccessibilityNodeInfoCompat.obtain()
         val setSelectionActionLabel = "setSelection"
         val setTextActionLabel = "setText"
         val text = "hello"
-        semanticsModifier = SemanticsModifierCore(1, true) {
+        val semanticsModifier = SemanticsModifierCore(1, true) {
             this.text = AnnotatedString(text)
             this.textSelectionRange = TextRange(1)
             this.focused = true
@@ -191,7 +209,7 @@
             setText(setTextActionLabel) { true }
             setSelection(setSelectionActionLabel) { _, _, _ -> true }
         }
-        semanticsNode = SemanticsNode(
+        val semanticsNode = SemanticsNode(
             SemanticsWrapper(InnerPlaceable(LayoutNode()), semanticsModifier),
             true
         )
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 bffcb8c8..46ca6ca 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
@@ -59,6 +59,7 @@
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
 import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.clipRect
 import androidx.compose.ui.graphics.drawscope.translate
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 84b5920..0257d38 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
@@ -585,7 +585,7 @@
                             emit<LayoutNode, Applier<Any>>(
                                 ctor = LayoutEmitHelper.constructor,
                                 update = {
-                                    node = this.node
+                                    set(Unit) { node = this }
                                     set(noOpMeasureBlocks, LayoutEmitHelper.setMeasureBlocks)
                                 }
                             )
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
index 6a601b5..4dba953cc 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.kt
@@ -460,6 +460,15 @@
             }
         }
 
+        semanticsNode.config.getOrNull(SemanticsActions.Dismiss)?.let {
+            info.addAction(
+                AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+                    AccessibilityNodeInfoCompat.ACTION_DISMISS,
+                    it.label
+                )
+            )
+        }
+
         if (semanticsNode.config.contains(CustomActions)) {
             val customActions = semanticsNode.config[CustomActions]
             if (customActions.size >= AccessibilityActionsResourceIds.size) {
@@ -891,6 +900,11 @@
                     it.action()
                 } ?: false
             }
+            AccessibilityNodeInfoCompat.ACTION_DISMISS -> {
+                return node.config.getOrNull(SemanticsActions.Dismiss)?.let {
+                    it.action()
+                } ?: false
+            }
             // TODO: handling for other system actions
             else -> {
                 val label = actionIdToLabel[virtualViewId]?.get(action) ?: return false
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
index c377937..b8ad9f3 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/ImageResources.kt
@@ -30,6 +30,8 @@
  *
  * Note: This API is transient and will be likely removed for encouraging async resource loading.
  *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
  * @param id the resource identifier
  * @return the decoded image data associated with the resource
  */
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
new file mode 100644
index 0000000..d8af23f
--- /dev/null
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/PainterResources.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.ui.res
+
+import android.content.res.Resources
+import android.util.TypedValue
+import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.nativeCanvas
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.imageFromResource
+import androidx.compose.ui.graphics.painter.ImagePainter
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.rememberVectorPainter
+import androidx.compose.ui.graphics.vector.VectorPainter
+import androidx.compose.ui.graphics.vector.compat.seekToStartTag
+import androidx.compose.ui.platform.AmbientContext
+
+/**
+ * Create a [Painter] from an Android resource id. This can load either an instance of
+ * [ImagePainter] or [VectorPainter] for [ImageBitmap] based assets or vector based assets
+ * respectively. The resources with the given id must point to either fully rasterized
+ * images (ex. PNG or JPG files) or VectorDrawable xml assets. API based xml Drawables
+ * are not supported here.
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.PainterResourceSample
+ *
+ * Alternative Drawable implementations can be used with compose by calling
+ * [drawIntoCanvas] and drawing with the Android framework canvas provided through [nativeCanvas]
+ *
+ * Example:
+ * @sample androidx.compose.ui.samples.AndroidDrawableInDrawScopeSample
+ *
+ * @param id Resources object to query the image file from
+ *
+ * @return [Painter] used for drawing the loaded resource
+ */
+@Composable
+fun painterResource(@DrawableRes id: Int): Painter {
+    val context = AmbientContext.current
+    val res = context.resources
+    val value = remember { TypedValue() }
+    res.getValue(id, value, true)
+    val path = value.string
+    // Assume .xml suffix implies loading a VectorDrawable resource
+    return if (path?.endsWith(".xml") == true) {
+        val imageVector = remember(path, id) {
+            loadVectorResource(context.theme, res, id)
+        }
+        rememberVectorPainter(imageVector)
+    } else {
+        // Otherwise load the bitmap resource
+        val imageBitmap = remember(path, id) {
+            loadImageBitmapResource(res, id)
+        }
+        ImagePainter(imageBitmap)
+    }
+}
+
+/**
+ * Helper method to validate that the xml resource is a vector drawable then load
+ * the ImageVector. Because this throws exceptions we cannot have this implementation as part of
+ * the composable implementation it is invoked in.
+ */
+private fun loadVectorResource(theme: Resources.Theme, res: Resources, id: Int): ImageVector {
+    @Suppress("ResourceType") val parser = res.getXml(id)
+    if (parser.seekToStartTag().name != "vector") {
+        throw IllegalArgumentException(errorMessage)
+    }
+    return loadVectorResourceInner(theme, res, parser)
+}
+
+/**
+ * Helper method to validate the asset resource is a supported resource type and returns
+ * an ImageBitmap resource. Because this throws exceptions we cannot have this implementation
+ * as part of the composable implementation it is invoked in.
+ */
+private fun loadImageBitmapResource(res: Resources, id: Int): ImageBitmap {
+    try {
+        return imageFromResource(res, id)
+    } catch (throwable: Throwable) {
+        throw IllegalArgumentException(errorMessage)
+    }
+}
+
+private const val errorMessage =
+    "Only VectorDrawables and rasterized asset types are supported ex. PNG, JPG"
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
index 8f59986..3fd252b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/res/VectorResources.kt
@@ -16,8 +16,8 @@
 
 package androidx.compose.ui.res
 
-import android.annotation.SuppressLint
 import android.content.res.Resources
+import android.content.res.XmlResourceParser
 import android.util.TypedValue
 import android.util.Xml
 import androidx.annotation.DrawableRes
@@ -39,6 +39,8 @@
  * based off of it's dimensions appropriately
  *
  * Note: This API is transient and will be likely removed for encouraging async resource loading.
+ *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
  */
 @Composable
 fun vectorResource(@DrawableRes id: Int): ImageVector {
@@ -57,6 +59,8 @@
  * [PendingResource]. Once the loading finishes, recompose is scheduled and this function will
  * return deferred vector drawable resource with [LoadedResource] or [FailedResource].
  *
+ * For loading generic loading of rasterized or vector assets see [painterResource]
+ *
  * @param id the resource identifier
  * @param pendingResource an optional resource to be used during loading instead.
  * @param failedResource an optional resource to be used if resource loading failed.
@@ -90,11 +94,23 @@
 internal fun loadVectorResource(
     theme: Resources.Theme? = null,
     res: Resources,
-    resId: Int
+    resId: Int,
+): ImageVector =
+    loadVectorResourceInner(theme, res, res.getXml(resId).apply { seekToStartTag() })
+
+/**
+ * Helper method that parses a vector asset from the given [XmlResourceParser] position.
+ * This method assumes the parser is already been positioned to the start tag
+ */
+@Throws(XmlPullParserException::class)
+@SuppressWarnings("RestrictedApi")
+internal fun loadVectorResourceInner(
+    theme: Resources.Theme? = null,
+    res: Resources,
+    parser: XmlResourceParser
 ): ImageVector {
-    @SuppressLint("ResourceType") val parser = res.getXml(resId)
     val attrs = Xml.asAttributeSet(parser)
-    val builder = parser.seekToStartTag().createVectorImageBuilder(res, theme, attrs)
+    val builder = parser.createVectorImageBuilder(res, theme, attrs)
 
     var nestedGroups = 0
     while (!parser.isAtEnd()) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
index 30a3c4d..715a43f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ContentDrawScope.kt
@@ -13,20 +13,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.compose.ui
 
-import androidx.compose.ui.graphics.drawscope.DrawScope
+package androidx.compose.ui
 
 /**
  * Receiver scope for drawing content into a layout, where the content can
  * be drawn between other canvas operations. If [drawContent] is not called,
  * the contents of the layout will not be drawn.
- *
- * @see DrawModifier
  */
-interface ContentDrawScope : DrawScope {
-    /**
-     * Causes child drawing operations to run during the `onPaint` lambda.
-     */
-    fun drawContent()
-}
+@Deprecated(
+    "Use ContentDrawScope in the graphics package instead",
+    ReplaceWith(
+        "ContentDrawScope",
+        "androidx.compose.ui.graphics.drawscope.ContentDrawScope"
+    )
+)
+typealias ContentDrawScope = androidx.compose.ui.graphics.drawscope.ContentDrawScope
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
index 398b8db..8a2591f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/DrawModifier.kt
@@ -37,7 +37,7 @@
     "Use DrawModifier from the androidx.compose.ui.draw package instead",
     ReplaceWith(
         "DrawModifier",
-        "androidx.compose.ui.draw"
+        "androidx.compose.ui.draw.DrawModifier"
     )
 )
 typealias DrawModifier = androidx.compose.ui.draw.DrawModifier
@@ -50,7 +50,7 @@
     "Use DrawCacheModifier from the androidx.compose.ui.draw package instead",
     ReplaceWith(
         "DrawCacheModifier",
-        "androidx.compose.ui.draw"
+        "androidx.compose.ui.draw.DrawCacheModifier"
     )
 )
 typealias DrawCacheModifier = androidx.compose.ui.draw.DrawCacheModifier
@@ -62,7 +62,7 @@
     "Use drawBehind from the androidx.compose.ui.draw package instead",
     ReplaceWith(
         "drawBehind(onDraw)",
-        "androidx.compose.ui.draw"
+        "androidx.compose.ui.draw.drawBehind"
     )
 )
 fun Modifier.drawBehind(onDraw: DrawScope.() -> Unit) = drawBehind(onDraw)
@@ -87,7 +87,7 @@
     "Use the drawWithCache from the androidx.compose.ui.draw package instead",
     ReplaceWith(
         "drawWithCache(onBuildCache)",
-        "androidx.compose.ui.draw"
+        "androidx.compose.ui.draw.drawWithCache"
     )
 )
 fun Modifier.drawWithCache(onBuildDrawCache: CacheDrawScope.() -> DrawResult) =
@@ -103,7 +103,7 @@
  */
 @Deprecated(
     "Use CacheDrawScope from the androidx.compose.ui.draw package instead",
-    ReplaceWith("CacheDrawScope", "androidx.compose.ui.draw")
+    ReplaceWith("CacheDrawScope", "androidx.compose.ui.draw.CacheDrawScope")
 )
 typealias CacheDrawScope = androidx.compose.ui.draw.CacheDrawScope
 
@@ -113,12 +113,16 @@
  */
 @Deprecated(
     "Use DrawResult from the androidx.compose.ui.draw package instead",
-    ReplaceWith("DrawResult", "androidx.compose.ui.draw")
+    ReplaceWith("DrawResult", "androidx.compose.ui.draw.DrawResult")
 )
 typealias DrawResult = androidx.compose.ui.draw.DrawResult
 
+@Suppress("DEPRECATION")
 @Deprecated(
     "Use drawWithContent from the androidx.compose.ui.draw package instead",
-    ReplaceWith("drawWithContent(onDraw)", "androidx.compose.ui.draw")
+    ReplaceWith(
+        "drawWithContent(onDraw)",
+        "androidx.compose.ui.draw.drawWithContent"
+    )
 )
 fun Modifier.drawWithContent(onDraw: ContentDrawScope.() -> Unit) = drawWithContent(onDraw)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
index a2f6821..43ba66b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt
@@ -17,11 +17,11 @@
 package androidx.compose.ui.draw
 
 import androidx.compose.runtime.remember
-import androidx.compose.ui.ContentDrawScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.graphics.Canvas
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.graphics.drawscope.DrawScope
 import androidx.compose.ui.platform.InspectorInfo
 import androidx.compose.ui.platform.InspectorValueInfo
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 378184a..f50f987 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -17,7 +17,7 @@
 package androidx.compose.ui.draw
 
 import androidx.compose.ui.Alignment
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.layout.MeasureScope
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Size
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
index 6f534f2..de8dcf9 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/SuspendingPointerInputFilter.kt
@@ -177,7 +177,7 @@
  * a LayoutNode.
  *
  * [SuspendingPointerInputFilter] implements the [PointerInputScope] used to offer the
- * [Modifier.pointerInput] DSL and carries the [Density] from [DensityAmbient] at the point of
+ * [Modifier.pointerInput] DSL and carries the [Density] from [AmbientDensity] at the point of
  * the modifier's materialization. Even if this value were returned to the [PointerInputFilter]
  * callbacks, we would still need the value at composition time in order for [Modifier.pointerInput]
  * to begin its internal [LaunchedEffect] for the provided code block.
@@ -236,6 +236,13 @@
     private var lastPointerEvent: PointerEvent? = null
 
     /**
+     * The size of the bounds of this input filter. Normally [PointerInputFilter.size] can
+     * be used, but for tests, it is better to not rely on something set to an `internal`
+     * method.
+     */
+    private var boundsSize: IntSize = IntSize.Zero
+
+    /**
      * Snapshot the current [pointerHandlers] and run [block] on each one.
      * May not be called reentrant or concurrent with itself.
      *
@@ -255,13 +262,9 @@
         try {
             when (pass) {
                 PointerEventPass.Initial, PointerEventPass.Final ->
-                    dispatchingPointerHandlers.forEach {
-                        block(it)
-                    }
+                    dispatchingPointerHandlers.forEach(block)
                 PointerEventPass.Main ->
-                    dispatchingPointerHandlers.forEachReversed {
-                        block(it)
-                    }
+                    dispatchingPointerHandlers.forEachReversed(block)
             }
         } finally {
             dispatchingPointerHandlers.clear()
@@ -286,6 +289,7 @@
         pass: PointerEventPass,
         bounds: IntSize
     ) {
+        boundsSize = bounds
         if (pass == PointerEventPass.Initial) {
             currentPointers.clear()
             pointerEvent.changes.fastMapTo(currentPointers) { it.current }
@@ -336,7 +340,7 @@
         val handlerCoroutine = PointerEventHandlerCoroutine(
             continuation,
             currentPointers,
-            size,
+            boundsSize,
             viewConfiguration,
             this
         )
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index 0470a3f..f4e5345 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -17,7 +17,7 @@
 
 import androidx.compose.runtime.collection.MutableVector
 import androidx.compose.runtime.collection.mutableVectorOf
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.FocusModifier
 import androidx.compose.ui.FocusObserverModifier
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
index 34db148..e6d3278 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt
@@ -20,7 +20,7 @@
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextRange
-import androidx.compose.ui.text.annotatedString
+import androidx.compose.ui.text.buildAnnotatedString
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.util.annotation.IntRange
 import kotlin.reflect.KProperty
@@ -155,7 +155,7 @@
             if (parentValue == null) {
                 childValue
             } else {
-                annotatedString {
+                buildAnnotatedString {
                     append(parentValue)
                     append(", ")
                     append(childValue)
@@ -289,6 +289,13 @@
     val PasteText = SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("PasteText")
 
     /**
+     * Action to dismiss a dismissible node.
+     *
+     * @see SemanticsPropertyReceiver.dismiss
+     */
+    val Dismiss = SemanticsPropertyKey<AccessibilityAction<() -> Boolean>>("Dismiss")
+
+    /**
      * Custom actions which are defined by app developers.
      *
      * @see SemanticsPropertyReceiver.customActions
@@ -665,4 +672,17 @@
     action: () -> Boolean
 ) {
     this[SemanticsActions.PasteText] = AccessibilityAction(label, action)
+}
+
+/**
+ * This function adds the [SemanticsActions.Dismiss] to the [SemanticsPropertyReceiver].
+ *
+ * @param label Optional label for this action.
+ * @param action Action to be performed when the [SemanticsActions.Dismiss] is called.
+ */
+fun SemanticsPropertyReceiver.dismiss(
+    label: String? = null,
+    action: () -> Boolean
+) {
+    this[SemanticsActions.Dismiss] = AccessibilityAction(label, action)
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
index 323a091..9a82047 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppFrame.kt
@@ -21,36 +21,72 @@
 import androidx.compose.ui.window.MenuBar
 import java.awt.image.BufferedImage
 
+/**
+ * AppFrame is an abstract class that represents a window.
+ * <p>
+ * Known subclasses: AppWindow
+ */
 abstract class AppFrame {
 
+    /**
+     * Gets ComposeWindow object.
+     */
     abstract val window: ComposeWindow
 
     internal var menuBar: MenuBar? = null
 
+    /**
+     * Gets the parent window. If the value is not null, the current window is a dialog.
+     */
     var invoker: AppFrame? = null
         protected set
 
+    /**
+     * Gets the title of the window. The title is displayed in the windows's native border.
+     */
     val title: String
         get() = window.title
 
+    /**
+     * Gets the width of the window.
+     */
     val width: Int
         get() = window.width
 
+    /**
+     * Gets the height of the window.
+     */
     val height: Int
         get() = window.height
 
+    /**
+     * Gets the current x coordinate of the window.
+     */
     val x: Int
         get() = window.x
 
+    /**
+     * Gets the current y coordinate of the window.
+     */
     val y: Int
         get() = window.y
 
+    /**
+     * Returns true if the winodw is closed, false otherwise.
+     */
     var isClosed: Boolean = false
         internal set
 
+    /**
+     * Returns the icon image of the window. If the icon is not set, null is returned.
+     */
     var icon: BufferedImage? = null
         internal set
 
+    /**
+     * Returns events of the window. Each event is described as a lambda that is invoked when
+     * needed.
+     */
     var events: WindowEvents = WindowEvents()
         internal set
 
@@ -58,22 +94,64 @@
 
     internal var onDismiss: (() -> Unit)? = null
 
+    /**
+     * Sets the title of the window.
+     *
+     * @param title Window title text.
+     */
     abstract fun setTitle(title: String)
 
+    /**
+     * Sets the image icon of the window.
+     *
+     * @param image Image of the icon.
+     */
     abstract fun setIcon(image: BufferedImage?)
 
+    /**
+     * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+     * Linux) or at the top of the screen (Mac OS).
+     *
+     * @param manuBar Window menu bar.
+     */
     abstract fun setMenuBar(menuBar: MenuBar)
 
+    /**
+     * Removes the menu bar of the window.
+     */
     abstract fun removeMenuBar()
 
+    /**
+     * Sets the new position of the window on the screen.
+     *
+     * @param x the new x-coordinate of the window.
+     * @param y the new y-coordinate of the window.
+     */
     abstract fun setLocation(x: Int, y: Int)
 
+    /**
+     * Sets the window to the center of the screen.
+     */
     abstract fun setWindowCentered()
 
+    /**
+     * Sets the new size of the window.
+     *
+     * @param width the new width of the window.
+     * @param height the new height of the window.
+     */
     abstract fun setSize(width: Int, height: Int)
 
+    /**
+     * Shows a window with the given Compose content.
+     *
+     * @param content Composable content of the window.
+     */
     abstract fun show(content: @Composable () -> Unit)
 
+    /**
+     * Closes the window.
+     */
     abstract fun close()
 
     internal abstract fun dispose()
@@ -87,6 +165,20 @@
     internal abstract fun unlockWindow()
 }
 
+/**
+ * WindowEvents is the class that contains all the events supported by window.
+ *
+ * @param onOpen The event that is invoked after the window appears.
+ * @param onClose The event that is invoked after the window is closed.
+ * @param onMinimize The event that is invoked after the window is minimized.
+ * @param onMaximize The event that is invoked after the window is maximized.
+ * @param onRestore The event that is invoked when the window is restored after the window has been
+ * maximized or minimized.
+ * @param onFocusGet The event that is invoked when the window gets focus.
+ * @param onFocusLost The event that is invoked when the window lost focus.
+ * @param onResize The event that is invoked when the window is resized.
+ * @param onRelocate The event that is invoked when the window is relocated.
+ */
 class WindowEvents(
     var onOpen: (() -> Unit)? = null,
     var onClose: (() -> Unit)? = null,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
index d154ee8..f4131a1 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/AppWindow.kt
@@ -89,6 +89,9 @@
  */
 class AppWindow : AppFrame {
 
+    /**
+     * Gets ComposeWindow object.
+     */
     override val window: ComposeWindow
 
     init {
@@ -232,10 +235,20 @@
         pair = null
     }
 
+    /**
+     * Sets the title of the window.
+     *
+     * @param title Window title text.
+     */
     override fun setTitle(title: String) {
         window.setTitle(title)
     }
 
+    /**
+     * Sets the image icon of the window.
+     *
+     * @param image Image of the icon.
+     */
     override fun setIcon(image: BufferedImage?) {
         this.icon = image
         if (icon != null) {
@@ -249,16 +262,31 @@
         }
     }
 
+    /**
+     * Sets the menu bar of the window. The menu bar can be displayed inside a window (Windows,
+     * Linux) or at the top of the screen (Mac OS).
+     *
+     * @param manuBar Window menu bar.
+     */
     override fun setMenuBar(menuBar: MenuBar) {
         this.menuBar = menuBar
         window.setJMenuBar(menuBar.menuBar)
     }
 
+    /**
+     * Removes the menu bar of the window.
+     */
     override fun removeMenuBar() {
         this.menuBar = null
         window.setJMenuBar(JMenuBar())
     }
 
+    /**
+     * Sets the new size of the window.
+     *
+     * @param width the new width of the window.
+     * @param height the new height of the window.
+     */
     override fun setSize(width: Int, height: Int) {
         // better check min/max values of current window size
         var w = width
@@ -273,10 +301,19 @@
         window.setSize(w, h)
     }
 
+    /**
+     * Sets the new position of the window on the screen.
+     *
+     * @param x the new x-coordinate of the window.
+     * @param y the new y-coordinate of the window.
+     */
     override fun setLocation(x: Int, y: Int) {
         window.setLocation(x, y)
     }
 
+    /**
+     * Sets the window to the center of the screen.
+     */
     override fun setWindowCentered() {
         val dim: Dimension = Toolkit.getDefaultToolkit().getScreenSize()
         val x = dim.width / 2 - width / 2
@@ -293,6 +330,11 @@
         }
     }
 
+    /**
+     * Shows a window with the given Compose content.
+     *
+     * @param content Composable content of the window.
+     */
     @OptIn(ExperimentalKeyInput::class)
     override fun show(content: @Composable () -> Unit) {
         if (invoker != null) {
@@ -309,15 +351,18 @@
         events.invokeOnOpen()
     }
 
+    /**
+     * Closes the window.
+     */
     override fun close() {
         window.dispatchEvent(WindowEvent(window, WindowEvent.WINDOW_CLOSING))
     }
 
-    override fun dispose() {
+    internal override fun dispose() {
         invoker?.unlockWindow()
     }
 
-    override fun lockWindow() {
+    internal override fun lockWindow() {
         window.apply {
             defaultCloseOperation = WindowConstants.DO_NOTHING_ON_CLOSE
             setFocusableWindowState(false)
@@ -327,7 +372,7 @@
         invoker?.connectPair(this)
     }
 
-    override fun unlockWindow() {
+    internal override fun unlockWindow() {
         window.apply {
             defaultCloseOperation = WindowConstants.DISPOSE_ON_CLOSE
             setFocusableWindowState(true)
@@ -339,6 +384,9 @@
         disconnectPair()
     }
 
+    /**
+     * Gets the Keyboard object of the window.
+     */
     @ExperimentalKeyInput
     val keyboard: Keyboard = Keyboard()
 }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
index fbf5304..7d74767 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeInit.kt
@@ -18,8 +18,6 @@
 
 package androidx.compose.desktop
 
-import org.jetbrains.skiko.Library
-
 /**
  * Can be called multiple times.
  *
@@ -35,16 +33,6 @@
  *     }
  * }
  */
-fun initCompose() {
-    // call object initializer only once
-    ComposeInit
-}
-
-private object ComposeInit {
-    init {
-        Library.load("/", "skiko")
-        // Until https://github.com/Kotlin/kotlinx.coroutines/issues/2039 is resolved
-        // we have to set this property manually for coroutines to work.
-        System.getProperties().setProperty("kotlinx.coroutines.fast.service.loader", "false")
-    }
-}
\ No newline at end of file
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("We don't need to init Compose explicitly now")
+fun initCompose() = Unit
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
index 3524a49..c189e14 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeLayer.kt
@@ -16,12 +16,16 @@
 
 package androidx.compose.desktop
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
 import androidx.compose.ui.input.mouse.MouseScrollEvent
 import androidx.compose.ui.input.mouse.MouseScrollUnit
 import androidx.compose.ui.platform.DesktopComponent
+import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.platform.DesktopOwners
 import androidx.compose.ui.platform.FrameDispatcher
+import androidx.compose.ui.platform.setContent
 import androidx.compose.ui.unit.Density
 import org.jetbrains.skija.Canvas
 import org.jetbrains.skiko.HardwareLayer
@@ -137,12 +141,14 @@
 
     private fun initCanvas() {
         wrapped.addInputMethodListener(object : InputMethodListener {
-            override fun caretPositionChanged(p0: InputMethodEvent?) {
-                TODO("Implement input method caret change")
+            override fun caretPositionChanged(event: InputMethodEvent?) {
+                if (event != null) {
+                    owners?.onInputMethodEvent(event)
+                }
             }
 
             override fun inputMethodTextChanged(event: InputMethodEvent) = events.post {
-                owners?.onInputMethodTextChanged(event)
+                owners?.onInputMethodEvent(event)
             }
         })
 
@@ -277,7 +283,7 @@
         isDisposed = true
     }
 
-    fun needRedrawLayer() {
+    internal fun needRedrawLayer() {
         check(!isDisposed)
         frameDispatcher.scheduleFrame()
     }
@@ -285,4 +291,28 @@
     interface Renderer {
         suspend fun onFrame(canvas: Canvas, width: Int, height: Int, nanoTime: Long)
     }
+
+    internal fun setContent(
+        parent: Any? = null,
+        invalidate: () -> Unit = this::needRedrawLayer,
+        content: @Composable () -> Unit
+    ): Composition {
+        check(owners == null) {
+            "Cannot setContent twice."
+        }
+        val desktopOwners = DesktopOwners(wrapped, invalidate)
+        val desktopOwner = DesktopOwner(desktopOwners, density)
+
+        owners = desktopOwners
+        val composition = desktopOwner.setContent(content)
+
+        onDensityChanged(desktopOwner::density::set)
+
+        when (parent) {
+            is AppFrame -> parent.onDispose = desktopOwner::dispose
+            is ComposePanel -> parent.onDispose = desktopOwner::dispose
+        }
+
+        return composition
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
index fa2ca73..6915ec41 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposePanel.kt
@@ -27,12 +27,6 @@
  * ComposePanel is panel for building UI using Compose for Desktop.
  */
 class ComposePanel : JPanel {
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     constructor() : super() {
         setLayout(GridLayout(1, 1))
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
index 84257ff..b980f90 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/ComposeWindow.kt
@@ -15,17 +15,13 @@
  */
 package androidx.compose.desktop
 
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Composition
 import java.awt.event.ComponentAdapter
 import java.awt.event.ComponentEvent
 import javax.swing.JFrame
 
 class ComposeWindow : JFrame {
-    companion object {
-        init {
-            initCompose()
-        }
-    }
-
     val parent: AppFrame
     internal val layer = ComposeLayer()
 
@@ -43,6 +39,21 @@
         })
     }
 
+    /**
+     * Sets Compose content of the ComposeWindow.
+     *
+     * @param content Composable content of the ComposeWindow.
+     *
+     * @return Composition of the content.
+     */
+    fun setContent(content: @Composable () -> Unit): Composition {
+        return layer.setContent(
+            parent = parent,
+            invalidate = this::needRedrawLayer,
+            content = content
+        )
+    }
+
     private fun updateLayer() {
         if (!isVisible) {
             return
@@ -50,7 +61,7 @@
         layer.updateLayer()
     }
 
-    fun needRedrawLayer() {
+    internal fun needRedrawLayer() {
         if (!isVisible) {
             return
         }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
deleted file mode 100644
index 58b5578..0000000
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/desktop/Wrapper.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.compose.desktop
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Composition
-import androidx.compose.ui.platform.DesktopOwner
-import androidx.compose.ui.platform.DesktopOwners
-import androidx.compose.ui.platform.setContent
-
-/**
- * Sets Compose content of the ComposeWindow.
- *
- * @param content Composable content of the ComposeWindow.
- *
- * @return Composition of the content.
- */
-fun ComposeWindow.setContent(content: @Composable () -> Unit): Composition {
-    return this.layer.setContent(
-        parent = parent,
-        invalidate = this::needRedrawLayer,
-        content = content
-    )
-}
-
-internal fun ComposeLayer.setContent(
-    parent: Any? = null,
-    invalidate: () -> Unit = this::needRedrawLayer,
-    content: @Composable () -> Unit
-): Composition {
-    check(owners == null) {
-        "Cannot setContent twice."
-    }
-    val owners = DesktopOwners(this.wrapped, invalidate)
-    val owner = DesktopOwner(owners, density)
-    this.owners = owners
-    val composition = owner.setContent(content)
-
-    onDensityChanged(owner::density::set)
-
-    when (parent) {
-        is AppFrame -> parent.onDispose = owner::dispose
-        is ComposePanel -> parent.onDispose = owner::dispose
-    }
-
-    return composition
-}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
index fb56f9a..d615e5e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopOwners.kt
@@ -156,8 +156,19 @@
         platformInputService.onKeyTyped(event.keyChar)
     }
 
-    fun onInputMethodTextChanged(event: InputMethodEvent) {
-        platformInputService.onInputMethodTextChanged(event)
+    fun onInputMethodEvent(event: InputMethodEvent) {
+        if (!event.isConsumed()) {
+            when (event.id) {
+                InputMethodEvent.INPUT_METHOD_TEXT_CHANGED -> {
+                    platformInputService.replaceInputMethodText(event)
+                    event.consume()
+                }
+                InputMethodEvent.CARET_POSITION_CHANGED -> {
+                    platformInputService.inputMethodCaretPositionChanged(event)
+                    event.consume()
+                }
+            }
+        }
     }
 
     private fun pointerInputEvent(x: Int, y: Int, down: Boolean): PointerInputEvent {
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
index a455fb6..30a941e 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/DesktopPlatformInput.kt
@@ -155,9 +155,17 @@
         }
     }
 
-    fun onInputMethodTextChanged(event: InputMethodEvent) {
+    internal fun inputMethodCaretPositionChanged(
+        @Suppress("UNUSED_PARAMETER") event: InputMethodEvent
+    ) {
+        // Which OSes and which input method could produce such events? We need to have some
+        // specific cases in mind before implementing this
+        println("DesktopInputComponent.inputMethodCaretPositionChanged")
+    }
+
+    internal fun replaceInputMethodText(event: InputMethodEvent) {
         currentInput?.let { input ->
-            if (event.caret == null) {
+            if (event.text == null) {
                 return
             }
             val committed = event.text.toStringUntil(event.committedCharacterCount)
@@ -173,12 +181,15 @@
                 ops.add(DeleteSurroundingTextInCodePointsEditOp(1, 0))
             }
 
+            // newCursorPosition == 1 leads to effectively ignoring of this parameter in EditOps
+            // processing. the cursor will be set after the inserted text.
             if (committed.isNotEmpty()) {
-                ops.add(CommitTextEditOp(committed, committed.length))
+                ops.add(CommitTextEditOp(committed, 1))
             }
             if (composing.isNotEmpty()) {
-                ops.add(SetComposingTextEditOp(composing, composing.length))
+                ops.add(SetComposingTextEditOp(composing, 1))
             }
+
             input.onEditCommand.invoke(ops)
         }
     }
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 2df9345..545b562 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
@@ -17,17 +17,20 @@
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.onDispose
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.ui.gesture.tapGestureFilter
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.AmbientDensity
 import androidx.compose.ui.platform.DesktopOwner
 import androidx.compose.ui.platform.DesktopOwnersAmbient
 import androidx.compose.ui.platform.setContent
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.unit.IntBounds
 import androidx.compose.ui.unit.IntSize
-import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.round
 
 @Composable
 internal actual fun ActualPopup(
@@ -49,6 +52,24 @@
 ) {
     val owners = DesktopOwnersAmbient.current
     val density = AmbientDensity.current
+
+    var parentBounds = remember { mutableStateOf(IntBounds(0, 0, 0, 0)) }
+
+    // getting parent bounds
+    Layout(
+        content = {},
+        modifier = Modifier.onGloballyPositioned { childCoordinates ->
+            val coordinates = childCoordinates.parentCoordinates!!
+            parentBounds.value = IntBounds(
+                coordinates.localToGlobal(Offset.Zero).round(),
+                coordinates.size
+            )
+        },
+        measureBlock = { _, _ ->
+            layout(0, 0) {}
+        }
+    )
+
     val owner = remember {
         val owner = DesktopOwner(owners, density)
         owner.setContent {
@@ -60,16 +81,24 @@
                     }
                 },
                 measureBlock = { measurables, constraints ->
-                    val width = owner.size.width
-                    val height = owner.size.height
-                    layout(width, height) {
+                    val width = constraints.maxWidth
+                    val height = constraints.maxHeight
+
+                    val windowBounds = IntBounds(
+                        left = 0,
+                        top = 0,
+                        right = width,
+                        bottom = height
+                    )
+
+                    layout(constraints.maxWidth, constraints.maxHeight) {
                         measurables.forEach {
                             val placeable = it.measure(constraints)
                             val offset = popupPositionProvider.calculatePosition(
-                                IntBounds(0, 0, width, height),
-                                IntBounds(0, 0, width, height),
-                                LayoutDirection.Ltr,
-                                IntSize(placeable.width, placeable.height)
+                                parentGlobalBounds = parentBounds.value,
+                                windowGlobalBounds = windowBounds,
+                                layoutDirection = layoutDirection,
+                                popupContentSize = IntSize(placeable.width, placeable.height)
                             )
                             placeable.place(offset.x, offset.y)
                         }
@@ -83,4 +112,4 @@
     onDispose {
         owner.dispose()
     }
-}
\ No newline at end of file
+}
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
index 6a7f0d2..3672743 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.kt
@@ -15,23 +15,39 @@
  */
 package androidx.compose.ui.window
 
+import org.jetbrains.skiko.Library
 import java.awt.event.ActionListener
 import java.awt.event.ActionEvent
 import javax.swing.JMenu
 import javax.swing.JMenuBar
 import javax.swing.JMenuItem
 
+/**
+ * MenuBar is a class that represents a menu bar that can be attached to a window.
+ * The menu bar can be displayed inside a window (Windows, Linux) or at the top of
+ * the screen (Mac OS).
+ */
 class MenuBar {
     internal var menuBar: JMenuBar
 
     init {
         menuBar = JMenuBar()
+        // For the MenuBar to work correctly, we need to set the skiko system properties
+        Library.load()
     }
 
+    /**
+     * Constructs a MenuBar with the given menus.
+     *
+     * @param menu MenuBar menus.
+     */
     constructor(vararg menu: Menu) {
         menu(menu)
     }
 
+    /**
+     * Adds additional menus to the MenuBar.
+     */
     fun add(vararg menu: Menu) {
         menu(menu)
     }
@@ -55,10 +71,26 @@
     }
 }
 
+/**
+ * Menu is a class that represents a menu on a menu bar.
+ */
 class Menu {
+    /**
+     * Gets the menu name.
+     */
     val name: String
+
+    /**
+     * Gets the menu items.
+     */
     val list: List<MenuItem>
 
+    /**
+     * Constructs a Menu with the given name and menu items.
+     *
+     * @param name Menu name.
+     * @param item Menu items.
+     */
     constructor(name: String, vararg item: MenuItem) {
         this.name = name
         this.list = item.asList()
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
index cfd4ae3..d83546f 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuItem.kt
@@ -19,11 +19,34 @@
 import java.awt.Toolkit
 import javax.swing.KeyStroke
 
+/**
+ * MenuItem is a class that represents an implementation of an item in a menu.
+ * Can be used with Menu or Tray.
+ */
 class MenuItem {
+
+    /**
+     * Gets the MenuItem name.
+     */
     val name: String
+
+    /**
+     * Gets the MenuItem action when it is clicked or a keyboard shortcut is pressed (if specified).
+     */
     val action: (() -> Unit)?
+
+    /**
+     * Gets the MenuItem shortcut.
+     */
     val shortcut: KeyStroke
 
+    /**
+     * Constructs a MenuItem with the given name, action, and keyboard shortcut.
+     *
+     * @param name MenuItem name.
+     * @param onClick MenuItem action.
+     * @param shortcut MenuItem keyboard shortcut.
+     */
     constructor(
         name: String,
         onClick: (() -> Unit)? = null,
@@ -35,6 +58,14 @@
     }
 }
 
+/**
+ * Returns KeyStroke for the given key. KeyStroke contains a OS-specific modifier
+ * (Command on Mac OS and Control on Windows and Linux) and the given key.
+ *
+ * @param key Keyboard key.
+ *
+ * @return KeyStroke for the given key.
+ */
 fun KeyStroke(key: Key): KeyStroke {
     return KeyStroke.getKeyStroke(
         key.keyCode,
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
index d6e05cb..084d72b 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notifier.kt
@@ -21,15 +21,37 @@
 import java.awt.TrayIcon
 import java.awt.TrayIcon.MessageType
 
+/**
+ * Notifier is a class that can send system notifications.
+ */
 class Notifier {
+
+    /**
+     * Sends a regular notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun notify(title: String, message: String) {
         send(title, message, MessageType.INFO)
     }
 
+    /**
+     * Sends a warning notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun warn(title: String, message: String) {
         send(title, message, MessageType.WARNING)
     }
 
+    /**
+     * Sends a error notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun error(title: String, message: String) {
         send(title, message, MessageType.ERROR)
     }
diff --git a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
index 3bb7faa..5e51ec8 100644
--- a/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
+++ b/compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.kt
@@ -23,6 +23,9 @@
 import java.awt.TrayIcon
 import java.awt.TrayIcon.MessageType
 
+/**
+ * Tray is class for working with the system tray.
+ */
 class Tray {
     private lateinit var trayIcon: TrayIcon
     private var init: Boolean = false
@@ -31,8 +34,16 @@
         init = SystemTray.isSupported()
     }
 
+    /**
+     * Constructs a Tray with the empty icon image.
+     */
     constructor() {}
 
+    /**
+     * Constructs a Tray with the given icon image.
+     *
+     * @param image Tray icon image.
+     */
     constructor(image: Image) {
         if (!init) {
             return
@@ -42,6 +53,12 @@
         trayIcon.setImageAutoSize(true)
     }
 
+    /**
+     * Constructs a Tray with the given icon image and tooltip.
+     *
+     * @param image Tray icon image.
+     * @param tooltip Tray icon tooltip.
+     */
     constructor(image: Image, tooltip: String) {
         if (!init) {
             return
@@ -51,6 +68,11 @@
         trayIcon.setImageAutoSize(true)
     }
 
+    /**
+     * Sets the Tray icon image.
+     *
+     * @param image Tray icon image.
+     */
     fun icon(image: Image) {
         if (!init) {
             return
@@ -63,6 +85,11 @@
         }
     }
 
+    /**
+     * Sets the Tray menu.
+     *
+     * @param item Menu items.
+     */
     fun menu(vararg item: MenuItem) {
         if (!init) {
             return
@@ -85,6 +112,12 @@
         }
     }
 
+    /**
+     * Sends a regular notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun notify(title: String, message: String) {
         if (!init) {
             return
@@ -92,6 +125,12 @@
         trayIcon.displayMessage(title, message, MessageType.INFO)
     }
 
+    /**
+     * Sends a warning notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun warn(title: String, message: String) {
         if (!init) {
             return
@@ -99,6 +138,12 @@
         trayIcon.displayMessage(title, message, MessageType.WARNING)
     }
 
+    /**
+     * Sends a error notification with the given title and message.
+     *
+     * @param title Notification title.
+     * @param message Notification message.
+     */
     fun error(title: String, message: String) {
         if (!init) {
             return
@@ -106,6 +151,9 @@
         trayIcon.displayMessage(title, message, MessageType.ERROR)
     }
 
+    /**
+     * Removes the tray icon from the system tray.
+     */
     fun remove() {
         try {
             SystemTray.getSystemTray().remove(trayIcon)
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/AWTDebounceEventQueueTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
similarity index 100%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/FrameDispatcherTest.kt
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
similarity index 97%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
index 27a16c4..078976f 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/MutableResourceTest.kt
@@ -16,7 +16,6 @@
 
 package androidx.compose.desktop
 
-import androidx.compose.ui.test.TestThread
 import org.junit.Assert.assertTrue
 import org.junit.Test
 
diff --git a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
similarity index 96%
rename from compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
rename to compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
index f7f4eb3..f98d967 100644
--- a/compose/desktop/desktop/src/jvmTest/kotlin/androidx/compose/ui/test/TestThread.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/desktop/TestThread.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package androidx.compose.ui.test
+package androidx.compose.desktop
 
 internal class TestThread(private val _run: () -> Unit) : Thread() {
     private var exception: Exception? = null
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
new file mode 100644
index 0000000..e7e98ab
--- /dev/null
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopInputComponentTest.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.ui.platform
+
+import androidx.compose.ui.text.InternalTextApi
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.EditProcessor
+import androidx.compose.ui.text.input.ImeOptions
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.input.TextInputService
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import java.awt.Component
+import java.awt.event.InputMethodEvent
+import java.text.AttributedString
+
+private object DummyComponent : Component()
+
+@RunWith(JUnit4::class)
+class DesktopInputComponentTest {
+    @OptIn(InternalTextApi::class)
+    @Test
+    fun replaceInputMethodText_basic() {
+        val processor = EditProcessor()
+
+        val input = DesktopPlatformInput(DummyDesktopComponent)
+        val inputService = TextInputService(input)
+
+        val token = inputService.startInput(
+            TextFieldValue(),
+            ImeOptions.Default,
+            processor::onEditCommands,
+            {}
+        )
+
+        processor.onNewState(TextFieldValue("h"), inputService, token)
+
+        val familyEmoji = "\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66"
+
+        input.replaceInputMethodText(
+            InputMethodEvent(
+                DummyComponent,
+                InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
+                AttributedString(familyEmoji).iterator,
+                11,
+                null,
+                null
+            )
+        )
+
+        val buffer = processor.mBufferState
+
+        Assert.assertEquals("${familyEmoji}h", buffer.text)
+        Assert.assertEquals(TextRange(11), buffer.selection)
+    }
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
index 1e5c29a..10adbae 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/DesktopOwnerTest.kt
@@ -39,8 +39,8 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Size
 import androidx.compose.ui.gesture.scrollorientationlocking.Orientation
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.mouse.MouseScrollEvent
 import androidx.compose.ui.input.mouse.MouseScrollUnit
 import androidx.compose.ui.layout.Layout
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
index e30a9da..b637b80 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/GraphicsLayerTest.kt
@@ -22,12 +22,12 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.TransformOrigin
-import androidx.compose.ui.graphicsLayer
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.test.TestComposeWindow
+import androidx.compose.ui.test.junit4.DesktopScreenshotTestRule
+import androidx.compose.ui.unit.dp
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
index c17175e..6c3c4b2 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/RenderingTestScope.kt
@@ -20,7 +20,6 @@
 import androidx.compose.runtime.Providers
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
-import androidx.compose.ui.test.initCompose
 import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -34,7 +33,6 @@
     platform: DesktopPlatform = DesktopPlatform.Linux,
     block: suspend RenderingTestScope.() -> Unit
 ) = runBlocking(Dispatchers.Main) {
-    initCompose()
     val scope = RenderingTestScope(width, height, platform)
     try {
         scope.block()
diff --git a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
index fb44ef2..e697605 100644
--- a/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
+++ b/compose/ui/ui/src/desktopTest/kotlin/androidx/compose/ui/platform/SkijaLayerTest.kt
@@ -16,15 +16,15 @@
 
 package androidx.compose.ui.platform
 
-import androidx.compose.ui.TransformOrigin
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.TransformOrigin
+import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.unit.IntOffset
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.round
-import androidx.compose.ui.test.junit4.createComposeRule
 import org.junit.Assert.assertEquals
 import org.junit.Rule
 import org.junit.Test
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 73bcf4c..b954d68 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
@@ -15,7 +15,7 @@
  */
 package androidx.compose.ui.node
 
-import androidx.compose.ui.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
 import androidx.compose.ui.draw.DrawModifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.TransformOrigin
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 3c70810..729b498 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -2227,6 +2227,7 @@
     method public static int checkArgumentInRange(int, int, int, String);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int, String?);
     method @IntRange(from=0) public static int checkArgumentNonnegative(int);
+    method public static int checkFlagsArgument(int, int);
     method public static <T> T checkNotNull(T?);
     method public static <T> T checkNotNull(T?, Object);
     method public static void checkState(boolean, String?);
diff --git a/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
new file mode 100644
index 0000000..3d9b90e
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/PreconditionsTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.core.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.fail;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class PreconditionsTest {
+
+    @Test
+    public void testCheckFlagsArgument() throws Exception {
+        final int allowedFlags = (1 << 0) | (1 << 1) | (1 << 2);
+
+        int flags = 0;
+        assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+        flags = (1 << 1) | (1 << 2);
+        assertThat(Preconditions.checkFlagsArgument(flags, allowedFlags)).isEqualTo(flags);
+
+        flags = (1 << 3);
+        try {
+            Preconditions.checkFlagsArgument(flags, allowedFlags);
+            fail("Expected IllegalArgumentException");
+        } catch (IllegalArgumentException expected) { }
+    }
+}
diff --git a/core/core/src/main/java/androidx/core/util/Preconditions.java b/core/core/src/main/java/androidx/core/util/Preconditions.java
index 9f8833a..9a759f8 100644
--- a/core/core/src/main/java/androidx/core/util/Preconditions.java
+++ b/core/core/src/main/java/androidx/core/util/Preconditions.java
@@ -164,6 +164,20 @@
     }
 
     /**
+     * Check the requested flags, throwing if any requested flags are outside the allowed set.
+     *
+     * @return the validated requested flags.
+     */
+    public static int checkFlagsArgument(final int requestedFlags, final int allowedFlags) {
+        if ((requestedFlags & allowedFlags) != requestedFlags) {
+            throw new IllegalArgumentException("Requested flags 0x"
+                    + Integer.toHexString(requestedFlags) + ", but only 0x"
+                    + Integer.toHexString(allowedFlags) + " are allowed");
+        }
+        return requestedFlags;
+    }
+
+    /**
      * Ensures that that the argument numeric value is non-negative.
      *
      * @param value a numeric int value
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
index 84d597a..37548c3 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/DataStoreFactory.kt
@@ -28,8 +28,9 @@
  */
 public object DataStoreFactory {
     /**
-     * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
-     * there is never more than one DataStore acting on a file at a time.
+     * Create an instance of SingleProcessDataStore. Never create more than one instance of
+     * DataStore for a given file; doing so can break all DataStore functionality. You should
+     * consider managing your DataStore instance as a singleton.
      *
      * T is the type DataStore acts on. The type T must be immutable. Mutating a type used in
      * DataStore invalidates any guarantees that DataStore provides and will result in
@@ -47,6 +48,8 @@
      * @param produceFile Function which returns the file that the new DataStore will act on. The
      * function must return the same path every time. No two instances of DataStore should act on
      * the same file at the same time.
+     *
+     * @return a new DataStore instance with the provided configuration
      */
     @JvmOverloads // Generate constructors for default params for java users.
     public fun <T> create(
diff --git a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
index fcdddd7..1dbcac4 100644
--- a/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
+++ b/datastore/datastore-core/src/main/java/androidx/datastore/core/SingleProcessDataStore.kt
@@ -300,11 +300,16 @@
             }
 
             if (!scratchFile.renameTo(file)) {
-                throw IOException("$scratchFile could not be renamed to $file")
+                throw IOException(
+                    "Unable to rename $scratchFile." +
+                        "This likely means that there are multiple instances of DataStore " +
+                        "for this file. Ensure that you are only creating a single instance of " +
+                        "datastore for this file."
+                )
             }
         } catch (ex: IOException) {
             if (scratchFile.exists()) {
-                scratchFile.delete()
+                scratchFile.delete() // Swallow failure to delete
             }
             throw ex
         }
diff --git a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
index a3fedbc..ed8110d 100644
--- a/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
+++ b/datastore/datastore-preferences-core/src/main/java/androidx/datastore/preferences/core/PreferenceDataStoreFactory.kt
@@ -31,8 +31,9 @@
  */
 public object PreferenceDataStoreFactory {
     /**
-     * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
-     * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+     * Create an instance of SingleProcessDataStore. Never create more than one instance of
+     * DataStore for a given file; doing so can break all DataStore functionality. You should
+     * consider managing your DataStore instance as a singleton.
      *
      * @param produceFile Function which returns the file that the new DataStore will act on.
      * The function must return the same path every time. No two instances of PreferenceDataStore
@@ -49,6 +50,8 @@
      * The function must return the same path every time. No two instances of PreferenceDataStore
      * should act on the same file at the same time. The file must have the extension
      * preferences_pb.
+     *
+     * @return a new DataStore instance with the provided configuration
      */
     @JvmOverloads
     public fun create(
diff --git a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
index 245df17..b25b66b 100644
--- a/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
+++ b/datastore/datastore-preferences/src/main/java/androidx/datastore/preferences/PreferenceDataStoreFactory.kt
@@ -31,8 +31,11 @@
 import java.io.File
 
 /**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton. Note: this function is *not*
+ * comparable to [Context.getSharedPreferences]; this function creates and returns a
+ * new instance of DataStore each time it is called.
  *
  * @param name The name of the preferences. The preferences will be stored in a file obtained
  * by calling: File(context.filesDir, "datastore/" + name + ".preferences_pb")
@@ -43,6 +46,8 @@
  * may be run more than once whether or not it already succeeded (potentially because another
  * migration failed or a write to disk failed.)
  * @param scope The scope in which IO operations and transform functions will execute.
+ *
+ * @return a new DataStore instance with the provided configuration
  */
 public fun Context.createDataStore(
     name: String,
diff --git a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt b/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
index b3274d2..b37786a 100644
--- a/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
+++ b/datastore/datastore/src/main/java/androidx/datastore/DataStoreFactory.kt
@@ -28,8 +28,11 @@
 import java.io.File
 
 /**
- * Create an instance of SingleProcessDataStore. The user is responsible for ensuring that
- * there is never more than one instance of SingleProcessDataStore acting on a file at a time.
+ * Create an instance of SingleProcessDataStore. Never create more than one instance of
+ * DataStore for a given file; doing so can break all DataStore functionality. You should
+ * consider managing your DataStore instance as a singleton. Note: this function is *not*
+ * comparable to [Context.getSharedPreferences]; this function creates and returns a
+ * new instance of DataStore each time it is called.
  *
  * @param fileName the filename relative to Context.filesDir that DataStore acts on. The File is
  * obtained by calling File(context.filesDir, fileName). No two instances of DataStore should
@@ -42,6 +45,8 @@
  * may be run more than once whether or not it already succeeded (potentially because another
  * migration failed or a write to disk failed.)
  * @param scope The scope in which IO operations and transform functions will execute.
+ *
+ * @return a new DataStore instance with the provided configuration
  */
 public fun <T> Context.createDataStore(
     fileName: String,
diff --git a/development/build_log_simplifier/messages.ignore b/development/build_log_simplifier/messages.ignore
index cf48ed2..537f909 100644
--- a/development/build_log_simplifier/messages.ignore
+++ b/development/build_log_simplifier/messages.ignore
@@ -653,4 +653,19 @@
 Attempt to define local of type int as it\$iv:java\.lang\.Object
 java\.lang\.Object androidx\.compose\.foundation\.gestures\.TapGestureDetectorKt\.waitForUpOrCancellation\(androidx\.compose\.ui\.input\.pointer\.HandlePointerInputScope, kotlin\.coroutines\.Continuation\)
 # > Task :hilt:integration-tests:hilt-testapp-viewmodel:kaptDebugKotlin
-warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
\ No newline at end of file
+warning: The following options were not recognized by any processor: '\[dagger\.fastInit, dagger\.hilt\.android\.internal\.disableAndroidSuperclassValidation, kapt\.kotlin\.generated\]'
+# > Task :preference:preference:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/preference\/preference\/src\/androidTest\/java\/androidx\/preference\/tests\/PreferenceDialogFragmentCompatTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTargetFragment\(Fragment\?\, Int\)\: Unit\' is deprecated\. Deprecated in Java
+# > Task :viewpager2:viewpager2:compileDebugAndroidTestKotlin
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/HostFragmentBackStackTest\.kt\: \([0-9]+\, [0-9]+\)\: \'enableDebugLogging\(Boolean\)\: Unit\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for systemWindowInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'getter for stableInsets\: Insets\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemWindowInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setSystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setStableInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setTappableElementInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'setMandatorySystemGestureInsets\(Insets\)\: WindowInsetsCompat\.Builder\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeSystemWindowInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: Unnecessary non\-null assertion \(\!\!\) on a non\-null receiver of type WindowInsetsCompat
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeStableInsets\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
+w\: \$SUPPORT\/viewpager[0-9]+\/viewpager[0-9]+\/src\/androidTest\/java\/androidx\/viewpager[0-9]+\/widget\/OnApplyWindowInsetsListenerTest\.kt\: \([0-9]+\, [0-9]+\)\: \'consumeDisplayCutout\(\)\: WindowInsetsCompat\' is deprecated\. Deprecated in Java
\ No newline at end of file
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index 5a145f5..3a97ddb 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -4,8 +4,8 @@
 }
 
 dependencies {
-    docs("androidx.activity:activity:1.2.0-beta01")
-    docs("androidx.activity:activity-ktx:1.2.0-beta01")
+    docs("androidx.activity:activity:1.2.0-beta02")
+    docs("androidx.activity:activity-ktx:1.2.0-beta02")
     docs("androidx.ads:ads-identifier:1.0.0-alpha04")
     docs("androidx.ads:ads-identifier-provider:1.0.0-alpha04")
     docs("androidx.annotation:annotation:1.2.0-alpha01")
@@ -19,8 +19,9 @@
     docs("androidx.autofill:autofill:1.1.0-rc01")
     docs("androidx.benchmark:benchmark-common:1.1.0-alpha01")
     docs("androidx.benchmark:benchmark-junit4:1.1.0-alpha01")
-    docs("androidx.biometric:biometric:1.1.0-rc01")
-    docs("androidx.browser:browser:1.3.0-rc01")
+    docs("androidx.biometric:biometric:1.2.0-alpha01")
+    docs("androidx.biometric:biometric-ktx:1.2.0-alpha01")
+    docs("androidx.browser:browser:1.3.0")
     docs("androidx.camera:camera-camera2:1.0.0-beta12")
     docs("androidx.camera:camera-core:1.0.0-beta12")
     docs("androidx.camera:camera-extensions:1.0.0-alpha19")
@@ -30,41 +31,44 @@
     docs("androidx.cardview:cardview:1.0.0")
     docs("androidx.collection:collection:1.1.0")
     docs("androidx.collection:collection-ktx:1.1.0")
-    docs("androidx.compose.animation:animation:1.0.0-alpha07")
-    docs("androidx.compose.animation:animation-core:1.0.0-alpha07")
-    samples("androidx.compose.animation:animation-samples:1.0.0-alpha07")
-    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha07")
-    docs("androidx.compose.foundation:foundation:1.0.0-alpha07")
-    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha07")
-    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha07")
-    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha07")
-    docs("androidx.compose.material:material:1.0.0-alpha07")
-    docs("androidx.compose.material:material-icons-core:1.0.0-alpha07")
-    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha07")
-    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha07")
-    samples("androidx.compose.material:material-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha07")
-    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha07")
-    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-text:1.0.0-alpha07")
+    docs("androidx.compose.animation:animation:1.0.0-alpha08")
+    docs("androidx.compose.animation:animation-core:1.0.0-alpha08")
+    samples("androidx.compose.animation:animation-samples:1.0.0-alpha08")
+    samples("androidx.compose.animation:animation-core-samples:1.0.0-alpha08")
+    docs("androidx.compose.foundation:foundation:1.0.0-alpha08")
+    docs("androidx.compose.foundation:foundation-layout:1.0.0-alpha08")
+    samples("androidx.compose.foundation:foundation-layout-samples:1.0.0-alpha08")
+    samples("androidx.compose.foundation:foundation-samples:1.0.0-alpha08")
+    docs("androidx.compose.material:material:1.0.0-alpha08")
+    docs("androidx.compose.material:material-icons-core:1.0.0-alpha08")
+    samples("androidx.compose.material:material-icons-core-samples:1.0.0-alpha08")
+    docs("androidx.compose.material:material-icons-extended:1.0.0-alpha08")
+    samples("androidx.compose.material:material-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-dispatch:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-livedata:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-livedata-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-rxjava2:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-rxjava2-samples:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-rxjava3:1.0.0-alpha08")
+    docs("androidx.compose.runtime:runtime-saved-instance-state:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-saved-instance-state-samples:1.0.0-alpha08")
+    samples("androidx.compose.runtime:runtime-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-geometry:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-graphics:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-graphics-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-test:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-test-junit4:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-text:1.0.0-alpha08")
     docs("androidx.compose.ui:ui-text-android:1.0.0-alpha06")
-    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-unit:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-util:1.0.0-alpha07")
-    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha07")
-    samples("androidx.compose.ui:ui-samples:1.0.0-alpha07")
+    samples("androidx.compose.ui:ui-text-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-unit:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-unit-samples:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-util:1.0.0-alpha08")
+    docs("androidx.compose.ui:ui-viewbinding:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-viewbinding-samples:1.0.0-alpha08")
+    samples("androidx.compose.ui:ui-samples:1.0.0-alpha08")
     docs("androidx.concurrent:concurrent-futures:1.1.0")
     docs("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     docs("androidx.contentpager:contentpager:1.0.0")
@@ -76,10 +80,10 @@
     docs("androidx.core:core-ktx:1.5.0-alpha05")
     docs("androidx.cursoradapter:cursoradapter:1.0.0")
     docs("androidx.customview:customview:1.1.0")
-    docs("androidx.datastore:datastore:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-core:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-preferences:1.0.0-alpha04")
-    docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha04")
+    docs("androidx.datastore:datastore:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-core:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-preferences:1.0.0-alpha05")
+    docs("androidx.datastore:datastore-preferences-core:1.0.0-alpha05")
     docs("androidx.documentfile:documentfile:1.0.0")
     docs("androidx.drawerlayout:drawerlayout:1.1.1")
     docs("androidx.dynamicanimation:dynamicanimation:1.1.0-alpha02")
@@ -87,9 +91,9 @@
     docs("androidx.emoji:emoji:1.2.0-alpha01")
     docs("androidx.emoji:emoji-appcompat:1.2.0-alpha01")
     docs("androidx.emoji:emoji-bundled:1.2.0-alpha01")
-    docs("androidx.enterprise:enterprise-feedback:1.1.0-beta01")
-    docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-beta01")
-    docs("androidx.exifinterface:exifinterface:1.3.1")
+    docs("androidx.enterprise:enterprise-feedback:1.1.0-rc01")
+    docs("androidx.enterprise:enterprise-feedback-testing:1.1.0-rc01")
+    docs("androidx.exifinterface:exifinterface:1.3.2")
     docs("androidx.fragment:fragment:1.3.0-beta01")
     docs("androidx.fragment:fragment-ktx:1.3.0-beta01")
     docs("androidx.fragment:fragment-testing:1.3.0-beta01")
@@ -99,10 +103,10 @@
     docs("androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02")
     docs("androidx.hilt:hilt-work:1.0.0-alpha02")
     docs("androidx.interpolator:interpolator:1.0.0")
-    docs("androidx.leanback:leanback:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-paging:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-preference:1.1.0-alpha05")
-    docs("androidx.leanback:leanback-tab:1.1.0-alpha05")
+    docs("androidx.leanback:leanback:1.1.0-beta01")
+    docs("androidx.leanback:leanback-paging:1.1.0-alpha06")
+    docs("androidx.leanback:leanback-preference:1.1.0-beta01")
+    docs("androidx.leanback:leanback-tab:1.1.0-beta01")
     docs("androidx.lifecycle:lifecycle-common:2.3.0-beta01")
     docs("androidx.lifecycle:lifecycle-common-java8:2.3.0-beta01")
     docs("androidx.lifecycle:lifecycle-extensions:2.2.0")
@@ -121,36 +125,36 @@
     docs("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
     docs("androidx.loader:loader:1.1.0")
     docs("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0-alpha01")
-    docs("androidx.media2:media2-common:1.1.0-rc01")
-    docs("androidx.media2:media2-player:1.1.0-rc01")
-    docs("androidx.media2:media2-session:1.1.0-rc01")
-    docs("androidx.media2:media2-widget:1.1.0-rc01")
-    docs("androidx.media:media:1.2.0")
+    docs("androidx.media2:media2-common:1.1.0")
+    docs("androidx.media2:media2-player:1.1.0")
+    docs("androidx.media2:media2-session:1.1.0")
+    docs("androidx.media2:media2-widget:1.1.0")
+    docs("androidx.media:media:1.2.1")
     docs("androidx.mediarouter:mediarouter:1.2.0")
-    docs("androidx.navigation:navigation-common:2.3.1")
-    docs("androidx.navigation:navigation-common-ktx:2.3.1")
-    docs("androidx.navigation:navigation-compose:1.0.0-alpha02")
+    docs("androidx.navigation:navigation-common:2.3.2")
+    docs("androidx.navigation:navigation-common-ktx:2.3.2")
+    docs("androidx.navigation:navigation-compose:1.0.0-alpha03")
     samples("androidx.navigation:navigation-compose-samples:1.0.0-alpha01")
-    docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.1")
-    docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.1")
-    docs("androidx.navigation:navigation-fragment:2.3.1")
-    docs("androidx.navigation:navigation-fragment-ktx:2.3.1")
-    docs("androidx.navigation:navigation-runtime:2.3.1")
-    docs("androidx.navigation:navigation-runtime-ktx:2.3.1")
-    docs("androidx.navigation:navigation-testing:2.3.1")
-    docs("androidx.navigation:navigation-ui:2.3.1")
-    docs("androidx.navigation:navigation-ui-ktx:2.3.1")
-    docs("androidx.paging:paging-common:3.0.0-alpha09")
-    docs("androidx.paging:paging-common-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-compose:1.0.0-alpha02")
+    docs("androidx.navigation:navigation-dynamic-features-fragment:2.3.2")
+    docs("androidx.navigation:navigation-dynamic-features-runtime:2.3.2")
+    docs("androidx.navigation:navigation-fragment:2.3.2")
+    docs("androidx.navigation:navigation-fragment-ktx:2.3.2")
+    docs("androidx.navigation:navigation-runtime:2.3.2")
+    docs("androidx.navigation:navigation-runtime-ktx:2.3.2")
+    docs("androidx.navigation:navigation-testing:2.3.2")
+    docs("androidx.navigation:navigation-ui:2.3.2")
+    docs("androidx.navigation:navigation-ui-ktx:2.3.2")
+    docs("androidx.paging:paging-common:3.0.0-alpha10")
+    docs("androidx.paging:paging-common-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-compose:1.0.0-alpha03")
     samples("androidx.paging:paging-compose-samples:3.0.0-alpha08")
-    docs("androidx.paging:paging-guava:3.0.0-alpha09")
-    docs("androidx.paging:paging-runtime:3.0.0-alpha09")
-    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava2:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha09")
-    docs("androidx.paging:paging-rxjava3:3.0.0-alpha09")
-    samples("androidx.paging:paging-samples:3.0.0-alpha09")
+    docs("androidx.paging:paging-guava:3.0.0-alpha10")
+    docs("androidx.paging:paging-runtime:3.0.0-alpha10")
+    docs("androidx.paging:paging-runtime-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava2:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava2-ktx:3.0.0-alpha10")
+    docs("androidx.paging:paging-rxjava3:3.0.0-alpha10")
+    samples("androidx.paging:paging-samples:3.0.0-alpha10")
     docs("androidx.palette:palette:1.0.0")
     docs("androidx.palette:palette-ktx:1.0.0")
     docs("androidx.percentlayout:percentlayout:1.0.1")
@@ -158,7 +162,7 @@
     docs("androidx.preference:preference-ktx:1.1.1")
     docs("androidx.print:print:1.1.0-beta01")
     docs("androidx.recommendation:recommendation:1.0.0")
-    docs("androidx.recyclerview:recyclerview:1.2.0-alpha06")
+    docs("androidx.recyclerview:recyclerview:1.2.0-beta01")
     docs("androidx.recyclerview:recyclerview-selection:2.0.0-alpha01")
     docs("androidx.remotecallback:remotecallback:1.0.0-alpha02")
     docs("androidx.room:room-common:2.3.0-alpha03")
@@ -170,10 +174,10 @@
     docs("androidx.room:room-testing:2.3.0-alpha03")
     docs("androidx.savedstate:savedstate:1.1.0-beta01")
     docs("androidx.savedstate:savedstate-ktx:1.1.0-beta01")
-    docs("androidx.security:security-crypto:1.1.0-alpha02")
+    docs("androidx.security:security-crypto:1.1.0-alpha03")
     docs("androidx.security:security-crypto-ktx:1.1.0-alpha02")
     docs("androidx.security:security-identity-credential:1.0.0-alpha01")
-    docs("androidx.sharetarget:sharetarget:1.1.0-beta01")
+    docs("androidx.sharetarget:sharetarget:1.1.0-rc01")
     docs("androidx.slice:slice-builders:1.1.0-alpha01")
     docs("androidx.slice:slice-builders-ktx:1.0.0-alpha07")
     docs("androidx.slice:slice-core:1.1.0-alpha01")
@@ -188,7 +192,8 @@
     docs("androidx.textclassifier:textclassifier:1.0.0-alpha03")
     docs("androidx.tracing:tracing:1.0.0")
     docs("androidx.tracing:tracing-ktx:1.0.0")
-    docs("androidx.transition:transition:1.4.0-beta01")
+    docs("androidx.transition:transition:1.4.0-rc01")
+    docs("androidx.transition:transition-ktx:1.4.0-rc01")
     docs("androidx.tvprovider:tvprovider:1.1.0-alpha01")
     docs("androidx.vectordrawable:vectordrawable:1.2.0-alpha02")
     docs("androidx.vectordrawable:vectordrawable-animated:1.2.0-alpha01")
@@ -196,27 +201,27 @@
     docs("androidx.versionedparcelable:versionedparcelable:1.1.1")
     docs("androidx.viewpager2:viewpager2:1.1.0-alpha01")
     docs("androidx.viewpager:viewpager:1.0.0")
-    docs("androidx.wear:wear:1.2.0-alpha02")
+    docs("androidx.wear:wear:1.2.0-alpha03")
     stubs(fileTree(dir: '../wear/wear_stubs/', include: ['com.google.android.wearable-stubs.jar']))
-    docs("androidx.wear:wear-complications-data:1.0.0-alpha02")
-    docs("androidx.wear:wear-complications-provider:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-client:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-data:1.0.0-alpha02")
+    docs("androidx.wear:wear-complications-data:1.0.0-alpha03")
+    docs("androidx.wear:wear-complications-provider:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-client:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-complications-rendering:1.0.0-alpha03")
+    docs("androidx.wear:wear-watchface-data:1.0.0-alpha03")
     samples("androidx.wear:wear-watchface-samples:1.0.0-alpha02")
-    docs("androidx.wear:wear-watchface-style:1.0.0-alpha02")
-    docs("androidx.wear:wear-input:1.0.0-rc01")
-    docs("androidx.wear:wear-input-testing:1.0.0-rc01")
-    docs("androidx.webkit:webkit:1.4.0-rc01")
+    docs("androidx.wear:wear-watchface-style:1.0.0-alpha03")
+    docs("androidx.wear:wear-input:1.0.0")
+    docs("androidx.wear:wear-input-testing:1.0.0")
+    docs("androidx.webkit:webkit:1.4.0-rc02")
     docs("androidx.window:window:1.0.0-alpha01")
     stubs(fileTree(dir: '../window/stubs/', include: ['window-sidecar-release-0.1.0-alpha01.aar']))
     stubs(project(":window:window-extensions"))
-    docs("androidx.work:work-gcm:2.5.0-beta01")
-    docs("androidx.work:work-multiprocess:2.5.0-beta01")
-    docs("androidx.work:work-runtime:2.5.0-beta01")
-    docs("androidx.work:work-runtime-ktx:2.5.0-beta01")
-    docs("androidx.work:work-rxjava2:2.5.0-beta01")
-    docs("androidx.work:work-rxjava3:2.5.0-beta01")
-    docs("androidx.work:work-testing:2.5.0-beta01")
+    docs("androidx.work:work-gcm:2.5.0-beta02")
+    docs("androidx.work:work-multiprocess:2.5.0-beta02")
+    docs("androidx.work:work-runtime:2.5.0-beta02")
+    docs("androidx.work:work-runtime-ktx:2.5.0-beta02")
+    docs("androidx.work:work-rxjava2:2.5.0-beta02")
+    docs("androidx.work:work-rxjava3:2.5.0-beta02")
+    docs("androidx.work:work-testing:2.5.0-beta02")
 }
diff --git a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
index 5899803..77b8b66 100644
--- a/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
+++ b/exifinterface/exifinterface/src/androidTest/java/androidx/exifinterface/media/ExifInterfaceTest.java
@@ -70,6 +70,8 @@
 /**
  * Test {@link ExifInterface}.
  */
+// TODO: Add NEF test file from CTS after reducing file size in order to test uncompressed thumbnail
+// image.
 @RunWith(AndroidJUnit4.class)
 public class ExifInterfaceTest {
     private static final String TAG = ExifInterface.class.getSimpleName();
@@ -97,19 +99,20 @@
             "jpeg_with_datetime_tag_primary_format.jpg";
     private static final String JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT =
             "jpeg_with_datetime_tag_secondary_format.jpg";
+    private static final String HEIC_WITH_EXIF = "heic_with_exif.heic";
     private static final int[] IMAGE_RESOURCES = new int[] {
             R.raw.jpeg_with_exif_byte_order_ii, R.raw.jpeg_with_exif_byte_order_mm,
             R.raw.dng_with_exif_with_xmp, R.raw.jpeg_with_exif_with_xmp,
             R.raw.png_with_exif_byte_order_ii, R.raw.png_without_exif, R.raw.webp_with_exif,
             R.raw.webp_with_anim_without_exif, R.raw.webp_without_exif,
             R.raw.webp_lossless_without_exif, R.raw.jpeg_with_datetime_tag_primary_format,
-            R.raw.jpeg_with_datetime_tag_secondary_format};
+            R.raw.jpeg_with_datetime_tag_secondary_format, R.raw.heic_with_exif};
     private static final String[] IMAGE_FILENAMES = new String[] {
             JPEG_WITH_EXIF_BYTE_ORDER_II, JPEG_WITH_EXIF_BYTE_ORDER_MM, DNG_WITH_EXIF_WITH_XMP,
             JPEG_WITH_EXIF_WITH_XMP, PNG_WITH_EXIF_BYTE_ORDER_II, PNG_WITHOUT_EXIF,
             WEBP_WITH_EXIF, WEBP_WITHOUT_EXIF_WITH_ANIM_DATA, WEBP_WITHOUT_EXIF,
             WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING, JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT,
-            JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT};
+            JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT, HEIC_WITH_EXIF};
 
     private static final int USER_READ_WRITE = 0600;
     private static final String TEST_TEMP_FILE_NAME = "testImage";
@@ -455,6 +458,19 @@
         writeToFilesWithoutExif(WEBP_WITHOUT_EXIF_WITH_LOSSLESS_ENCODING);
     }
 
+    /**
+     * .heic file is a container for HEIF format images, which ExifInterface supports.
+     */
+    @Test
+    @LargeTest
+    public void testHeicFile() throws Throwable {
+        // TODO: Reading HEIC file for SDK < 28 throws an exception. Revisit once issue is solved.
+        //  (b/172025296)
+        if (Build.VERSION.SDK_INT > 27) {
+            readFromFilesWithExif(HEIC_WITH_EXIF, R.array.heic_with_exif);
+        }
+    }
+
     @Test
     @LargeTest
     public void testDoNotFailOnCorruptedImage() throws Throwable {
@@ -628,22 +644,32 @@
         }
     }
 
+    /**
+     * JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT contains the following tags:
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+     *   TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
+     */
     @Test
     @SmallTest
     public void testGetSetDateTime() throws IOException {
-        final long expectedDatetimeValue = 1454059947000L;
-        final long expectedDatetimeOffsetLongValue = 32400000L; /* +09:00 converted to msec */
+        final long expectedGetDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        // GPS datetime does not support subsec precision
+        final long expectedGetGpsDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
         final String expectedDatetimeOffsetStringValue = "+09:00";
-        final String dateTimeValue = "2017:02:02 22:22:22";
-        final String dateTimeOriginalValue = "2017:01:01 11:11:11";
 
         File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTime());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTimeOriginal());
-        assertEquals(expectedDatetimeValue, (long) exif.getDateTimeDigitized());
-        assertEquals(expectedDatetimeValue, (long) exif.getGpsDateTime());
-        // getDateTime() = TAG_DATETIME + TAG_OFFSET_TIME
+        // Test getting datetime values
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeOriginal());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTimeDigitized());
+        assertEquals(expectedGetGpsDatetimeValue, (long) exif.getGpsDateTime());
         assertEquals(expectedDatetimeOffsetStringValue,
                 exif.getAttribute(ExifInterface.TAG_OFFSET_TIME));
         assertEquals(expectedDatetimeOffsetStringValue,
@@ -651,24 +677,9 @@
         assertEquals(expectedDatetimeOffsetStringValue,
                 exif.getAttribute(ExifInterface.TAG_OFFSET_TIME_DIGITIZED));
 
-        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
-        exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
-        exif.saveAttributes();
-
-        // Check that the DATETIME value is not overwritten by DATETIME_ORIGINAL's value.
-        exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
-
-        // Now remove the DATETIME value.
-        exif.setAttribute(ExifInterface.TAG_DATETIME, null);
-        exif.saveAttributes();
-
-        // When the DATETIME has no value, then it should be set to DATETIME_ORIGINAL's value.
-        exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
-
-        long currentTimeStamp = System.currentTimeMillis();
+        // Test setting datetime values
+        final long currentTimeStamp = System.currentTimeMillis();
+        final long expectedDatetimeOffsetLongValue = 32400000L;
         exif.setDateTime(currentTimeStamp);
         exif.saveAttributes();
         exif = new ExifInterface(imageFile.getAbsolutePath());
@@ -685,30 +696,90 @@
      * Setting a datetime tag value with the secondary format with
      * {@link ExifInterface#setAttribute(String, String)} should automatically convert it to the
      * primary format.
+     *
+     * JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT contains the following tags:
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "2016:01:29 18:32:27"
+     *   TAG_OFFSET_TIME, TAG_OFFSET_TIME_ORIGINAL, TAG_OFFSET_TIME_DIGITIZED = "100000"
+     *   TAG_DATETIME, TAG_DATETIME_ORIGINAL, TAG_DATETIME_DIGITIZED = "+09:00"
      */
     @Test
     @SmallTest
     public void testGetSetDateTimeForSecondaryFormat() throws Exception {
-        final long dateTimePrimaryFormatLongValue = 1604075491000L;
-        final String dateTimePrimaryFormatStringValue = "2020-10-30 16:31:31";
+        // Test getting datetime values
+        final long expectedGetDatetimeValue =
+                1454027547000L /* TAG_DATETIME value ("2016:01:29 18:32:27") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        final String expectedDateTimeStringValue = "2016-01-29 18:32:27";
+
         File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_SECONDARY_FORMAT);
-
-        // Check that secondary format value is read correctly.
         ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
-        assertEquals(dateTimePrimaryFormatStringValue,
+        assertEquals(expectedDateTimeStringValue,
                 exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimePrimaryFormatLongValue, (long) exif.getDateTime());
+        assertEquals(expectedGetDatetimeValue, (long) exif.getDateTime());
 
-        final long dateTimeSecondaryFormatLongValue = 1577836800000L;
-        final String dateTimeSecondaryFormatStringValue = "2020-01-01 00:00:00";
-        final String modifiedDateTimeSecondaryFormatStringValue = "2020:01:01 00:00:00";
+        // Test setting datetime value: check that secondary format value is modified correctly
+        // when it is saved.
+        final long newDateTimeLongValue =
+                1577772000000L /* TAG_DATETIME value ("2020-01-01 00:00:00") converted to msec */
+                + 100L /* TAG_SUBSEC_TIME value ("100000") converted to msec */
+                + 32400000L /* TAG_OFFSET_TIME value ("+09:00") converted to msec */;
+        final String newDateTimeStringValue = "2020-01-01 00:00:00";
+        final String modifiedNewDateTimeStringValue = "2020:01:01 00:00:00";
 
-        // Check that secondary format value is written correctly.
-        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeSecondaryFormatStringValue);
+        exif.setAttribute(ExifInterface.TAG_DATETIME, newDateTimeStringValue);
         exif.saveAttributes();
-        assertEquals(modifiedDateTimeSecondaryFormatStringValue,
-                exif.getAttribute(ExifInterface.TAG_DATETIME));
-        assertEquals(dateTimeSecondaryFormatLongValue, (long) exif.getDateTime());
+        assertEquals(modifiedNewDateTimeStringValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(newDateTimeLongValue, (long) exif.getDateTime());
+    }
+
+    @Test
+    @LargeTest
+    public void testAddDefaultValuesForCompatibility() throws Exception {
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+
+        // 1. Check that the TAG_DATETIME value is not overwritten by TAG_DATETIME_ORIGINAL's value
+        // when TAG_DATETIME value exists.
+        final String dateTimeValue = "2017:02:02 22:22:22";
+        final String dateTimeOriginalValue = "2017:01:01 11:11:11";
+        exif.setAttribute(ExifInterface.TAG_DATETIME, dateTimeValue);
+        exif.setAttribute(ExifInterface.TAG_DATETIME_ORIGINAL, dateTimeOriginalValue);
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertEquals(dateTimeValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL));
+
+        // 2. Check that when TAG_DATETIME has no value, it is set to TAG_DATETIME_ORIGINAL's value.
+        exif.setAttribute(ExifInterface.TAG_DATETIME, null);
+        exif.saveAttributes();
+        exif = new ExifInterface(imageFile.getAbsolutePath());
+        assertEquals(dateTimeOriginalValue, exif.getAttribute(ExifInterface.TAG_DATETIME));
+    }
+
+    // TODO: Add tests for other variations (e.g. single/double digit number strings)
+    @Test
+    @LargeTest
+    public void testParsingSubsec() throws IOException {
+        File imageFile = getFileFromExternalDir(JPEG_WITH_DATETIME_TAG_PRIMARY_FORMAT);
+        ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 0ms */ "000000");
+        exif.saveAttributes();
+        long currentDateTimeValue = exif.getDateTime();
+
+        // Check that TAG_SUBSEC_TIME values starting with zero are supported.
+        // Note: getDateTime() supports only up to 1/1000th of a second.
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 1ms */ "001000");
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 1, (long) exif.getDateTime());
+
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 10ms */ "010000");
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 10, (long) exif.getDateTime());
+
+        exif.setAttribute(ExifInterface.TAG_SUBSEC_TIME, /* 100ms */ "100000");
+        exif.saveAttributes();
+        assertEquals(currentDateTimeValue + 100, (long) exif.getDateTime());
     }
 
     @Test
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
new file mode 100644
index 0000000..ec03bfd
--- /dev/null
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/heic_with_exif.heic
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
index f50e845..5930e2c 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_primary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
index 53ad4e9..9899556 100644
--- a/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
+++ b/exifinterface/exifinterface/src/androidTest/res/raw/jpeg_with_datetime_tag_secondary_format.jpg
Binary files differ
diff --git a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
index e6f342e..b3438c9 100644
--- a/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
+++ b/exifinterface/exifinterface/src/androidTest/res/values/arrays.xml
@@ -367,4 +367,48 @@
         <item>0</item>
         <item>0</item>
     </array>
+    <array name="heic_with_exif">
+        <!--Whether thumbnail exists-->
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0</item>
+        <item>false</item>
+        <!--Whether GPS LatLong information exists-->
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <item>0.0</item>
+        <!--Whether Make information exists-->
+        <item>true</item>
+        <item>3519</item>
+        <item>4</item>
+        <item>LGE</item>
+        <item>Nexus 5</item>
+        <item>0.0</item>
+        <item />
+        <item>0.0</item>
+        <item>0</item>
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item />
+        <item>1080</item>
+        <item>1920</item>
+        <item />
+        <item>1</item>
+        <item>0</item>
+        <item>false</item>
+        <item>0</item>
+        <item>0</item>
+    </array>
 </resources>
\ No newline at end of file
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
index c0347c4..76d6f81 100644
--- a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterface.java
@@ -16,6 +16,13 @@
 
 package androidx.exifinterface.media;
 
+import static androidx.exifinterface.media.ExifInterfaceUtils.byteArrayToHexString;
+import static androidx.exifinterface.media.ExifInterfaceUtils.convertToLongArray;
+import static androidx.exifinterface.media.ExifInterfaceUtils.copy;
+import static androidx.exifinterface.media.ExifInterfaceUtils.isSupportedFormatForSavingAttributes;
+import static androidx.exifinterface.media.ExifInterfaceUtils.parseSubSeconds;
+import static androidx.exifinterface.media.ExifInterfaceUtils.startsWith;
+
 import android.annotation.SuppressLint;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
@@ -3835,21 +3842,21 @@
     static final byte MARKER_EOI = (byte) 0xd9;
 
     // Supported Image File Types
-    private static final int IMAGE_TYPE_UNKNOWN = 0;
-    private static final int IMAGE_TYPE_ARW = 1;
-    private static final int IMAGE_TYPE_CR2 = 2;
-    private static final int IMAGE_TYPE_DNG = 3;
-    private static final int IMAGE_TYPE_JPEG = 4;
-    private static final int IMAGE_TYPE_NEF = 5;
-    private static final int IMAGE_TYPE_NRW = 6;
-    private static final int IMAGE_TYPE_ORF = 7;
-    private static final int IMAGE_TYPE_PEF = 8;
-    private static final int IMAGE_TYPE_RAF = 9;
-    private static final int IMAGE_TYPE_RW2 = 10;
-    private static final int IMAGE_TYPE_SRW = 11;
-    private static final int IMAGE_TYPE_HEIF = 12;
-    private static final int IMAGE_TYPE_PNG = 13;
-    private static final int IMAGE_TYPE_WEBP = 14;
+    static final int IMAGE_TYPE_UNKNOWN = 0;
+    static final int IMAGE_TYPE_ARW = 1;
+    static final int IMAGE_TYPE_CR2 = 2;
+    static final int IMAGE_TYPE_DNG = 3;
+    static final int IMAGE_TYPE_JPEG = 4;
+    static final int IMAGE_TYPE_NEF = 5;
+    static final int IMAGE_TYPE_NRW = 6;
+    static final int IMAGE_TYPE_ORF = 7;
+    static final int IMAGE_TYPE_PEF = 8;
+    static final int IMAGE_TYPE_RAF = 9;
+    static final int IMAGE_TYPE_RW2 = 10;
+    static final int IMAGE_TYPE_SRW = 11;
+    static final int IMAGE_TYPE_HEIF = 12;
+    static final int IMAGE_TYPE_PNG = 13;
+    static final int IMAGE_TYPE_WEBP = 14;
 
     static {
         sFormatterPrimary = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.US);
@@ -4694,7 +4701,7 @@
      * </p>
      */
     public void saveAttributes() throws IOException {
-        if (!isSupportedFormatForSavingAttributes()) {
+        if (!isSupportedFormatForSavingAttributes(mMimeType)) {
             throw new IOException("ExifInterface only supports saving attributes on JPEG, PNG, "
                     + "or WebP formats.");
         }
@@ -5152,6 +5159,10 @@
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME} value as number of milliseconds since
      * Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME}.
+     *
      * @return null if date time information is unavailable or invalid.
      *
      * @hide
@@ -5167,6 +5178,10 @@
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME_DIGITIZED} value as number of
      * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_DIGITIZED}.
+     *
      * @return null if digitized date time information is unavailable or invalid.
      *
      * @hide
@@ -5182,6 +5197,10 @@
     /**
      * Returns parsed {@link ExifInterface#TAG_DATETIME_ORIGINAL} value as number of
      * milliseconds since Jan. 1, 1970, midnight local time.
+     *
+     * <p>Note: The return value includes the first three digits (or less depending on the length
+     * of the string) of {@link ExifInterface#TAG_SUBSEC_TIME_ORIGINAL}.
+     *
      * @return null if original date time information is unavailable or invalid.
      *
      * @hide
@@ -5224,15 +5243,7 @@
             }
 
             if (subSecs != null) {
-                try {
-                    long sub = Long.parseLong(subSecs);
-                    while (sub > 1000) {
-                        sub /= 10;
-                    }
-                    msecs += sub;
-                } catch (NumberFormatException e) {
-                    // Ignored
-                }
+                msecs += parseSubSeconds(subSecs);
             }
             return msecs;
         } catch (IllegalArgumentException e) {
@@ -8073,87 +8084,4 @@
             Log.e(TAG, "closeFileDescriptor is called in API < 21, which must be wrong.");
         }
     }
-
-    /**
-     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
-     * Returns the total number of bytes transferred.
-     */
-    private static int copy(InputStream in, OutputStream out) throws IOException {
-        int total = 0;
-        byte[] buffer = new byte[8192];
-        int c;
-        while ((c = in.read(buffer)) != -1) {
-            total += c;
-            out.write(buffer, 0, c);
-        }
-        return total;
-    }
-
-    /**
-     * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
-     * closed.
-     */
-    private static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
-        int remainder = numBytes;
-        byte[] buffer = new byte[8192];
-        while (remainder > 0) {
-            int bytesToRead = Math.min(remainder, 8192);
-            int bytesRead = in.read(buffer, 0, bytesToRead);
-            if (bytesRead != bytesToRead) {
-                throw new IOException("Failed to copy the given amount of bytes from the input"
-                        + "stream to the output stream.");
-            }
-            remainder -= bytesRead;
-            out.write(buffer, 0, bytesRead);
-        }
-    }
-
-    /**
-     * Convert given int[] to long[]. If long[] is given, just return it.
-     * Return null for other types of input.
-     */
-    private static long[] convertToLongArray(Object inputObj) {
-        if (inputObj instanceof int[]) {
-            int[] input = (int[]) inputObj;
-            long[] result = new long[input.length];
-            for (int i = 0; i < input.length; i++) {
-                result[i] = input[i];
-            }
-            return result;
-        } else if (inputObj instanceof long[]) {
-            return (long[]) inputObj;
-        }
-        return null;
-    }
-
-    private static boolean startsWith(byte[] cur, byte[] val) {
-        if (cur == null || val == null) {
-            return false;
-        }
-        if (cur.length < val.length) {
-            return false;
-        }
-        for (int i = 0; i < val.length; i++) {
-            if (cur[i] != val[i]) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static String byteArrayToHexString(byte[] bytes) {
-        StringBuilder sb = new StringBuilder(bytes.length * 2);
-        for (int i = 0; i < bytes.length; i++) {
-            sb.append(String.format("%02x", bytes[i]));
-        }
-        return sb.toString();
-    }
-
-    private boolean isSupportedFormatForSavingAttributes() {
-        if (mMimeType == IMAGE_TYPE_JPEG || mMimeType == IMAGE_TYPE_PNG
-                || mMimeType == IMAGE_TYPE_WEBP) {
-            return true;
-        }
-        return false;
-    }
 }
diff --git a/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
new file mode 100644
index 0000000..8c3390d
--- /dev/null
+++ b/exifinterface/exifinterface/src/main/java/androidx/exifinterface/media/ExifInterfaceUtils.java
@@ -0,0 +1,129 @@
+/*
+ * 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.exifinterface.media;
+
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_JPEG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_PNG;
+import static androidx.exifinterface.media.ExifInterface.IMAGE_TYPE_WEBP;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+class ExifInterfaceUtils {
+    private static final String TAG = "ExifInterfaceUtils";
+
+    private ExifInterfaceUtils() {
+        // Prevent instantiation
+    }
+    /**
+     * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
+     * Returns the total number of bytes transferred.
+     */
+    static int copy(InputStream in, OutputStream out) throws IOException {
+        int total = 0;
+        byte[] buffer = new byte[8192];
+        int c;
+        while ((c = in.read(buffer)) != -1) {
+            total += c;
+            out.write(buffer, 0, c);
+        }
+        return total;
+    }
+
+    /**
+     * Copies the given number of the bytes from {@code in} to {@code out}. Neither stream is
+     * closed.
+     */
+    static void copy(InputStream in, OutputStream out, int numBytes) throws IOException {
+        int remainder = numBytes;
+        byte[] buffer = new byte[8192];
+        while (remainder > 0) {
+            int bytesToRead = Math.min(remainder, 8192);
+            int bytesRead = in.read(buffer, 0, bytesToRead);
+            if (bytesRead != bytesToRead) {
+                throw new IOException("Failed to copy the given amount of bytes from the input"
+                        + "stream to the output stream.");
+            }
+            remainder -= bytesRead;
+            out.write(buffer, 0, bytesRead);
+        }
+    }
+
+    /**
+     * Convert given int[] to long[]. If long[] is given, just return it.
+     * Return null for other types of input.
+     */
+    static long[] convertToLongArray(Object inputObj) {
+        if (inputObj instanceof int[]) {
+            int[] input = (int[]) inputObj;
+            long[] result = new long[input.length];
+            for (int i = 0; i < input.length; i++) {
+                result[i] = input[i];
+            }
+            return result;
+        } else if (inputObj instanceof long[]) {
+            return (long[]) inputObj;
+        }
+        return null;
+    }
+
+    static boolean startsWith(byte[] cur, byte[] val) {
+        if (cur == null || val == null) {
+            return false;
+        }
+        if (cur.length < val.length) {
+            return false;
+        }
+        for (int i = 0; i < val.length; i++) {
+            if (cur[i] != val[i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean isSupportedFormatForSavingAttributes(int mimeType) {
+        if (mimeType == IMAGE_TYPE_JPEG || mimeType == IMAGE_TYPE_PNG
+                || mimeType == IMAGE_TYPE_WEBP) {
+            return true;
+        }
+        return false;
+    }
+
+    static String byteArrayToHexString(byte[] bytes) {
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+        for (int i = 0; i < bytes.length; i++) {
+            sb.append(String.format("%02x", bytes[i]));
+        }
+        return sb.toString();
+    }
+
+    static long parseSubSeconds(String subSec) {
+        try {
+            final int len = Math.min(subSec.length(), 3);
+            long sub = Long.parseLong(subSec.substring(0, len));
+            for (int i = len; i < 3; i++) {
+                sub *= 10;
+            }
+            return sub;
+        } catch (NumberFormatException e) {
+            // Ignored
+        }
+        return 0L;
+    }
+}
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 6cfa8e7..88435eb 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -36,11 +36,11 @@
     api("androidx.collection:collection-ktx:1.1.0") {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.3.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-livedata-core-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01")
-    api("androidx.savedstate:savedstate-ktx:1.1.0-beta01") {
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
+    api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
     api(KOTLIN_STDLIB)
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 961ddb2..a6c2996 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -38,10 +38,10 @@
     api("androidx.viewpager:viewpager:1.0.0")
     api("androidx.loader:loader:1.0.0")
     api(projectOrArtifact(":activity:activity"))
-    api("androidx.lifecycle:lifecycle-livedata-core:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.0-beta01")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.0-beta01")
-    api("androidx.savedstate:savedstate:1.1.0-beta01")
+    api(projectOrArtifact(":lifecycle:lifecycle-livedata-core"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
+    api(projectOrArtifact(":savedstate:savedstate"))
     api("androidx.annotation:annotation-experimental:1.0.0")
 
     androidTestImplementation("androidx.appcompat:appcompat:1.1.0", {
diff --git a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
index 167669e..ab97f41 100644
--- a/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
+++ b/jetifier/jetifier/preprocessor/src/main/kotlin/com/android/tools/build/jetifier/preprocessor/Main.kt
@@ -42,7 +42,7 @@
             isRequired = false
         )
 
-        private fun createOption(
+        internal fun createOption(
             argName: String,
             desc: String,
             isRequired: Boolean = true,
@@ -94,4 +94,4 @@
 
 fun main(args: Array<String>) {
     Main().run(args)
-}
\ No newline at end of file
+}
diff --git a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
index 587c1ce..9e7ec3d 100644
--- a/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
+++ b/jetifier/jetifier/standalone/src/main/kotlin/com/android/tools/build/jetifier/standalone/Main.kt
@@ -102,7 +102,7 @@
             isRequired = false
         )
 
-        private fun createOption(
+        internal fun createOption(
             argName: String,
             argNameLong: String,
             desc: String,
diff --git a/leanback/leanback/src/androidTest/AndroidManifest.xml b/leanback/leanback/src/androidTest/AndroidManifest.xml
index 2534e63..41194c1 100644
--- a/leanback/leanback/src/androidTest/AndroidManifest.xml
+++ b/leanback/leanback/src/androidTest/AndroidManifest.xml
@@ -66,6 +66,11 @@
         <activity android:name="androidx.leanback.app.TestActivity"
                   android:exported="true"
                   android:theme="@style/Theme.Leanback"/>
+
+        <activity android:name="androidx.leanback.app.TestActivity$TestActivity2"
+            android:exported="true"
+            android:theme="@style/Theme.Leanback"/>
+
     </application>
 
 </manifest>
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
index 7ae71d7..00c7aca 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BackgroundManagerTest.java
@@ -46,8 +46,11 @@
     public TestName mUnitTestName = new TestName();
 
     @Rule
-    public final TestActivity.TestActivityTestRule mRule = new TestActivity.TestActivityTestRule(
-            generateProviderName("activity1"));
+    public final TestActivity.TestActivityTestRule mRule = new TestActivity.TestActivityTestRule();
+
+    @Rule
+    public final TestActivity.TestActivityTestRule2 mRule2 =
+            new TestActivity.TestActivityTestRule2();
 
     String generateProviderName(String name) {
         return mUnitTestName.getMethodName() + "_" + name;
@@ -159,7 +162,7 @@
         assertIsColorDrawable(manager, color);
     }
 
-    void assertIsColorDrawable(BackgroundManager manager, int color) {
+    static void assertIsColorDrawable(BackgroundManager manager, int color) {
         assertNull(manager.mLayerDrawable.mWrapper[0]);
         assertTrue(manager.mLayerDrawable.getDrawable(0)
                 instanceof BackgroundManager.EmptyDrawable);
@@ -168,7 +171,7 @@
         assertNull(manager.mBackgroundDrawable);
     }
 
-    void assertIsBitmapDrawable(BackgroundManager manager, Bitmap bitmap) {
+    static void assertIsBitmapDrawable(BackgroundManager manager, Bitmap bitmap) {
         assertNull(manager.mLayerDrawable.mWrapper[0]);
         assertTrue(manager.mLayerDrawable.getDrawable(0)
                 instanceof BackgroundManager.EmptyDrawable);
@@ -179,7 +182,7 @@
                 .mState.mBitmap, bitmap);
     }
 
-    void assertIsDrawable(BackgroundManager manager, Drawable drawable) {
+    static void assertIsDrawable(BackgroundManager manager, Drawable drawable) {
         assertNull(manager.mLayerDrawable.mWrapper[0]);
         assertTrue(manager.mLayerDrawable.getDrawable(0)
                 instanceof BackgroundManager.EmptyDrawable);
@@ -188,7 +191,7 @@
         assertSame(manager.mBackgroundDrawable, drawable);
     }
 
-    Bitmap createBitmap(int width, int height, int color) {
+    static Bitmap createBitmap(int width, int height, int color) {
         final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
         Canvas canvas = new Canvas(bitmap);
         Paint paint = new Paint();
@@ -227,20 +230,21 @@
         setBitmapAndVerify(manager, createBitmap(200, 100, Color.MAGENTA));
     }
 
+    public static class EstablishInOnAttachToWindow extends TestActivity.Provider {
+        @Override
+        public void onAttachedToWindow(TestActivity activity) {
+            BackgroundManager.getInstance(activity).attach(activity.getWindow());
+        }
+
+        @Override
+        public void onStart(TestActivity activity) {
+            BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+        }
+    }
+
     @Test
     public void establishInOnAttachToWindow() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onAttachedToWindow(TestActivity activity) {
-                BackgroundManager.getInstance(activity).attach(activity.getWindow());
-            }
-
-            @Override
-            public void onStart(TestActivity activity) {
-                BackgroundManager.getInstance(activity).setColor(Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(EstablishInOnAttachToWindow.class);
 
         BackgroundManager manager = BackgroundManager.getInstance(activity1);
         waitForBackgroundAnimationFinish(manager);
@@ -249,20 +253,21 @@
         testSwitchBackgrounds(manager);
     }
 
+    public static class MultipleSetBitmaps extends TestActivity.Provider {
+        @Override
+        public void onAttachedToWindow(TestActivity activity) {
+            BackgroundManager.getInstance(activity).attach(activity.getWindow());
+        }
+
+        @Override
+        public void onStart(TestActivity activity) {
+            BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+        }
+    }
+
     @Test
     public void multipleSetBitmaps() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onAttachedToWindow(TestActivity activity) {
-                BackgroundManager.getInstance(activity).attach(activity.getWindow());
-            }
-
-            @Override
-            public void onStart(TestActivity activity) {
-                BackgroundManager.getInstance(activity).setColor(Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(MultipleSetBitmaps.class);
 
         final BackgroundManager manager = BackgroundManager.getInstance(activity1);
         waitForBackgroundAnimationFinish(manager);
@@ -285,20 +290,21 @@
         assertIsBitmapDrawable(manager, bitmap4);
     }
 
+    public static class MultipleSetBitmapsAndColor extends TestActivity.Provider {
+        @Override
+        public void onAttachedToWindow(TestActivity activity) {
+            BackgroundManager.getInstance(activity).attach(activity.getWindow());
+        }
+
+        @Override
+        public void onStart(TestActivity activity) {
+            BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+        }
+    }
+
     @Test
     public void multipleSetBitmapsAndColor() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onAttachedToWindow(TestActivity activity) {
-                BackgroundManager.getInstance(activity).attach(activity.getWindow());
-            }
-
-            @Override
-            public void onStart(TestActivity activity) {
-                BackgroundManager.getInstance(activity).setColor(Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(MultipleSetBitmapsAndColor.class);
 
         final BackgroundManager manager = BackgroundManager.getInstance(activity1);
         waitForBackgroundAnimationFinish(manager);
@@ -326,21 +332,22 @@
         assertIsColorDrawable(manager, color);
     }
 
+    public static class EstablishInOnCreate extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager.getInstance(activity).attach(activity.getWindow());
+        }
+
+        @Override
+        public void onStart(TestActivity activity) {
+            BackgroundManager.getInstance(activity).setColor(Color.BLUE);
+        }
+    }
+
     @Test
     public void establishInOnCreate() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager.getInstance(activity).attach(activity.getWindow());
-            }
-
-            @Override
-            public void onStart(TestActivity activity) {
-                BackgroundManager.getInstance(activity).setColor(Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(EstablishInOnCreate.class);
 
         BackgroundManager manager = BackgroundManager.getInstance(activity1);
         waitForBackgroundAnimationFinish(manager);
@@ -349,20 +356,21 @@
         testSwitchBackgrounds(manager);
     }
 
+    public static class EstablishInOnStart extends TestActivity.Provider {
+        @Override
+        public void onStart(TestActivity activity) {
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            if (!m.isAttached()) {
+                // onStart will be called multiple times, attach() can only be called once.
+                m.attach(activity.getWindow());
+            }
+            m.setColor(Color.BLUE);
+        }
+    }
+
     @Test
     public void establishInOnStart() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onStart(TestActivity activity) {
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                if (!m.isAttached()) {
-                    // onStart will be called mutliple times, attach() can only be called once.
-                    m.attach(activity.getWindow());
-                }
-                m.setColor(Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(EstablishInOnStart.class);
 
         BackgroundManager manager = BackgroundManager.getInstance(activity1);
         waitForBackgroundAnimationFinish(manager);
@@ -371,150 +379,155 @@
         testSwitchBackgrounds(manager);
     }
 
+    public static class AssignColorImmediately extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            // if we set color before attach, it will be assigned immediately
+            m.setColor(Color.BLUE);
+            m.attach(activity.getWindow());
+            assertIsColorDrawable(m, Color.BLUE);
+        }
+    }
+
     @Test
     public void assignColorImmediately() throws Throwable {
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                // if we set color before attach, it will be assigned immediately
-                m.setColor(Color.BLUE);
-                m.attach(activity.getWindow());
-                assertIsColorDrawable(m, Color.BLUE);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(AssignColorImmediately.class);
 
         BackgroundManager manager = BackgroundManager.getInstance(activity1);
 
         testSwitchBackgrounds(manager);
     }
 
+    public static class AssignBitmapImmediately extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            // if we set bitmap before attach, it will be assigned immediately
+            m.setBitmap(mBitmap);
+            m.attach(activity.getWindow());
+            assertIsBitmapDrawable(m, mBitmap);
+        }
+    }
+
     @Test
     public void assignBitmapImmediately() throws Throwable {
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                // if we set bitmap before attach, it will be assigned immediately
-                m.setBitmap(bitmap);
-                m.attach(activity.getWindow());
-                assertIsBitmapDrawable(m, bitmap);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(AssignBitmapImmediately.class);
 
         BackgroundManager manager = BackgroundManager.getInstance(activity1);
 
         testSwitchBackgrounds(manager);
     }
 
+    public static class InheritBitmapByNewActivity extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            // if we set bitmap before attach, it will be assigned immediately
+            m.setBitmap(mBitmap);
+            m.attach(activity.getWindow());
+            assertIsBitmapDrawable(m, mBitmap);
+        }
+    }
 
+    public static class InheritBitmapByNewActivity_2 extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.attach(activity.getWindow());
+        }
+    };
     @Test
     public void inheritBitmapByNewActivity() throws Throwable {
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                // if we set bitmap before attach, it will be assigned immediately
-                m.setBitmap(bitmap);
-                m.attach(activity.getWindow());
-                assertIsBitmapDrawable(m, bitmap);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(InheritBitmapByNewActivity.class);
 
-        TestActivity activity2 = mRule.launchSecondActivity(
-                generateProviderName("activity2"),
-                new TestActivity.Provider() {
-                    @Override
-                    public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                        super.onCreate(activity, savedInstanceState);
-                        BackgroundManager m = BackgroundManager.getInstance(activity);
-                        m.attach(activity.getWindow());
-                        assertIsBitmapDrawable(m, bitmap);
-                    }
-                });
+        InheritBitmapByNewActivity provider = (InheritBitmapByNewActivity) activity1.getProvider();
+        TestActivity activity2 = mRule2.launchActivity(InheritBitmapByNewActivity_2.class);
 
         waitForActivityStop(activity1);
+        assertIsBitmapDrawable(BackgroundManager.getInstance(activity2), provider.mBitmap);
 
         BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
-        assertIsBitmapDrawable(manager2, bitmap);
+        assertIsBitmapDrawable(manager2, provider.mBitmap);
         activity2.finish();
     }
 
+    public static class InheritColorByNewActivity extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            // if we set color before attach, it will be assigned immediately
+            m.setColor(Color.BLUE);
+            m.attach(activity.getWindow());
+            assertIsColorDrawable(m, Color.BLUE);
+        }
+    }
+
+    public static class InheritColorByNewActivity2 extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.attach(activity.getWindow());
+            assertIsColorDrawable(m, Color.BLUE);
+        }
+    };
+
     @Test
     public void inheritColorByNewActivity() throws Throwable {
-        final int color = Color.BLUE;
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                // if we set color before attach, it will be assigned immediately
-                m.setColor(color);
-                m.attach(activity.getWindow());
-                assertIsColorDrawable(m, color);
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(InheritColorByNewActivity.class);
 
-        TestActivity activity2 = mRule.launchSecondActivity(
-                generateProviderName("activity2"),
-                new TestActivity.Provider() {
-                    @Override
-                    public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                        super.onCreate(activity, savedInstanceState);
-                        BackgroundManager m = BackgroundManager.getInstance(activity);
-                        m.attach(activity.getWindow());
-                        assertIsColorDrawable(m, color);
-                    }
-                });
+        TestActivity activity2 = mRule2.launchActivity(InheritColorByNewActivity2.class);
         waitForActivityStop(activity1);
 
         BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
-        assertIsColorDrawable(manager2, color);
+        assertIsColorDrawable(manager2, Color.BLUE);
         activity2.finish();
     }
 
+    public static class ReturnFromNewActivity extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            // if we set bitmap before attach, it will be assigned immediately
+            m.setColor(Color.RED);
+            m.setBitmap(mBitmap);
+            m.attach(activity.getWindow());
+            assertIsBitmapDrawable(m, mBitmap);
+        }
+    }
+
+    public static class ReturnFromNewActivity2 extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.attach(activity.getWindow());
+        }
+    }
+
     @Test
     public void returnFromNewActivity() throws Throwable {
-        final int color = Color.RED;
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                // if we set bitmap before attach, it will be assigned immediately
-                m.setColor(color);
-                m.setBitmap(bitmap);
-                m.attach(activity.getWindow());
-                assertIsBitmapDrawable(m, bitmap);
-            }
-
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(ReturnFromNewActivity.class);
         final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
 
-        TestActivity activity2 = mRule.launchSecondActivity(
-                generateProviderName("activity2"),
-                new TestActivity.Provider() {
-                    @Override
-                    public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                        super.onCreate(activity, savedInstanceState);
-                        BackgroundManager m = BackgroundManager.getInstance(activity);
-                        m.attach(activity.getWindow());
-                        assertIsBitmapDrawable(m, bitmap);
-                    }
-                });
+        ReturnFromNewActivity provider = (ReturnFromNewActivity) activity1.getProvider();
+        TestActivity activity2 = mRule2.launchActivity(ReturnFromNewActivity2.class);
         waitForActivityStop(activity1);
+
         final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+        assertIsBitmapDrawable(manager2, provider.mBitmap);
 
         final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
         setBitmapAndVerify(manager2, bitmap2);
@@ -527,47 +540,48 @@
 
         // when return from the other app, last drawable is cleared.
         waitForBackgroundAnimationFinish(manager1);
-        assertIsColorDrawable(manager1, color);
+        assertIsColorDrawable(manager1, Color.RED);
+    }
+
+    public static class ManuallyReleaseInOnStop extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.setAutoReleaseOnStop(false);
+            // if we set bitmap before attach, it will be assigned immediately
+            m.setColor(Color.RED);
+            m.setBitmap(mBitmap);
+            m.attach(activity.getWindow());
+            assertIsBitmapDrawable(m, mBitmap);
+        }
+
+        @Override
+        public void onStop(TestActivity activity) {
+            BackgroundManager.getInstance(activity).release();
+        }
+    }
+
+    public static class ManuallyReleaseInOnStop2 extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.attach(activity.getWindow());
+        }
     }
 
     @Test
     public void manuallyReleaseInOnStop() throws Throwable {
-        final int color = Color.RED;
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                m.setAutoReleaseOnStop(false);
-                // if we set bitmap before attach, it will be assigned immediately
-                m.setColor(color);
-                m.setBitmap(bitmap);
-                m.attach(activity.getWindow());
-                assertIsBitmapDrawable(m, bitmap);
-            }
-
-            @Override
-            public void onStop(TestActivity activity) {
-                BackgroundManager.getInstance(activity).release();
-            }
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(ManuallyReleaseInOnStop.class);
         final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
 
-        TestActivity activity2 = mRule.launchSecondActivity(
-                generateProviderName("activity2"),
-                new TestActivity.Provider() {
-                    @Override
-                    public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                        super.onCreate(activity, savedInstanceState);
-                        BackgroundManager m = BackgroundManager.getInstance(activity);
-                        m.attach(activity.getWindow());
-                        assertIsBitmapDrawable(m, bitmap);
-                    }
-                });
+        ManuallyReleaseInOnStop provider = (ManuallyReleaseInOnStop) activity1.getProvider();
+        TestActivity activity2 = mRule2.launchActivity(ManuallyReleaseInOnStop2.class);
         waitForActivityStop(activity1);
         final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+        assertIsBitmapDrawable(manager2, provider.mBitmap);
 
         final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
         setBitmapAndVerify(manager2, bitmap2);
@@ -580,76 +594,81 @@
 
         // when return from the other app, last drawable is cleared.
         waitForBackgroundAnimationFinish(manager1);
-        assertIsColorDrawable(manager1, color);
+        assertIsColorDrawable(manager1, Color.RED);
+    }
+
+    public static class DisableAutoRelease extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.setAutoReleaseOnStop(false);
+            // if we set bitmap before attach, it will be assigned immediately
+            m.setColor(Color.RED);
+            m.setBitmap(mBitmap);
+            m.attach(activity.getWindow());
+            assertIsBitmapDrawable(m, mBitmap);
+        }
+
+    }
+
+    public static class DisableAutoRelease2 extends TestActivity.Provider {
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.attach(activity.getWindow());
+        }
     }
 
     @Test
     public void disableAutoRelease() throws Throwable {
-        final int color = Color.RED;
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                m.setAutoReleaseOnStop(false);
-                // if we set bitmap before attach, it will be assigned immediately
-                m.setColor(color);
-                m.setBitmap(bitmap);
-                m.attach(activity.getWindow());
-                assertIsBitmapDrawable(m, bitmap);
-            }
-
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 = mRule.launchActivity(DisableAutoRelease.class);
         final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
 
-        TestActivity activity2 = mRule.launchSecondActivity(
-                generateProviderName("activity2"),
-                new TestActivity.Provider() {
-                    @Override
-                    public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                        super.onCreate(activity, savedInstanceState);
-                        BackgroundManager m = BackgroundManager.getInstance(activity);
-                        m.attach(activity.getWindow());
-                        assertIsBitmapDrawable(m, bitmap);
-                    }
-                });
+        DisableAutoRelease provider = (DisableAutoRelease) activity1.getProvider();
+        TestActivity activity2 = mRule2.launchActivity(DisableAutoRelease2.class);
         waitForActivityStop(activity1);
         final BackgroundManager manager2 = BackgroundManager.getInstance(activity2);
+        assertIsBitmapDrawable(manager2, provider.mBitmap);
 
         final Bitmap bitmap2 = createBitmap(200, 100, Color.GREEN);
         setBitmapAndVerify(manager2, bitmap2);
 
         // after activity2 is launched, activity will keep its drawable because
         // setAutoReleaseOnStop(false)
-        assertIsBitmapDrawable(manager1, bitmap);
+        assertIsBitmapDrawable(manager1, provider.mBitmap);
 
         activity2.finish();
 
         // when return from the activity, it's still the same bitmap
         waitForBackgroundAnimationFinish(manager1);
-        assertIsBitmapDrawable(manager1, bitmap);
+        assertIsBitmapDrawable(manager1, provider.mBitmap);
+    }
+
+    public static class DelayDrawableChangeUntilFullAlpha extends TestActivity.Provider {
+        final Bitmap mBitmap = createBitmap(200, 100, Color.BLUE);
+        @Override
+        public void onCreate(TestActivity activity, Bundle savedInstanceState) {
+            super.onCreate(activity, savedInstanceState);
+            BackgroundManager m = BackgroundManager.getInstance(activity);
+            m.setAutoReleaseOnStop(false);
+            m.attach(activity.getWindow());
+            m.setColor(Color.RED);
+        }
+
     }
 
     @Test
     public void delayDrawableChangeUntilFullAlpha() throws Throwable {
-        final Bitmap bitmap = createBitmap(200, 100, Color.BLUE);
-        TestActivity.setProvider(mRule.getProviderName(), new TestActivity.Provider() {
-            @Override
-            public void onCreate(TestActivity activity, Bundle savedInstanceState) {
-                super.onCreate(activity, savedInstanceState);
-                BackgroundManager m = BackgroundManager.getInstance(activity);
-                m.setAutoReleaseOnStop(false);
-                m.attach(activity.getWindow());
-                m.setColor(Color.RED);
-            }
-
-        });
-        final TestActivity activity1 = mRule.launchActivity();
+        final TestActivity activity1 =
+                mRule.launchActivity(DelayDrawableChangeUntilFullAlpha.class);
         final BackgroundManager manager1 = BackgroundManager.getInstance(activity1);
         assertIsColorDrawable(manager1, Color.RED);
 
+        DelayDrawableChangeUntilFullAlpha provider =
+                (DelayDrawableChangeUntilFullAlpha) activity1.getProvider();
         // when set drawable, the change will be pending because alpha is 128
         mRule.runOnUiThread(new Runnable() {
             @Override
@@ -657,7 +676,7 @@
                 assertSame(manager1.mLayerDrawable,
                         activity1.getWindow().getDecorView().getBackground());
                 activity1.getWindow().getDecorView().getBackground().setAlpha(128);
-                manager1.setBitmap(bitmap);
+                manager1.setBitmap(provider.mBitmap);
             }
         });
         assertEquals(Color.RED,
@@ -672,6 +691,6 @@
             }
         });
         waitForBackgroundAnimationFinish(manager1);
-        assertIsBitmapDrawable(manager1, bitmap);
+        assertIsBitmapDrawable(manager1, provider.mBitmap);
     }
 }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
index 9c6e789..058c193 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseFragmentTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
@@ -33,11 +34,15 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
 import androidx.leanback.testutils.PollingCheck;
 import androidx.leanback.widget.ArrayObjectAdapter;
 import androidx.leanback.widget.ItemBridgeAdapter;
@@ -45,9 +50,11 @@
 import androidx.leanback.widget.Presenter;
 import androidx.leanback.widget.Row;
 import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.VerticalGridView;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 
@@ -341,6 +348,66 @@
         });
     }
 
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(BrowseFragmentTestActivity.EXTRA_HEADERS_STATE,
+                BrowseFragment.HEADERS_DISABLED);
+        mActivity = activityTestRule.launchActivity(intent);
+        waitForEntranceTransitionFinished();
+
+        VerticalGridView gridView = mActivity.getBrowseTestFragment().getGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        mActivity.getFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
+
     static void tapView(View v) {
         Instrumentation inst = InstrumentationRegistry.getInstrumentation();
         int[] xy = new int[2];
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
index 72dc227..1a36149 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/BrowseSupportFragmentTest.java
@@ -21,6 +21,7 @@
 import static org.junit.Assert.assertTrue;
 
 import android.app.Instrumentation;
+import android.content.Context;
 import android.content.Intent;
 import android.os.Build;
 import android.os.Bundle;
@@ -30,11 +31,15 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 import android.widget.FrameLayout;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
 import androidx.leanback.testutils.PollingCheck;
 import androidx.leanback.widget.ArrayObjectAdapter;
 import androidx.leanback.widget.ItemBridgeAdapter;
@@ -42,9 +47,11 @@
 import androidx.leanback.widget.Presenter;
 import androidx.leanback.widget.Row;
 import androidx.leanback.widget.RowPresenter;
+import androidx.leanback.widget.VerticalGridView;
 import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 
@@ -338,6 +345,66 @@
         });
     }
 
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        Intent intent = new Intent();
+        intent.putExtra(BrowseSupportFragmentTestActivity.EXTRA_HEADERS_STATE,
+                BrowseSupportFragment.HEADERS_DISABLED);
+        mActivity = activityTestRule.launchActivity(intent);
+        waitForEntranceTransitionFinished();
+
+        VerticalGridView gridView = mActivity.getBrowseTestSupportFragment().getGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        mActivity.getSupportFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
+
     static void tapView(View v) {
         Instrumentation inst = InstrumentationRegistry.getInstrumentation();
         int[] xy = new int[2];
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
index 504f501..cc83090 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/TestActivity.java
@@ -20,17 +20,17 @@
 import android.os.Bundle;
 
 import androidx.annotation.CallSuper;
-import androidx.leanback.testutils.PollingCheck;
 import androidx.testutils.AnimationActivityTestRule;
 
-import java.util.HashMap;
-
 /**
  * A general Activity that allows test set a Provider to custom activity's behavior in life
  * cycle events.
  */
 public class TestActivity extends Activity {
 
+    public static class TestActivity2 extends  TestActivity {
+    }
+
     public static class Provider {
 
         TestActivity mActivity;
@@ -70,14 +70,14 @@
 
         final String mProviderName;
 
-        public TestActivityTestRule(TestActivity.Provider provider, String providerName) {
-            this(providerName);
-            TestActivity.setProvider(mProviderName, provider);
+        public TestActivityTestRule(Class<? extends TestActivity.Provider> providerClass) {
+            super(TestActivity.class, false, false);
+            mProviderName = providerClass.getName();
         }
 
-        public TestActivityTestRule(String providerName) {
+        public TestActivityTestRule() {
             super(TestActivity.class, false, false);
-            mProviderName = providerName;
+            mProviderName = null;
         }
 
         public String getProviderName() {
@@ -90,42 +90,28 @@
             return launchActivity(intent);
         }
 
-        /**
-         * Launch secondary TestActivity without using TestActivityRule.
-         */
-        TestActivity launchSecondActivity(String providerName2, TestActivity.Provider provider2)
-                throws Throwable {
-            TestActivity.setProvider(providerName2, provider2);
-            runOnUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    Intent intent = new Intent(getActivity(), TestActivity.class);
-                    intent.putExtra(TestActivity.EXTRA_PROVIDER, providerName2);
-                    getActivity().startActivity(intent);
-                }
-            });
+        public TestActivity launchActivity(Class<? extends TestActivity.Provider> providerClass) {
+            Intent intent = new Intent();
+            intent.putExtra(TestActivity.EXTRA_PROVIDER, providerClass.getName());
+            return launchActivity(intent);
+        }
+    }
 
-            PollingCheck.waitFor(5000/*timeout*/, new PollingCheck.PollingCheckCondition() {
-                @Override
-                public boolean canPreProceed() {
-                    return false;
-                }
+    public static class TestActivityTestRule2 extends AnimationActivityTestRule<TestActivity2> {
 
-                @Override
-                public boolean canProceed() {
-                    return provider2.getActivity() != null && provider2.getActivity().isStarted();
-                }
-            });
-            afterActivityLaunched();
-            return provider2.getActivity();
+        public TestActivityTestRule2() {
+            super(TestActivity2.class, false, false);
         }
 
+        public TestActivity launchActivity(Class<? extends TestActivity.Provider> providerClass) {
+            Intent intent = new Intent();
+            intent.putExtra(TestActivity.EXTRA_PROVIDER, providerClass.getName());
+            return launchActivity(intent);
+        }
     }
 
     public static final String EXTRA_PROVIDER = "testActivityProvider";
 
-    static HashMap<String, Provider> sProviders = new HashMap<>();
-
     String mProviderName;
     Provider mProvider;
     boolean mStarted;
@@ -133,15 +119,19 @@
     public TestActivity() {
     }
 
-    public static void setProvider(String name, Provider provider) {
-        sProviders.put(name, provider);
+    public Provider getProvider() {
+        return mProvider;
     }
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         mProviderName = getIntent().getStringExtra(EXTRA_PROVIDER);
-        mProvider = sProviders.get(mProviderName);
+        try {
+            mProvider = (Provider) Class.forName(mProviderName).newInstance();
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
         if (mProvider != null) {
             mProvider.onCreate(this, savedInstanceState);
         }
@@ -197,7 +187,6 @@
     protected void onDestroy() {
         if (mProvider != null) {
             mProvider.onDestroy(this);
-            setProvider(mProviderName, null);
         }
         super.onDestroy();
     }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
index ecd2773..2f0f90e 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridFragmentTest.java
@@ -19,13 +19,25 @@
 
 package androidx.leanback.app;
 
+import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 
 import android.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
 import androidx.leanback.widget.ArrayObjectAdapter;
 import androidx.leanback.widget.VerticalGridPresenter;
+import androidx.leanback.widget.VerticalGridView;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
@@ -35,6 +47,19 @@
 @RunWith(AndroidJUnit4.class)
 public class VerticalGridFragmentTest extends SingleFragmentTestBase {
 
+    static void loadData(ArrayObjectAdapter adapter, int items) {
+        int index = 0;
+        for (int j = 0; j < items; ++j) {
+            adapter.add("Hello world-" + (index++));
+            adapter.add("This is a test-" + (index++));
+            adapter.add("Android TV-" + (index++));
+            adapter.add("Leanback-" + (index++));
+            adapter.add("Hello world-" + (index++));
+            adapter.add("Android TV-" + (index++));
+            adapter.add("Leanback-" + (index++));
+        }
+    }
+
     public static class GridFragment extends VerticalGridFragment {
         @Override
         public void onCreate(Bundle savedInstanceState) {
@@ -45,7 +70,9 @@
             VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
             gridPresenter.setNumberOfColumns(3);
             setGridPresenter(gridPresenter);
-            setAdapter(new ArrayObjectAdapter());
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new StringPresenter());
+            setAdapter(adapter);
+            loadData(adapter, 10);
         }
     }
 
@@ -69,4 +96,62 @@
         activity.finish();
     }
 
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        SingleFragmentTestActivity activity = launchAndWaitActivity(
+                GridFragment.class,
+                1000);
+
+        VerticalGridView gridView = ((GridFragment) activity.getTestFragment())
+                .mGridViewHolder.getGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        activity.getFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
 }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
index 4594ec8..3b28421 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/app/VerticalGridSupportFragmentTest.java
@@ -16,13 +16,25 @@
 
 package androidx.leanback.app;
 
+import android.content.Context;
+import android.os.Build;
 import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
 
 import androidx.fragment.app.Fragment;
+import androidx.leanback.test.R;
+import androidx.leanback.testutils.LeakDetector;
+import androidx.leanback.testutils.PollingCheck;
 import androidx.leanback.widget.ArrayObjectAdapter;
 import androidx.leanback.widget.VerticalGridPresenter;
+import androidx.leanback.widget.VerticalGridView;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
+import androidx.test.filters.SdkSuppress;
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
@@ -32,6 +44,19 @@
 @RunWith(AndroidJUnit4.class)
 public class VerticalGridSupportFragmentTest extends SingleSupportFragmentTestBase {
 
+    static void loadData(ArrayObjectAdapter adapter, int items) {
+        int index = 0;
+        for (int j = 0; j < items; ++j) {
+            adapter.add("Hello world-" + (index++));
+            adapter.add("This is a test-" + (index++));
+            adapter.add("Android TV-" + (index++));
+            adapter.add("Leanback-" + (index++));
+            adapter.add("Hello world-" + (index++));
+            adapter.add("Android TV-" + (index++));
+            adapter.add("Leanback-" + (index++));
+        }
+    }
+
     public static class GridFragment extends VerticalGridSupportFragment {
         @Override
         public void onCreate(Bundle savedInstanceState) {
@@ -42,7 +67,9 @@
             VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
             gridPresenter.setNumberOfColumns(3);
             setGridPresenter(gridPresenter);
-            setAdapter(new ArrayObjectAdapter());
+            ArrayObjectAdapter adapter = new ArrayObjectAdapter(new StringPresenter());
+            setAdapter(adapter);
+            loadData(adapter, 10);
         }
     }
 
@@ -66,4 +93,62 @@
         activity.finish();
     }
 
+    public static final class EmptyFragment extends Fragment {
+        EditText mEditText;
+
+        @Override
+        public View onCreateView(
+                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+            return mEditText = new EditText(container.getContext());
+        }
+
+        @Override
+        public void onStart() {
+            super.onStart();
+            // focus IME on the new fragment because there is a memory leak that IME remembers
+            // last editable view, which will cause a false reporting of leaking View.
+            InputMethodManager imm =
+                    (InputMethodManager) getActivity()
+                            .getSystemService(Context.INPUT_METHOD_SERVICE);
+            mEditText.requestFocus();
+            imm.showSoftInput(mEditText, 0);
+        }
+
+        @Override
+        public void onDestroyView() {
+            mEditText = null;
+            super.onDestroyView();
+        }
+    }
+
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) // API 17 retains local Variable
+    @Test
+    public void viewLeakTest() throws Throwable {
+        SingleSupportFragmentTestActivity activity = launchAndWaitActivity(
+                GridFragment.class,
+                1000);
+
+        VerticalGridView gridView = ((GridFragment) activity.getTestFragment())
+                .mGridViewHolder.getGridView();
+        LeakDetector leakDetector = new LeakDetector();
+        leakDetector.observeObject(gridView);
+        leakDetector.observeObject(gridView.getRecycledViewPool());
+        for (int i = 0; i < gridView.getChildCount(); i++) {
+            leakDetector.observeObject(gridView.getChildAt(i));
+        }
+        gridView = null;
+        EmptyFragment emptyFragment = new EmptyFragment();
+        activity.getSupportFragmentManager().beginTransaction()
+                .replace(R.id.main_frame, emptyFragment)
+                .addToBackStack("BK")
+                .commit();
+
+        PollingCheck.waitFor(1000, new PollingCheck.PollingCheckCondition() {
+            @Override
+            public boolean canProceed() {
+                return emptyFragment.isResumed();
+            }
+        });
+        leakDetector.assertNoLeak();
+    }
 }
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java b/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
index af86a35..1564cbd 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/testutils/LeakDetector.java
@@ -29,10 +29,17 @@
         mWeakReferences.add(new WeakReference<>(object));
     }
 
-    public void assertNoLeak() {
+    public void assertNoLeak() throws Exception {
         System.gc();
         System.runFinalization();
         for (WeakReference<?> weakReference : mWeakReferences) {
+            int count = 0;
+            while (weakReference.get() != null && count < 5) {
+                System.gc();
+                System.runFinalization();
+                Thread.sleep(1000);
+                count++;
+            }
             /**
              * Debugging leak: Sleep and run adb command:
              * adb shell am dumpheap PID_OF_TEST /data/local/tmp/test_leak.hprof
diff --git a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
index ef4e3c3..37b2bac 100644
--- a/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
+++ b/leanback/leanback/src/androidTest/java/androidx/leanback/widget/ImageCardViewTest.java
@@ -63,7 +63,16 @@
 
     // The following provider will create an Activity which can inflate the ImageCardView
     // And the ImageCardView can be fetched through ID for future testing.
-    private final TestActivity.Provider mImageCardViewProvider = new TestActivity.Provider() {
+    public static final class ImageCardViewProvider extends TestActivity.Provider {
+        // Sample Drawable which will be used as the parameter for some methods.
+        private Drawable mSampleDrawable;
+
+        // Another Sample Drawable.
+        private Drawable mSampleDrawable2;
+
+        // Generated Image View Id.
+        private int mImageCardViewId;
+
         @Override
         public void onCreate(TestActivity activity, Bundle savedInstanceState) {
             super.onCreate(activity, savedInstanceState);
@@ -90,8 +99,7 @@
     // Enable lifecycle based testing
     @Rule
     public final TestActivity.TestActivityTestRule mRule =
-            new TestActivity.TestActivityTestRule(
-                    mImageCardViewProvider, generateProviderName(IMAGE_CARD_VIEW_ACTIVITY));
+            new TestActivity.TestActivityTestRule(ImageCardViewProvider.class);
 
     // Only support alpha animation
     private static final String ALPHA = "alpha";
@@ -166,6 +174,11 @@
 
         // Initialize testing rule and testing activity
         final TestActivity imageCardViewTestActivity = mRule.launchActivity();
+        ImageCardViewProvider provider = (ImageCardViewProvider)
+                imageCardViewTestActivity.getProvider();
+        mImageCardViewId = provider.mImageCardViewId;
+        mSampleDrawable = provider.mSampleDrawable;
+        mSampleDrawable2 = provider.mSampleDrawable2;
 
         // Create card view and image view
         mImageCardView = (ImageCardView) imageCardViewTestActivity.findViewById(mImageCardViewId);
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
index b29c59c..1dccd30 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseFragment.java
@@ -190,6 +190,13 @@
         mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
     }
 
+    @Override
+    public void onDestroyView() {
+        mProgressBarManager.setRootView(null);
+        mProgressBarManager.setProgressBarView(null);
+        super.onDestroyView();
+    }
+
     /**
      * Enables entrance transition.<p>
      * Entrance transition is the standard slide-in transition that shows rows of data in
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
index c84d119..783a8ea 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BaseSupportFragment.java
@@ -185,6 +185,13 @@
         mStateMachine.fireEvent(EVT_ON_CREATEVIEW);
     }
 
+    @Override
+    public void onDestroyView() {
+        mProgressBarManager.setRootView(null);
+        mProgressBarManager.setProgressBarView(null);
+        super.onDestroyView();
+    }
+
     /**
      * Enables entrance transition.<p>
      * Entrance transition is the standard slide-in transition that shows rows of data in
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
index c8012e5..de4327a 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseFragment.java
@@ -1239,6 +1239,11 @@
         mMainFragmentAdapter = null;
         mMainFragment = null;
         mHeadersFragment = null;
+        mBrowseFrame = null;
+        mScaleFrameLayout = null;
+        mSceneAfterEntranceTransition = null;
+        mSceneWithHeaders = null;
+        mSceneWithoutHeaders = null;
         super.onDestroyView();
     }
 
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
index 24b3b64..ef17147 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/BrowseSupportFragment.java
@@ -1216,6 +1216,11 @@
         mMainFragmentAdapter = null;
         mMainFragment = null;
         mHeadersSupportFragment = null;
+        mBrowseFrame = null;
+        mScaleFrameLayout = null;
+        mSceneAfterEntranceTransition = null;
+        mSceneWithHeaders = null;
+        mSceneWithoutHeaders = null;
         super.onDestroyView();
     }
 
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java b/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
index 276b899..9b5f3e4 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/ProgressBarManager.java
@@ -91,13 +91,15 @@
      * @param progressBarView custom view that will be shown to indicate progress.
      */
     public void setProgressBarView(View progressBarView) {
-        if (progressBarView.getParent() == null) {
+        if (progressBarView != null && progressBarView.getParent() == null) {
             throw new IllegalArgumentException("Must have a parent");
         }
 
         this.mProgressBarView = progressBarView;
-        this.mProgressBarView.setVisibility(View.INVISIBLE);
-        mUserProvidedProgressBar = true;
+        if (this.mProgressBarView != null) {
+            this.mProgressBarView.setVisibility(View.INVISIBLE);
+            mUserProvidedProgressBar = true;
+        }
     }
 
     /**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
index 13ab244..73047073 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridFragment.java
@@ -222,7 +222,9 @@
     @Override
     public void onDestroyView() {
         super.onDestroyView();
+        mGridViewHolder.getGridView().swapAdapter(null, true);
         mGridViewHolder = null;
+        mSceneAfterEntranceTransition = null;
     }
 
     /**
diff --git a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
index 7966043..26b9a648 100644
--- a/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
+++ b/leanback/leanback/src/main/java/androidx/leanback/app/VerticalGridSupportFragment.java
@@ -217,7 +217,9 @@
     @Override
     public void onDestroyView() {
         super.onDestroyView();
+        mGridViewHolder.getGridView().swapAdapter(null, true);
         mGridViewHolder = null;
+        mSceneAfterEntranceTransition = null;
     }
 
     /**
diff --git a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
index 13b478b..c581d23 100644
--- a/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
+++ b/lifecycle/lifecycle-viewmodel/src/main/java/androidx/lifecycle/ViewModel.java
@@ -35,8 +35,7 @@
  * finished.
  * <p>
  * In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
- * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
- * existing ViewModel.
+ * configuration change (e.g. rotation). The new owner instance just re-connects to the existing model.
  * <p>
  * The purpose of the ViewModel is to acquire and keep the information that is necessary for an
  * Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
index a65e7a2..2b98060 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2Provider.java
@@ -40,6 +40,7 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -53,10 +54,9 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 
 /**
  * Provides non-system routes (and related RouteControllers) by using MediaRouter2.
@@ -154,9 +154,18 @@
 
     protected void refreshRoutes() {
         // Syetem routes should not be published by this provider.
-        List<MediaRoute2Info> newRoutes = mMediaRouter2.getRoutes().stream().distinct()
-                .filter(r -> !r.isSystemRoute())
-                .collect(Collectors.toList());
+        List<MediaRoute2Info> newRoutes = new ArrayList<>();
+        Set<MediaRoute2Info> route2InfoSet = new ArraySet<>();
+        for (MediaRoute2Info route : mMediaRouter2.getRoutes()) {
+            // A route should be unique
+            if (route == null || route2InfoSet.contains(route) || route.isSystemRoute()) {
+                continue;
+            }
+            route2InfoSet.add(route);
+
+            // Not using new ArrayList(route2InfoSet) here for preserving the order.
+            newRoutes.add(route);
+        }
 
         if (newRoutes.equals(mRoutes)) {
             return;
@@ -175,10 +184,13 @@
                     extras.getString(MediaRouter2Utils.KEY_ORIGINAL_ROUTE_ID));
         }
 
-        List<MediaRouteDescriptor> routeDescriptors = mRoutes.stream()
-                .map(MediaRouter2Utils::toMediaRouteDescriptor)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList());
+        List<MediaRouteDescriptor> routeDescriptors = new ArrayList<>();
+        for (MediaRoute2Info route : mRoutes) {
+            MediaRouteDescriptor descriptor = MediaRouter2Utils.toMediaRouteDescriptor(route);
+            if (route != null) {
+                routeDescriptors.add(descriptor);
+            }
+        }
         MediaRouteProviderDescriptor descriptor = new MediaRouteProviderDescriptor.Builder()
                 .setSupportsDynamicGroupRoute(true)
                 .addRoutes(routeDescriptors)
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
index a279629..cfde237 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRoute2ProviderServiceAdapter.java
@@ -29,6 +29,7 @@
 import android.annotation.SuppressLint;
 import android.content.Context;
 import android.content.Intent;
+import android.media.MediaRoute2Info;
 import android.media.MediaRoute2ProviderService;
 import android.media.RouteDiscoveryPreference;
 import android.media.RoutingSessionInfo;
@@ -61,9 +62,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.UUID;
-import java.util.stream.Collectors;
 
 @RequiresApi(api = Build.VERSION_CODES.R)
 class MediaRoute2ProviderServiceAdapter extends MediaRoute2ProviderService {
@@ -279,12 +278,8 @@
 
     @Override
     public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {
-        MediaRouteSelector selector = new MediaRouteSelector.Builder()
-                .addControlCategories(preference.getPreferredFeatures().stream()
-                        .map(MediaRouter2Utils::toControlCategory)
-                        .collect(Collectors.toList())).build();
-        mServiceImpl.setBaseDiscoveryRequest(new MediaRouteDiscoveryRequest(selector,
-                preference.shouldPerformActiveScan()));
+        mServiceImpl.setBaseDiscoveryRequest(
+                MediaRouter2Utils.toMediaRouteDiscoveryRequest(preference));
     }
 
     public void setProviderDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
@@ -292,17 +287,26 @@
         List<MediaRouteDescriptor> routeDescriptors =
                 (descriptor == null) ? Collections.emptyList() : descriptor.getRoutes();
 
-        Map<String, MediaRouteDescriptor> descriptorMap =
-                routeDescriptors.stream().filter(Objects::nonNull)
-                        // Ignores duplicated route IDs.
-                        .collect(Collectors.toMap(r -> r.getId(), r -> r, (fst, snd) -> fst));
+        Map<String, MediaRouteDescriptor> descriptorMap = new ArrayMap<>();
+        for (MediaRouteDescriptor desc : routeDescriptors) {
+            // Ignores duplicated route IDs.
+            if (desc == null || descriptorMap.containsKey(desc.getId())) {
+                continue;
+            }
+            descriptorMap.put(desc.getId(), desc);
+        }
 
         updateStaticSessions(descriptorMap);
         // Handle duplicated IDs
-        notifyRoutes(descriptorMap.values().stream()
-                .map(MediaRouter2Utils::toFwkMediaRoute2Info)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toList()));
+
+        List<MediaRoute2Info> routes = new ArrayList<>();
+        for (MediaRouteDescriptor desc : descriptorMap.values()) {
+            MediaRoute2Info fwkMediaRouteInfo = MediaRouter2Utils.toFwkMediaRoute2Info(desc);
+            if (fwkMediaRouteInfo != null) {
+                routes.add(fwkMediaRouteInfo);
+            }
+        }
+        notifyRoutes(routes);
     }
 
     private DynamicGroupRouteController findControllerBySessionId(String sessionId) {
@@ -354,11 +358,13 @@
     }
 
     void updateStaticSessions(Map<String, MediaRouteDescriptor> routeDescriptors) {
-        List<SessionRecord> staticSessions;
+        List<SessionRecord> staticSessions = new ArrayList<>();
         synchronized (mLock) {
-            staticSessions = mSessionRecords.values().stream()
-                    .filter(r -> (r.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0)
-                    .collect(Collectors.toList());
+            for (SessionRecord sessionRecord : mSessionRecords.values()) {
+                if ((sessionRecord.getFlags() & SessionRecord.SESSION_FLAG_DYNAMIC) == 0) {
+                    staticSessions.add(sessionRecord);
+                }
+            }
         }
         for (SessionRecord sessionRecord : staticSessions) {
             DynamicGroupRouteControllerProxy controller =
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
index ea442e4..91db8ba 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouter2Utils.java
@@ -33,6 +33,7 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
+import android.util.ArraySet;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -42,9 +43,7 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
-import java.util.stream.Collectors;
 
 //TODO: Remove SuppressLInt
 @SuppressLint("NewApi")
@@ -185,14 +184,26 @@
         if (features == null) {
             return new ArrayList<>();
         }
-        return features.stream().distinct().map(f -> {
+
+        List<IntentFilter> controlFilters = new ArrayList<>();
+        Set<String> featuresSet = new ArraySet<>();
+        for (String feature : features) {
+            // A feature should be unique.
+            if (featuresSet.contains(feature)) {
+                continue;
+            }
+            featuresSet.add(feature);
+
             IntentFilter filter = new IntentFilter();
-            filter.addCategory(toControlCategory(f));
+            filter.addCategory(toControlCategory(feature));
             // TODO: Add actions by using extras. (see RemotePlaybackClient#detectFeatures())
             // filter.addAction(MediaControlIntent.ACTION_PLAY);
             // filter.addAction(MediaControlIntent.ACTION_SEEK);
-            return filter;
-        }).collect(Collectors.toList());
+
+            controlFilters.add(filter);
+        }
+
+        return controlFilters;
     }
 
     @NonNull
@@ -200,8 +211,29 @@
         if (routes == null) {
             return new ArrayList<>();
         }
-        return routes.stream().filter(Objects::nonNull)
-                .map(MediaRoute2Info::getId).collect(Collectors.toList());
+
+        List<String> routeIds = new ArrayList<>();
+        for (MediaRoute2Info route : routes) {
+            if (route == null) {
+                continue;
+            }
+            routeIds.add(route.getId());
+        }
+        return routeIds;
+    }
+
+    @NonNull
+    static MediaRouteDiscoveryRequest toMediaRouteDiscoveryRequest(
+            @NonNull RouteDiscoveryPreference preference) {
+        List<String> controlCategories = new ArrayList<>();
+        for (String feature : preference.getPreferredFeatures()) {
+            controlCategories.add(MediaRouter2Utils.toControlCategory(feature));
+        }
+        MediaRouteSelector selector = new MediaRouteSelector.Builder()
+                .addControlCategories(controlCategories)
+                .build();
+
+        return new MediaRouteDiscoveryRequest(selector, preference.shouldPerformActiveScan());
     }
 
     @NonNull
@@ -211,9 +243,11 @@
             return new RouteDiscoveryPreference.Builder(new ArrayList<>(), false).build();
         }
         boolean activeScan = discoveryRequest.isActiveScan();
-        List<String> routeFeatures = discoveryRequest.getSelector().getControlCategories()
-                .stream().map(MediaRouter2Utils::toRouteFeature)
-                .collect(Collectors.toList());
+
+        List<String> routeFeatures = new ArrayList<>();
+        for (String controlCategory : discoveryRequest.getSelector().getControlCategories()) {
+            routeFeatures.add(MediaRouter2Utils.toRouteFeature(controlCategory));
+        }
         return new RouteDiscoveryPreference.Builder(routeFeatures, activeScan).build();
     }
 
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
index 3126485..8141dc1 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -34,7 +34,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Collectors;
 
 /**
  * Watches for media route provider services to be installed.
@@ -163,8 +162,12 @@
     @NonNull
     List<ServiceInfo> getMediaRoute2ProviderServices() {
         Intent intent = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE);
-        return mPackageManager.queryIntentServices(intent, 0).stream()
-                .map(resolveInfo -> resolveInfo.serviceInfo).collect(Collectors.toList());
+
+        List<ServiceInfo> serviceInfoList = new ArrayList<>();
+        for (ResolveInfo resolveInfo : mPackageManager.queryIntentServices(intent, 0)) {
+            serviceInfoList.add(resolveInfo.serviceInfo);
+        }
+        return serviceInfoList;
     }
 
     private int findProvider(String packageName, String className) {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
index 268a5f7..41f6df0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/JavaPoetExt.kt
@@ -17,6 +17,8 @@
 
 import androidx.room.compiler.processing.javac.JavacElement
 import androidx.room.compiler.processing.javac.JavacExecutableElement
+import androidx.room.compiler.processing.ksp.KspMethodElement
+import androidx.room.compiler.processing.ksp.KspMethodType
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
@@ -100,10 +102,18 @@
         elm: XMethodElement,
         owner: XDeclaredType
     ): MethodSpec.Builder {
-        return overridingWithFinalParams(
-            elm,
-            elm.asMemberOf(owner)
-        )
+        val asMember = elm.asMemberOf(owner)
+        return if (elm is KspMethodElement && asMember is KspMethodType) {
+            overridingWithFinalParams(
+                executableElement = elm,
+                resolvedType = asMember.inheritVarianceForOverride()
+            )
+        } else {
+            overridingWithFinalParams(
+                executableElement = elm,
+                resolvedType = asMember
+            )
+        }
     }
 
     private fun overridingWithFinalParams(
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 db41d40..03fc077 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
@@ -166,6 +166,18 @@
      * If this is a wildcard with an extends bound, returns that bounded typed.
      */
     fun extendsBound(): XType?
+
+    /**
+     * Creates a type with nullability [XNullability.NULLABLE] or returns this if the nullability is
+     * already [XNullability.NULLABLE].
+     */
+    fun makeNullable(): XType
+
+    /**
+     * Creates a type with nullability [XNullability.NONNULL] or returns this if the nullability is
+     * already [XNullability.NONNULL].
+     */
+    fun makeNonNullable(): XType
 }
 
 /**
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
index 3a79c3a..7e4e1f0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/DefaultJavacType.kt
@@ -56,4 +56,13 @@
     override val equalityItems by lazy {
         arrayOf(typeMirror)
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacType {
+        return DefaultJavacType(
+            env = env,
+            typeMirror = typeMirror,
+            kotlinType = kotlinType,
+            nullability = nullability
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
index 87489e2..189f9dc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacArrayType.kt
@@ -75,4 +75,14 @@
             elementNullability = componentTypeNullability
         )
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacType {
+        return JavacArrayType(
+            env = env,
+            typeMirror = typeMirror,
+            nullability = nullability,
+            knownComponentNullability = knownComponentNullability,
+            kotlinType = kotlinType
+        )
+    }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
index bed2e88..348ca07 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/javac/JavacDeclaredType.kt
@@ -65,4 +65,13 @@
             )
         }
     }
+
+    override fun copyWithNullability(nullability: XNullability): JavacDeclaredType {
+        return JavacDeclaredType(
+            env = env,
+            typeMirror = typeMirror,
+            kotlinType = kotlinType,
+            nullability = nullability
+        )
+    }
 }
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 dc9a6be..173ac32 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
@@ -77,16 +77,26 @@
         }
     }
 
-    override fun boxed(): XType {
-        return if (typeMirror.kind.isPrimitive) {
-            env.wrap(
-                typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
-                    .asType(),
-                kotlinType = kotlinType,
-                elementNullability = XNullability.NULLABLE
-            )
-        } else {
-            this
+    override fun boxed(): JavacType {
+        return when {
+            typeMirror.kind.isPrimitive -> {
+                env.wrap(
+                    typeMirror = env.typeUtils.boxedClass(MoreTypes.asPrimitiveType(typeMirror))
+                        .asType(),
+                    kotlinType = kotlinType,
+                    elementNullability = XNullability.NULLABLE
+                )
+            }
+            typeMirror.kind == TypeKind.VOID -> {
+                env.wrap(
+                    typeMirror = env.elementUtils.getTypeElement("java.lang.Void").asType(),
+                    kotlinType = kotlinType,
+                    elementNullability = XNullability.NULLABLE
+                )
+            }
+            else -> {
+                this
+            }
         }
     }
 
@@ -134,6 +144,32 @@
         return MoreTypes.isType(typeMirror)
     }
 
+    /**
+     * Create a copy of this type with the given nullability.
+     * This method is not called if the nullability of the type is already equal to the given
+     * nullability.
+     */
+    protected abstract fun copyWithNullability(nullability: XNullability): JavacType
+
+    final override fun makeNullable(): JavacType {
+        if (nullability == XNullability.NULLABLE) {
+            return this
+        }
+        if (typeMirror.kind.isPrimitive || typeMirror.kind == TypeKind.VOID) {
+            return boxed().makeNullable()
+        }
+        return copyWithNullability(XNullability.NULLABLE)
+    }
+
+    final override fun makeNonNullable(): JavacType {
+        if (nullability == XNullability.NONNULL) {
+            return this
+        }
+        // unlike makeNullable, we don't try to degrade to primitives here because it is valid for
+        // a boxed primitive to be marked as non-null.
+        return copyWithNullability(XNullability.NONNULL)
+    }
+
     companion object {
         private val BOXED_INT = TypeName.INT.box()
         private val BOXED_LONG = TypeName.LONG.box()
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
index f4a48bd..b7ec701 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAnnotatedExt.kt
@@ -18,14 +18,17 @@
 
 import com.google.devtools.ksp.symbol.KSAnnotated
 
-internal fun KSAnnotated.hasJvmStaticAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmStatic"
+private fun KSAnnotated.hasAnnotationWithQName(qName: String) = annotations.any {
+    try {
+        it.annotationType.resolve().declaration.qualifiedName?.asString() == qName
+    } catch (illegal: IllegalStateException) {
+        // see: https://github.com/google/ksp/issues/173
+        false
+    }
 }
 
-internal fun KSAnnotated.hasJvmFieldAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmField"
-}
+internal fun KSAnnotated.hasJvmStaticAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmStatic")
 
-internal fun KSAnnotated.hasJvmDefaultAnnotation() = annotations.any {
-    it.annotationType.resolve().declaration.qualifiedName?.asString() == "kotlin.jvm.JvmDefault"
-}
+internal fun KSAnnotated.hasJvmFieldAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmField")
+
+internal fun KSAnnotated.hasJvmDefaultAnnotation() = hasAnnotationWithQName("kotlin.jvm.JvmDefault")
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
index ba27191..6c230d8 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSAsMemberOf.kt
@@ -26,6 +26,11 @@
  * Returns the type of a property as if it is member of the given [ksType].
  */
 internal fun KSPropertyDeclaration.typeAsMemberOf(resolver: Resolver, ksType: KSType): KSType {
+    if (isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        return type.resolve()
+    }
     return resolver.asMemberOf(
         property = this,
         containing = ksType
@@ -37,6 +42,11 @@
     functionDeclaration: KSFunctionDeclaration,
     ksType: KSType
 ): KSType {
+    if (functionDeclaration.isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        return type.resolve()
+    }
     val asMember = resolver.asMemberOf(
         function = functionDeclaration,
         containing = ksType
@@ -51,8 +61,15 @@
     resolver: Resolver,
     ksType: KSType
 ): KSType {
-    return resolver.asMemberOf(
-        function = this,
-        containing = ksType
-    ).returnType ?: returnType?.resolve() ?: error("cannot find return type for $this")
+    val returnType = if (isStatic()) {
+        // calling as member with a static would throw as it might be a member of the companion
+        // object
+        returnType?.resolve()
+    } else {
+        resolver.asMemberOf(
+            function = this,
+            containing = ksType
+        ).returnType
+    }
+    return returnType ?: error("cannot find return type for $this")
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
index 60beee8..206f312 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSDeclarationExt.kt
@@ -18,6 +18,7 @@
 
 import com.google.devtools.ksp.symbol.KSClassDeclaration
 import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.Modifier
 
 /**
  * Finds the class that contains this declaration and throws [IllegalStateException] if it cannot
@@ -49,3 +50,7 @@
     }
     return parent as? KSClassDeclaration
 }
+
+internal fun KSDeclaration.isStatic(): Boolean {
+    return modifiers.contains(Modifier.JAVA_STATIC) || hasJvmStaticAnnotation()
+}
\ No newline at end of file
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
index 31b2ad9..82f56d6 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KSTypeExt.kt
@@ -16,15 +16,11 @@
 
 package androidx.room.compiler.processing.ksp
 
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.javac.kotlin.typeNameFromJvmSignature
 import androidx.room.compiler.processing.tryBox
 import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.processing.Resolver
-import com.squareup.javapoet.ClassName
-import com.squareup.javapoet.ParameterizedTypeName
-import com.squareup.javapoet.TypeName
-import com.squareup.javapoet.TypeVariableName
-import com.squareup.javapoet.WildcardTypeName
 import com.google.devtools.ksp.symbol.KSDeclaration
 import com.google.devtools.ksp.symbol.KSType
 import com.google.devtools.ksp.symbol.KSTypeArgument
@@ -33,6 +29,11 @@
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Variance
 import com.squareup.javapoet.ArrayTypeName
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.ParameterizedTypeName
+import com.squareup.javapoet.TypeName
+import com.squareup.javapoet.TypeVariableName
+import com.squareup.javapoet.WildcardTypeName
 
 internal const val ERROR_PACKAGE_NAME = "androidx.room.compiler.processing.kotlin.error"
 
@@ -99,7 +100,7 @@
                 TypeVariableName.get(param.name.asString(), type.typeName(resolver).tryBox())
             }
         }
-        else -> type.typeName(resolver)
+        else -> type.typeName(resolver).tryBox()
     }
 }
 
@@ -152,4 +153,10 @@
     return this.resolve().declaration is KSTypeParameter
 }
 
-fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
\ No newline at end of file
+fun KSType.isInline() = declaration.modifiers.contains(Modifier.INLINE)
+
+internal fun KSType.withNullability(nullability: XNullability) = when (nullability) {
+    XNullability.NULLABLE -> makeNullable()
+    XNullability.NONNULL -> makeNotNullable()
+    else -> throw IllegalArgumentException("Cannot set KSType nullability to platform")
+}
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
index 7831603..6b9912e 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspArrayType.kt
@@ -56,6 +56,13 @@
                 allowPrimitives = false
             )
         }
+
+        override fun copyWithNullability(nullability: XNullability): BoxedArray {
+            return BoxedArray(
+                env = env,
+                ksType = ksType.withNullability(nullability)
+            )
+        }
     }
 
     /**
@@ -67,7 +74,15 @@
         override val componentType: KspType
     ) : KspArrayType(
         env, ksType
-    )
+    ) {
+        override fun copyWithNullability(nullability: XNullability): PrimitiveArray {
+            return PrimitiveArray(
+                env = env,
+                ksType = ksType.withNullability(nullability),
+                componentType = componentType
+            )
+        }
+    }
 
     /**
      * Factory class to create instances of [KspArrayType].
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
index e34f3ef..0932efc 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspDeclaredType.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XDeclaredType
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.tryBox
 import com.google.devtools.ksp.symbol.KSType
@@ -38,7 +39,14 @@
         }
     }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspDeclaredType {
         return this
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return KspDeclaredType(
+            env = env,
+            ksType = ksType.withNullability(nullability)
+        )
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
index c1570e1..0ebc228 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableElement.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.processing.XTypeElement
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.NO_USE_SITE
 import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.Modifier
 
 internal abstract class KspExecutableElement(
     env: KspProcessingEnv,
@@ -59,8 +60,11 @@
     }
 
     override fun isVarArgs(): Boolean {
-        return declaration.parameters.any {
-            it.isVararg
-        }
+        // in java, only the last argument can be a vararg so for suspend functions, it is never
+        // a vararg function. this would change if room generated kotlin code
+        return !declaration.modifiers.contains(Modifier.SUSPEND) &&
+            declaration.parameters.any {
+                it.isVararg
+            }
     }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
index 629d421..fcfa5de 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspExecutableParameterElement.kt
@@ -20,7 +20,6 @@
 import androidx.room.compiler.processing.XDeclaredType
 import androidx.room.compiler.processing.XEquality
 import androidx.room.compiler.processing.XExecutableParameterElement
-import androidx.room.compiler.processing.XType
 import androidx.room.compiler.processing.ksp.KspAnnotated.UseSiteFilter.Companion.METHOD_PARAMETER
 import com.google.devtools.ksp.symbol.KSValueParameter
 
@@ -38,7 +37,7 @@
     override val name: String
         get() = parameter.name?.asString() ?: "_no_param_name"
 
-    override val type: XType by lazy {
+    override val type: KspType by lazy {
         parameter.typeAsMemberOf(
             resolver = env.resolver,
             functionDeclaration = method.declaration,
@@ -51,7 +50,7 @@
         }
     }
 
-    override fun asMemberOf(other: XDeclaredType): XType {
+    override fun asMemberOf(other: XDeclaredType): KspType {
         if (method.containing.type.isSameType(other)) {
             return type
         }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
index ea0aa93..aa1add9 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspHasModifiers.kt
@@ -17,6 +17,7 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XHasModifiers
+import com.google.devtools.ksp.getVisibility
 import com.google.devtools.ksp.isOpen
 import com.google.devtools.ksp.isPrivate
 import com.google.devtools.ksp.isProtected
@@ -28,6 +29,7 @@
 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
 import com.google.devtools.ksp.symbol.Modifier
 import com.google.devtools.ksp.symbol.Origin
+import com.google.devtools.ksp.symbol.Visibility
 
 /**
  * Implementation of [XHasModifiers] for ksp declarations.
@@ -36,7 +38,9 @@
     protected val declaration: KSDeclaration
 ) : XHasModifiers {
     override fun isPublic(): Boolean {
-        return declaration.isPublic()
+        // internals are public from java but KSP's declaration.isPublic excludes them.
+        return declaration.getVisibility() == Visibility.INTERNAL ||
+            declaration.getVisibility() == Visibility.PUBLIC
     }
 
     override fun isProtected(): Boolean {
@@ -52,8 +56,7 @@
     }
 
     override fun isStatic(): Boolean {
-        return declaration.modifiers.contains(Modifier.JAVA_STATIC) ||
-            declaration.hasJvmStaticAnnotation()
+        return declaration.isStatic()
     }
 
     override fun isTransient(): Boolean {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
index 48841e32..359f413 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodElement.kt
@@ -42,13 +42,7 @@
 
     @OptIn(KspExperimental::class)
     override val name: String by lazy {
-        try {
-            env.resolver.getJvmName(declaration)
-        } catch (ignored: ClassCastException) {
-            // TODO remove this catch once that issue is fixed.
-            // workaround for https://github.com/google/ksp/issues/164
-            declaration.simpleName.asString()
-        }
+        env.resolver.safeGetJvmName(declaration)
     }
 
     override val executableType: XMethodType by lazy {
@@ -104,9 +98,11 @@
     ) {
         override val returnType: XType by lazy {
             env.wrap(
-                checkNotNull(declaration.returnType) {
-                    "return type on a method declaration cannot be null"
-                }
+                ksType = declaration.returnTypeAsMemberOf(
+                    resolver = env.resolver,
+                    ksType = containing.type.ksType
+                ),
+                originatingReference = checkNotNull(declaration.returnType)
             )
         }
         override fun isSuspendFunction() = false
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
index 0ce3d78..4ce33b0 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspMethodType.kt
@@ -44,6 +44,15 @@
         }
     }
 
+    /**
+     * Creates a MethodType where variance is inherited for java code generation.
+     *
+     * see [OverrideVarianceResolver] for details.
+     */
+    fun inheritVarianceForOverride(): XMethodType {
+        return OverrideVarianceResolver(env, this).resolve()
+    }
+
     private class KspNormalMethodType(
         env: KspProcessingEnv,
         origin: KspMethodElement,
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
index 29fed31..c006611 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspPrimitiveType.kt
@@ -16,7 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
 import androidx.room.compiler.processing.tryUnbox
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.javapoet.TypeName
@@ -35,10 +35,27 @@
     override val typeName: TypeName
         get() = ksType.typeName(env.resolver).tryUnbox()
 
-    override fun boxed(): XType {
+    override fun boxed(): KspType {
         return env.wrap(
             ksType = ksType,
             allowPrimitives = false
         )
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return when (nullability) {
+            XNullability.NONNULL -> {
+                this
+            }
+            XNullability.NULLABLE -> {
+                // primitive types cannot be nullable hence we box them.
+                boxed().makeNullable()
+            }
+            else -> {
+                // this should actually never happens as the only time this is called is from
+                // make nullable-make nonnull but we have this error here for completeness.
+                error("cannot set nullability to unknown in KSP")
+            }
+        }
+    }
 }
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 cb03dab..1292542 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
@@ -156,4 +156,27 @@
     override fun isEnum(): Boolean {
         return (ksType.declaration as? KSClassDeclaration)?.classKind == ClassKind.ENUM_CLASS
     }
+
+    abstract override fun boxed(): KspType
+
+    /**
+     * Create a copy of this type with the given nullability.
+     * This method is not called if the nullability of the type is already equal to the given
+     * nullability.
+     */
+    protected abstract fun copyWithNullability(nullability: XNullability): KspType
+
+    final override fun makeNullable(): KspType {
+        if (nullability == XNullability.NULLABLE) {
+            return this
+        }
+        return copyWithNullability(XNullability.NULLABLE)
+    }
+
+    final override fun makeNonNullable(): KspType {
+        if (nullability == XNullability.NONNULL) {
+            return this
+        }
+        return copyWithNullability(XNullability.NONNULL)
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
index 36a3e12..0690474 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspTypeArgumentType.kt
@@ -16,10 +16,11 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
-import com.squareup.javapoet.TypeName
+import androidx.room.compiler.processing.XNullability
 import com.google.devtools.ksp.symbol.KSTypeArgument
 import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.squareup.javapoet.TypeName
 
 /**
  * The typeName for type arguments requires the type parameter, hence we have a special type
@@ -37,7 +38,23 @@
         typeArg.typeName(typeParam, env.resolver)
     }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspTypeArgumentType {
         return this
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspTypeArgumentType {
+        return KspTypeArgumentType(
+            env = env,
+            typeParam = typeParam,
+            typeArg = DelegatingTypeArg(
+                original = typeArg,
+                type = typeArg.requireType().withNullability(nullability).createTypeReference()
+            )
+        )
+    }
+
+    private class DelegatingTypeArg(
+        val original: KSTypeArgument,
+        override val type: KSTypeReference
+    ) : KSTypeArgument by original
 }
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 eb88523..60339a5 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
@@ -106,8 +106,14 @@
     }
 
     private val _declaredPropertyFields by lazy {
-        declaration
-            .getDeclaredProperties()
+        val declaredProperties = declaration.getDeclaredProperties()
+        val companionProperties = declaration
+            .findCompanionObject()
+            ?.getDeclaredProperties()
+            ?.filter {
+                it.isStatic()
+            }.orEmpty()
+        (declaredProperties + companionProperties)
             .map {
                 KspFieldElement(
                     env = env,
@@ -221,7 +227,15 @@
     }
 
     private val _declaredMethods by lazy {
-        val myMethods = declaration.getDeclaredFunctions().asSequence()
+        val instanceMethods = declaration.getDeclaredFunctions().asSequence()
+        val companionMethods = declaration.findCompanionObject()
+            ?.getDeclaredFunctions()
+            ?.asSequence()
+            ?.filter {
+                it.isStatic()
+            }
+            ?: emptySequence()
+        val declaredMethods = (instanceMethods + companionMethods)
             .filterNot {
                 // filter out constructors
                 it.simpleName.asString() == name
@@ -238,15 +252,7 @@
                     declaration = it
                 )
             }.toList()
-        val companionMethods = declaration.findCompanionObject()
-            ?.let {
-                env.wrapClassDeclaration(it)
-            }?.getDeclaredMethods()
-            ?.filter {
-                it.isStatic()
-            } ?: emptyList()
-
-        myMethods + syntheticGetterSetterMethods + companionMethods
+        declaredMethods + syntheticGetterSetterMethods
     }
 
     override fun getDeclaredMethods(): List<XMethodElement> {
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
index 2bf6728..cc5a601 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/KspVoidType.kt
@@ -16,7 +16,7 @@
 
 package androidx.room.compiler.processing.ksp
 
-import androidx.room.compiler.processing.XType
+import androidx.room.compiler.processing.XNullability
 import com.google.devtools.ksp.symbol.KSType
 import com.squareup.javapoet.TypeName
 
@@ -30,16 +30,16 @@
 internal class KspVoidType(
     env: KspProcessingEnv,
     ksType: KSType,
-    private val boxed: Boolean
+    val boxed: Boolean
 ) : KspType(env, ksType) {
     override val typeName: TypeName
-        get() = if (boxed) {
+        get() = if (boxed || nullability == XNullability.NULLABLE) {
             TypeName.VOID.box()
         } else {
             TypeName.VOID
         }
 
-    override fun boxed(): XType {
+    override fun boxed(): KspType {
         return if (boxed) {
             this
         } else {
@@ -50,4 +50,12 @@
             )
         }
     }
+
+    override fun copyWithNullability(nullability: XNullability): KspType {
+        return KspVoidType(
+            env = env,
+            ksType = ksType.withNullability(nullability),
+            boxed = boxed || nullability == XNullability.NULLABLE
+        )
+    }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
new file mode 100644
index 0000000..d4f3e0e
--- /dev/null
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/OverrideVarianceResolver.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.room.compiler.processing.ksp
+
+import androidx.room.compiler.processing.XMethodType
+import androidx.room.compiler.processing.XType
+import com.google.devtools.ksp.closestClassDeclaration
+import com.google.devtools.ksp.isOpen
+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.KSTypeArgument
+import com.google.devtools.ksp.symbol.KSTypeParameter
+import com.google.devtools.ksp.symbol.KSTypeReference
+import com.google.devtools.ksp.symbol.Variance
+import com.squareup.javapoet.TypeVariableName
+
+/**
+ * When kotlin generates java code, it has some interesting rules on how variance is handled.
+ *
+ * https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#variant-generics
+ *
+ * This helper class applies that to [KspMethodType].
+ *
+ * Note that, this is only relevant when Room tries to generate overrides. For regular type
+ * operations, we prefer the variance declared in Kotlin source.
+ */
+internal class OverrideVarianceResolver(
+    private val env: KspProcessingEnv,
+    private val methodType: KspMethodType
+) {
+    fun resolve(): XMethodType {
+        val overideeElm = methodType.origin.findOverridee()
+        return ResolvedMethodType(
+            // kotlin does not touch return type
+            returnType = methodType.returnType,
+            parameterTypes = methodType.parameterTypes.mapIndexed { index, xType ->
+                xType.maybeInheritVariance(overideeElm?.parameterTypes?.getOrNull(index))
+            },
+            typeVariableNames = methodType.typeVariableNames
+        )
+    }
+
+    private fun XType.maybeInheritVariance(
+        overridee: XType?
+    ): XType {
+        return if (this is KspType) {
+            this.inheritVariance(overridee as? KspType)
+        } else {
+            this
+        }
+    }
+
+    private fun KspType.inheritVariance(overridee: KspType?): KspType {
+        return env.wrap(
+            ksType = ksType.inheritVariance(overridee?.ksType),
+            allowPrimitives = this is KspPrimitiveType || (this is KspVoidType && !this.boxed)
+        )
+    }
+
+    /**
+     * Finds the method type for the method element that was overridden by this method element.
+     */
+    private fun KspMethodElement.findOverridee(): KspMethodType? {
+        // now find out if this is overriding a method
+        val funDeclaration = declaration
+        val declaredIn = funDeclaration.closestClassDeclaration() ?: return null
+        if (declaredIn == containing.declaration) {
+            // if declared in the same class, skip
+            return null
+        }
+        // it is declared in a super type, get that
+        val overrideeElm = KspMethodElement.create(
+            env = env,
+            containing = env.wrapClassDeclaration(declaredIn),
+            declaration = funDeclaration.findOverridee() ?: funDeclaration
+        )
+        val containing = overrideeElm.enclosingTypeElement.type as? KspDeclaredType ?: return null
+        return KspMethodType.create(
+            env = env,
+            origin = overrideeElm,
+            containing = containing
+        )
+    }
+
+    /**
+     * Update the variance of the arguments of this type based on the types declaration.
+     *
+     * For instance, in List<Foo>, it actually inherits the `out` variance from `List`.
+     */
+    private fun KSType.inheritVariance(
+        overridee: KSType?
+    ): KSType {
+        if (arguments.isEmpty()) return this
+        // need to swap arguments with the variance from declaration
+        val newArguments = arguments.mapIndexed { index, typeArg ->
+            val param = declaration.typeParameters.getOrNull(index)
+            val overrideeArg = overridee?.arguments?.getOrNull(index)
+            typeArg.inheritVariance(overrideeArg, param)
+        }
+        return this.replace(newArguments)
+    }
+
+    private fun KSTypeReference.inheritVariance(
+        overridee: KSTypeReference?
+    ): KSTypeReference {
+        return resolve()
+            .inheritVariance(overridee = overridee?.resolve())
+            .createTypeReference()
+    }
+
+    private fun KSTypeArgument.inheritVariance(
+        overridee: KSTypeArgument?,
+        param: KSTypeParameter?
+    ): KSTypeArgument {
+        if (param == null) {
+            return this
+        }
+        val myTypeRef = type ?: return this
+
+        if (variance != Variance.INVARIANT) {
+            return env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee?.type),
+                variance = variance
+            )
+        }
+        if (overridee != null) {
+            // get it from overridee
+            return env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee.type),
+                variance = if (overridee.variance == Variance.STAR) {
+                    Variance.COVARIANT
+                } else {
+                    overridee.variance
+                }
+            )
+        }
+        // Now we need to guess from this type. If the type is final, it does not inherit unless
+        // the parameter is CONTRAVARIANT (`in`).
+        val myType = myTypeRef.resolve()
+        val shouldInherit = param.variance == Variance.CONTRAVARIANT ||
+            when (val decl = myType.declaration) {
+                is KSClassDeclaration -> {
+                    decl.isOpen() ||
+                        decl.classKind == ClassKind.ENUM_CLASS ||
+                        decl.classKind == ClassKind.OBJECT
+                }
+                else -> true
+            }
+        return if (shouldInherit) {
+            env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee = null),
+                variance = param.variance
+            )
+        } else {
+            env.resolver.getTypeArgument(
+                typeRef = myTypeRef.inheritVariance(overridee = null),
+                variance = variance
+            )
+        }
+    }
+
+    /**
+     * [XMethodType] implementation where variance of types are resolved.
+     */
+    private class ResolvedMethodType(
+        override val returnType: XType,
+        override val parameterTypes: List<XType>,
+        override val typeVariableNames: List<TypeVariableName>
+    ) : XMethodType
+}
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 4901b29..e8a80d4 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
@@ -17,11 +17,15 @@
 package androidx.room.compiler.processing.ksp
 
 import androidx.room.compiler.processing.XExecutableElement
+import androidx.room.compiler.processing.XMethodElement
 import androidx.room.compiler.processing.ksp.synthetic.KspSyntheticPropertyMethodElement
-import com.google.devtools.ksp.closestClassDeclaration
-import com.google.devtools.ksp.getAllSuperTypes
+import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.processing.Resolver
 import com.google.devtools.ksp.symbol.KSDeclaration
+import com.google.devtools.ksp.symbol.KSFunctionDeclaration
+import com.google.devtools.ksp.symbol.KSPropertyAccessor
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
+import com.google.devtools.ksp.symbol.Origin
 
 internal fun Resolver.findClass(qName: String) = getClassDeclarationByName(
     getKSNameFromString(qName)
@@ -42,8 +46,8 @@
 }
 
 internal fun Resolver.overrides(
-    overriderElement: XExecutableElement,
-    overrideeElement: XExecutableElement
+    overriderElement: XMethodElement,
+    overrideeElement: XMethodElement
 ): Boolean {
     // in addition to functions declared in kotlin, we also synthesize getter/setter functions for
     // properties which means we cannot simply send the declaration to KSP for override check
@@ -55,16 +59,82 @@
     if (overriderElement.parameters.size != overrideeElement.parameters.size) {
         return false
     }
-    val ksOverrider = overriderElement.getDeclarationForOverride()
-    val ksOverridee = overrideeElement.getDeclarationForOverride()
-    if (!overrides(ksOverrider, ksOverridee)) {
+    // do a quick check on name before doing the more expensive operations
+    if (overriderElement.name != overrideeElement.name) {
         return false
     }
-    // TODO Workaround for https://github.com/google/ksp/issues/123
-    //  remove once that bug is fixed
-    val subClass = ksOverrider.closestClassDeclaration() ?: return false
-    val superClass = ksOverridee.closestClassDeclaration() ?: return false
-    return subClass.getAllSuperTypes().any {
-        it.declaration.closestClassDeclaration() == superClass
+    val ksOverrider = overriderElement.getDeclarationForOverride()
+    val ksOverridee = overrideeElement.getDeclarationForOverride()
+    if (overrides(ksOverrider, ksOverridee)) {
+        return true
+    }
+    // workaround for: https://github.com/google/ksp/issues/175
+    if (ksOverrider is KSFunctionDeclaration && ksOverridee is KSFunctionDeclaration) {
+        return ksOverrider.overrides(ksOverridee)
+    }
+    if (ksOverrider is KSPropertyDeclaration && ksOverridee is KSPropertyDeclaration) {
+        return ksOverrider.overrides(ksOverridee)
+    }
+    return false
+}
+
+private fun KSFunctionDeclaration.overrides(other: KSFunctionDeclaration): Boolean {
+    val overridee = try {
+        findOverridee()
+    } catch (ignored: ClassCastException) {
+        // workaround for https://github.com/google/ksp/issues/164
+        null
+    }
+    if (overridee == other) {
+        return true
+    }
+    return overridee?.overrides(other) ?: false
+}
+
+private fun KSPropertyDeclaration.overrides(other: KSPropertyDeclaration): Boolean {
+    val overridee = try {
+        findOverridee()
+    } catch (ex: NoSuchElementException) {
+        // workaround for https://github.com/google/ksp/issues/174
+        null
+    }
+    if (overridee == other) {
+        return true
+    }
+    return overridee?.overrides(other) ?: false
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+    declaration: KSFunctionDeclaration
+): String {
+    if (declaration.origin == Origin.JAVA) {
+        // https://github.com/google/ksp/issues/170
+        return declaration.simpleName.asString()
+    }
+    return try {
+        getJvmName(declaration)
+    } catch (ignored: ClassCastException) {
+        // TODO remove this catch once that issue is fixed.
+        // workaround for https://github.com/google/ksp/issues/164
+        return declaration.simpleName.asString()
+    }
+}
+
+@OptIn(KspExperimental::class)
+internal fun Resolver.safeGetJvmName(
+    accessor: KSPropertyAccessor,
+    fallback: () -> String
+): String {
+    if (accessor.origin == Origin.JAVA) {
+        // https://github.com/google/ksp/issues/170
+        return fallback()
+    }
+    return try {
+        getJvmName(accessor)
+    } catch (ignored: ClassCastException) {
+        // TODO remove this catch once that issue is fixed.
+        // workaround for https://github.com/google/ksp/issues/164
+        return fallback()
     }
 }
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
index 68121e3..f2b09cb 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticContinuationParameterElement.kt
@@ -46,8 +46,17 @@
         filter = NO_USE_SITE
     ) {
 
-    override val name: String
-        get() = "_syntheticContinuation"
+    override val name: String by lazy {
+        // kotlin names this as pN where N is the # of arguments
+        // seems like kapt doesn't handle conflicts with declared arguments but we should
+        val desiredName = "p${containing.declaration.parameters.size}"
+
+        if (containing.declaration.parameters.none { it.name?.asString() == desiredName }) {
+            desiredName
+        } else {
+            "_syntheticContinuation"
+        }
+    }
 
     override val equalityItems: Array<out Any?> by lazy {
         arrayOf("continuation", containing)
diff --git a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
index 868a69c..bf906fd 100644
--- a/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
+++ b/room/compiler-processing/src/main/java/androidx/room/compiler/processing/ksp/synthetic/KspSyntheticPropertyMethodElement.kt
@@ -35,6 +35,7 @@
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import androidx.room.compiler.processing.ksp.KspTypeElement
 import androidx.room.compiler.processing.ksp.overrides
+import androidx.room.compiler.processing.ksp.safeGetJvmName
 import com.google.devtools.ksp.KspExperimental
 import com.google.devtools.ksp.symbol.KSPropertyAccessor
 import java.util.Locale
@@ -121,15 +122,11 @@
         @OptIn(KspExperimental::class)
         override val name: String by lazy {
             field.declaration.getter?.let {
-                return@lazy env.resolver.getJvmName(it)
+                return@lazy env.resolver.safeGetJvmName(it) {
+                    computeGetterName(field.name)
+                }
             }
-            // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
-            val propName = field.name
-            if (propName.startsWith("is")) {
-                propName
-            } else {
-                "get${propName.capitalize(Locale.US)}"
-            }
+            computeGetterName(field.name)
         }
 
         override val returnType: XType by lazy {
@@ -150,6 +147,17 @@
                 field = field.copyTo(newContainer)
             )
         }
+
+        companion object {
+            private fun computeGetterName(propName: String): String {
+                // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+                return if (propName.startsWith("is")) {
+                    propName
+                } else {
+                    "get${propName.capitalize(Locale.US)}"
+                }
+            }
+        }
     }
 
     internal class Setter(
@@ -176,15 +184,11 @@
         @OptIn(KspExperimental::class)
         override val name: String by lazy {
             field.declaration.setter?.let {
-                return@lazy env.resolver.getJvmName(it)
+                return@lazy env.resolver.safeGetJvmName(it) {
+                    computeSetterName(field.name)
+                }
             }
-            // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
-            val propName = field.name
-            if (propName.startsWith("is")) {
-                "set${propName.substring(2)}"
-            } else {
-                "set${propName.capitalize(Locale.US)}"
-            }
+            computeSetterName(field.name)
         }
 
         override val returnType: XType by lazy {
@@ -238,5 +242,16 @@
                 return "method parameter"
             }
         }
+
+        companion object {
+            private fun computeSetterName(propName: String): String {
+                // see https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#properties
+                return if (propName.startsWith("is")) {
+                    "set${propName.substring(2)}"
+                } else {
+                    "set${propName.capitalize(Locale.US)}"
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
index e0f2844..49ad742 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/MethodSpecHelperTest.kt
@@ -16,10 +16,13 @@
 
 package androidx.room.compiler.processing
 
-import androidx.room.compiler.processing.javac.JavacProcessingEnv
+import androidx.room.compiler.processing.javac.JavacMethodElement
+import androidx.room.compiler.processing.javac.JavacTypeElement
 import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.XTestInvocation
+import androidx.room.compiler.processing.util.javaTypeUtils
 import androidx.room.compiler.processing.util.runKaptTest
-import androidx.room.compiler.processing.util.runKspTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import com.google.auto.common.MoreTypes
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.MethodSpec
@@ -27,7 +30,6 @@
 import javax.lang.model.element.ExecutableElement
 import javax.lang.model.element.Modifier
 import javax.lang.model.type.DeclaredType
-import javax.lang.model.util.ElementFilter
 import javax.lang.model.util.Types
 
 class MethodSpecHelperTest {
@@ -101,10 +103,35 @@
                     return 3;
                 }
 
+                open fun boxedLongArrayReturn(): Array<Long> {
+                    TODO();
+                }
+
+                open fun boxedIntArrayReturn(): Array<Int> {
+                    TODO();
+                }
+
+                protected open fun listArg(r:List<String>) {
+                }
+
+                open suspend fun suspendUnitFun() {
+                }
+
+                protected open suspend fun suspendBasic(p0:Int):String {
+                    TODO()
+                }
+
+                protected open suspend fun suspendVarArg(p0:Int, vararg p1:String):Long {
+                    TODO()
+                }
+
                 protected open fun <R> typeArgs(r:R): R {
                     return r;
                 }
 
+                internal open fun internalFun() {
+                }
+
                 @Throws(Exception::class)
                 protected open fun throwsException() {
                 }
@@ -114,47 +141,229 @@
         overridesCheck(source)
     }
 
-    private fun overridesCheck(source: Source) {
+    @Test
+    fun variance() {
+        // check our override impl matches javapoet
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface I1<in T>
+            interface I2<out T>
+            interface I3<T>
+            enum class Lang {
+               ES,
+               EN;
+            }
+            class Box<out T>(val value: T)
+
+            interface Base
+            class Derived : Base
+
+            interface Baz : MyInterface<String> {
+                fun boxDerived(value: Derived): Box<Derived> = Box(value)
+                fun unboxBase(box: Box<Base>): Base = box.value
+                fun unboxString(box: Box<String>): String = box.value
+                fun findByLanguages(langs: Set<Lang>): List<String>
+                fun f1(args : I1<String>): I1<String>
+                fun f2(args : I2<String>): I2<String>
+                fun f3(args : I3<String>): I3<String>
+                suspend fun s1(args : I1<String>): I1<String>
+                suspend fun s2(args : I2<String>): I2<String>
+                suspend fun s3(args : I3<String>): I3<String>
+                suspend fun s4(args : I1<String>): String
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_openType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            open class Book(val id:Int)
+            interface Baz : MyInterface<Book> {
+                fun myList(args: List<Book>):Unit
+                override fun receiveList(argsInParent : List<Book>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_finalType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface Baz : MyInterface<String> {
+                fun myList(args: List<String>):Unit
+                override fun receiveList(argsInParent : List<String>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source, ignoreInheritedMethods = true)
+    }
+
+    @Test
+    fun inheritedVariance_enumType() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            enum class EnumType {
+                FOO,
+                BAR;
+            }
+            interface MyInterface<T> {
+                fun receiveList(argsInParent : List<T>):Unit
+                suspend fun suspendReturnList(arg1:Int, arg2:String):List<T>
+            }
+            interface Baz : MyInterface<EnumType> {
+                fun myList(args: List<EnumType>):Unit
+                override fun receiveList(argsInParent : List<EnumType>):Unit
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun inheritedVariance_multiLevel() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar;
+            interface GrandParent<T> {
+                fun receiveList(list : List<T>): Unit
+                suspend fun suspendReceiveList(list : List<T>): Unit
+                suspend fun suspendReturnList(): List<T>
+            }
+            interface Parent: GrandParent<Number> {
+            }
+            interface Baz : Parent {
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    @Test
+    fun primitiveOverrides() {
+        val source = Source.kotlin(
+            "Foo.kt",
+            """
+            package foo.bar
+            data class LongFoo(val id: Long, val description: String)
+            /* Interface with generics only */
+            interface MyInterface<Key, Value> {
+                fun getItem(id: Key): Value?
+                //fun delete(id: Key)
+                //fun getFirstItemId(): Key
+            }
+            /* Interface with non-generics and generics */
+            interface Baz : MyInterface<Long, LongFoo> {
+                override fun getItem(id: Long): LongFoo?
+                //override fun delete(id: Long)
+                //fun insert(item: LongFoo)
+                //override fun getFirstItemId(): Long
+            }
+            """.trimIndent()
+        )
+        overridesCheck(source)
+    }
+
+    private fun overridesCheck(source: Source, ignoreInheritedMethods: Boolean = false) {
         // first build golden image with Java processor so we can use JavaPoet's API
-        val golden = buildMethodsViaJavaPoet(source)
-        runKspTest(
-            sources = listOf(source),
-            succeed = true
+        val golden = buildMethodsViaJavaPoet(source, ignoreInheritedMethods)
+        runProcessorTestIncludingKsp(
+            sources = listOf(source)
         ) { invocation ->
-            val element = invocation.processingEnv.requireTypeElement("foo.bar.Baz")
-            element.getDeclaredMethods().filter {
-                // TODO b/171572318
-                !invocation.isKsp || it.name != "throwsException"
-            }.forEachIndexed { index, method ->
-                val subject = MethodSpecHelper.overridingWithFinalParams(
-                    method,
-                    element.type
-                ).build().toString()
-                assertThat(subject).isEqualTo(golden[index])
+            val (target, methods) = invocation.getOverrideTestTargets(ignoreInheritedMethods)
+            methods.forEachIndexed { index, method ->
+
+                if (invocation.isKsp && method.name == "throwsException") {
+                    // TODO b/171572318
+                } else {
+                    val subject = MethodSpecHelper.overridingWithFinalParams(
+                        method,
+                        target.type
+                    ).build().toString()
+                    assertThat(subject).isEqualTo(golden[index])
+                }
             }
         }
     }
 
-    private fun buildMethodsViaJavaPoet(source: Source): List<String> {
+    private fun buildMethodsViaJavaPoet(
+        source: Source,
+        ignoreInheritedMethods: Boolean
+    ): List<String> {
         lateinit var result: List<String>
         runKaptTest(
             sources = listOf(source),
             succeed = true
-        ) {
-            val processingEnv = (it.processingEnv as JavacProcessingEnv)
-            val element = processingEnv.elementUtils.getTypeElement("foo.bar.Baz")
-            result = ElementFilter.methodsIn(element.enclosedElements)
+        ) { invocation ->
+            val (target, methods) = invocation.getOverrideTestTargets(
+                ignoreInheritedMethods
+            )
+            val element = (target as JavacTypeElement).element
+            result = methods
                 .map {
+                    (it as JavacMethodElement).element
+                }.map {
                     generateFromJavapoet(
                         it,
                         MoreTypes.asDeclared(element.asType()),
-                        processingEnv.typeUtils
+                        invocation.javaTypeUtils
                     ).build().toString()
                 }
         }
         return result
     }
 
+    /**
+     * Get test targets. There is an edge case where it is not possible to implement an interface
+     * in java, b/174313780. [ignoreInheritedMethods] helps avoid that case.
+     */
+    private fun XTestInvocation.getOverrideTestTargets(
+        ignoreInheritedMethods: Boolean
+    ): Pair<XTypeElement, List<XMethodElement>> {
+        val objectMethodNames = processingEnv
+            .requireTypeElement("java.lang.Object")
+            .getAllNonPrivateInstanceMethods()
+            .map {
+                it.name
+            }
+        val target = processingEnv.requireTypeElement("foo.bar.Baz")
+        val methods = if (ignoreInheritedMethods) {
+            target.getDeclaredMethods().filter { !it.isStatic() }
+        } else {
+            target.getAllNonPrivateInstanceMethods()
+        }
+        val selectedMethods = methods.filter {
+            it.isOverrideableIgnoringContainer()
+        }.filterNot {
+            it.name in objectMethodNames
+        }
+        return target to selectedMethods
+    }
+
     private fun generateFromJavapoet(
         method: ExecutableElement,
         owner: DeclaredType,
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
index d1e5326..d84af6e 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XElementTest.kt
@@ -23,6 +23,7 @@
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
 import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import com.google.common.truth.Truth.assertThat
 import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.TypeName
@@ -35,7 +36,7 @@
 class XElementTest {
     @Test
     fun modifiers() {
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             listOf(
                 Source.java(
                     "foo.bar.Baz",
@@ -145,7 +146,7 @@
                 }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             listOf(genericBase, boundedChild)
         ) {
             fun validateElement(element: XTypeElement, tTypeName: TypeName, rTypeName: TypeName) {
@@ -172,7 +173,12 @@
             }
             validateElement(
                 element = it.processingEnv.requireTypeElement("foo.bar.Base"),
-                tTypeName = TypeVariableName.get("T"),
+                tTypeName = if (it.isKsp) {
+                    // when inheritance resolution happens, KSP resolves them to object
+                    TypeName.OBJECT
+                } else {
+                    TypeVariableName.get("T")
+                },
                 rTypeName = TypeVariableName.get("R")
             )
             validateElement(
@@ -204,7 +210,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -251,7 +257,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("java.lang.Object")
@@ -274,7 +280,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             sources = listOf(subject)
         ) {
             val inner = ClassName.get("foo.bar", "Baz.Inner")
@@ -311,7 +317,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
index 5caee8c..a270b74 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XExecutableElementTest.kt
@@ -98,6 +98,26 @@
     }
 
     @Test
+    fun isVarArgs_kotlin() {
+        val subject = Source.kotlin(
+            "Subject.kt",
+            """
+            interface Subject {
+                fun method(vararg inputs: String)
+                suspend fun suspendMethod(vararg inputs: String);
+            }
+            """.trimIndent()
+        )
+        runProcessorTestIncludingKsp(
+            sources = listOf(subject)
+        ) {
+            val element = it.processingEnv.requireTypeElement("Subject")
+            assertThat(element.getMethod("method").isVarArgs()).isTrue()
+            assertThat(element.getMethod("suspendMethod").isVarArgs()).isFalse()
+        }
+    }
+
+    @Test
     fun kotlinDefaultImpl() {
         val subject = Source.kotlin(
             "Baz.kt",
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
index 46b1037..859e553 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/XNullabilityTest.kt
@@ -24,7 +24,9 @@
 import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.getParameter
 import androidx.room.compiler.processing.util.runProcessorTest
+import androidx.room.compiler.processing.util.runProcessorTestIncludingKsp
 import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.TypeName
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -66,6 +68,7 @@
             }
             """.trimIndent()
         )
+        // TODO run with KSP once https://github.com/google/ksp/issues/167 is fixed
         runProcessorTest(
             sources = listOf(source)
         ) {
@@ -161,7 +164,7 @@
             }
             """.trimIndent()
         )
-        runProcessorTest(
+        runProcessorTestIncludingKsp(
             sources = listOf(source)
         ) {
             val element = it.processingEnv.requireTypeElement("foo.bar.Baz")
@@ -251,4 +254,141 @@
             }
         }
     }
+
+    @Test
+    fun changeNullability_primitives() {
+        runProcessorTestIncludingKsp { invocation ->
+            PRIMITIVE_TYPES.forEach { primitiveTypeName ->
+                val primitive = invocation.processingEnv.requireType(primitiveTypeName)
+                assertThat(primitive.nullability).isEqualTo(NONNULL)
+                val nullable = primitive.makeNullable()
+                assertThat(nullable.nullability).isEqualTo(NULLABLE)
+                assertThat(nullable.typeName).isEqualTo(primitiveTypeName.box())
+
+                // When a boxed primitive is marked as non-null, it should stay as boxed primitive
+                // Even though this might be counter-intutive (because making it nullable will box
+                // it) it is more consistent as it is completely valid to annotate a boxed primitive
+                // with non-null while you cannot annoteted a primitive with nullable as it is not
+                // a valid state.
+                val boxedPrimitive = invocation.processingEnv.requireType(primitiveTypeName.box())
+                val nonNull = boxedPrimitive.makeNonNullable()
+                assertThat(nonNull.nullability).isEqualTo(NONNULL)
+                assertThat(nonNull.typeName).isEqualTo(primitiveTypeName.box())
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_typeArguments() {
+        // we need to make sure we don't convert type arguments into primitives!!
+        val kotlinSrc = Source.kotlin(
+            "KotlinClas.kt",
+            """
+                class KotlinClass(val subject: List<Int?>)
+            """.trimIndent()
+        )
+        val javaSrc = Source.java(
+            "JavaClass",
+            """
+                class JavaClass {
+                    java.util.List<Integer> subject;
+                }
+            """.trimIndent()
+        )
+        runProcessorTestIncludingKsp(sources = listOf(javaSrc, kotlinSrc)) { invocation ->
+            listOf("KotlinClass", "JavaClass").forEach {
+                val subject = invocation.processingEnv.requireTypeElement(it)
+                    .getField("subject").type
+                check(subject.isDeclared())
+                val typeArg = subject.typeArguments.first()
+                assertThat(typeArg.typeName).isEqualTo(TypeName.INT.box())
+                typeArg.makeNonNullable().let {
+                    assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+                    assertThat(it.nullability).isEqualTo(XNullability.NONNULL)
+                }
+                typeArg.makeNonNullable().makeNullable().let {
+                    assertThat(it.typeName).isEqualTo(TypeName.INT.box())
+                    assertThat(it.nullability).isEqualTo(NULLABLE)
+                }
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_declared() {
+        runProcessorTestIncludingKsp { invocation ->
+            val subject = invocation.processingEnv.requireType("java.util.List")
+            subject.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.isDeclared()).isTrue()
+            }
+            subject.makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isDeclared()).isTrue()
+            }
+            // ksp defaults to non-null so we do double conversion here to ensure it flips
+            // nullability
+            subject.makeNullable().makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isDeclared()).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun changeNullability_arrayTypes() {
+        runProcessorTestIncludingKsp { invocation ->
+            val subject = invocation.processingEnv.getArrayType(
+                invocation.processingEnv.requireType("java.util.List")
+            )
+            subject.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.isArray()).isTrue()
+            }
+            subject.makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isArray()).isTrue()
+            }
+            // ksp defaults to non-null so we do double conversion here to ensure it flips
+            // nullability
+            subject.makeNullable().makeNonNullable().let {
+                assertThat(it.nullability).isEqualTo(NONNULL)
+                assertThat(it.isArray()).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun makeNullable_void() {
+        val src = Source.java(
+            "Foo.java",
+            """
+            class Foo {
+                void subject() {}
+            }
+            """.trimIndent()
+        )
+        runProcessorTestIncludingKsp(sources = listOf(src)) { invocation ->
+            val voidType = invocation.processingEnv.requireTypeElement("Foo")
+                .getMethod("subject").returnType
+            assertThat(voidType.typeName).isEqualTo(TypeName.VOID)
+            voidType.makeNullable().let {
+                assertThat(it.nullability).isEqualTo(NULLABLE)
+                assertThat(it.typeName).isEqualTo(TypeName.VOID.box())
+            }
+        }
+    }
+
+    companion object {
+        val PRIMITIVE_TYPES = listOf(
+            TypeName.BOOLEAN,
+            TypeName.BYTE,
+            TypeName.SHORT,
+            TypeName.INT,
+            TypeName.LONG,
+            TypeName.CHAR,
+            TypeName.FLOAT,
+            TypeName.DOUBLE,
+        )
+    }
 }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
index f53ce78..1bc7787 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KSAsMemberOfTest.kt
@@ -22,8 +22,10 @@
 import androidx.room.compiler.processing.util.Source
 import androidx.room.compiler.processing.util.className
 import androidx.room.compiler.processing.util.getField
+import androidx.room.compiler.processing.util.getMethod
 import androidx.room.compiler.processing.util.runKspTest
 import com.google.common.truth.Truth.assertThat
+import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.ParameterizedTypeName
 import com.squareup.javapoet.TypeName
 import org.junit.Test
@@ -174,4 +176,53 @@
             }
         }
     }
+
+    @Test
+    fun asMemberOfStatics() {
+        val kotlinSrc = Source.kotlin(
+            "KotlinClass.kt",
+            """
+            class KotlinClass {
+                companion object {
+                    @JvmStatic
+                    var staticProp: String = ""
+                    @JvmStatic
+                    fun staticFun(x:Int) {}
+                }
+            }
+            """.trimIndent()
+        )
+        val javaSrc = Source.java(
+            "JavaClass",
+            """
+            class JavaClass {
+                void staticFun(int x) {}
+                static String staticProp;
+            }
+            """.trimIndent()
+        )
+        runKspTest(sources = listOf(kotlinSrc, javaSrc)) { invocation ->
+            listOf("KotlinClass", "JavaClass").forEach {
+                val typeElement = invocation.processingEnv.requireTypeElement(it)
+                typeElement.getMethod("staticFun").let { staticFun ->
+                    val asMember = staticFun.asMemberOf(typeElement.type)
+                    assertThat(asMember.returnType.typeName).isEqualTo(TypeName.VOID)
+                    assertThat(
+                        asMember.parameterTypes.single().typeName
+                    ).isEqualTo(TypeName.INT)
+                    // different codepath, execute it as well
+                    assertThat(
+                        staticFun.parameters.single().asMemberOf(typeElement.type).typeName
+                    ).isEqualTo(TypeName.INT)
+                }
+                typeElement.getField("staticProp").let { staticProp ->
+                    assertThat(
+                        staticProp.asMemberOf(typeElement.type).typeName
+                    ).isEqualTo(
+                        ClassName.get(String::class.java)
+                    )
+                }
+            }
+        }
+    }
 }
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
index 10c8f93..437bdc3 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/ksp/KspTypeElementTest.kt
@@ -440,6 +440,9 @@
         )
         runKspTest(sources = listOf(src), succeed = true) { invocation ->
             val subject = invocation.processingEnv.requireTypeElement("CompanionSubject")
+            assertThat(subject.getAllFieldNames()).containsExactly(
+                "mutableStatic", "immutableStatic"
+            )
             assertThat(subject.getDeclaredMethods().names()).containsExactly(
                 "getMutableStatic", "setMutableStatic", "getImmutableStatic"
             )
diff --git a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
index 0bc2a7d..1e24f10 100644
--- a/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
+++ b/room/compiler-processing/src/test/java/androidx/room/compiler/processing/util/XTestInvocationExt.kt
@@ -20,9 +20,13 @@
 import androidx.room.compiler.processing.ksp.KspProcessingEnv
 import com.google.devtools.ksp.processing.Resolver
 import javax.lang.model.util.Elements
+import javax.lang.model.util.Types
 
 val XTestInvocation.kspResolver: Resolver
     get() = (processingEnv as KspProcessingEnv).resolver
 
 val XTestInvocation.javaElementUtils: Elements
-    get() = (processingEnv as JavacProcessingEnv).elementUtils
\ No newline at end of file
+    get() = (processingEnv as JavacProcessingEnv).elementUtils
+
+val XTestInvocation.javaTypeUtils: Types
+    get() = (processingEnv as JavacProcessingEnv).typeUtils
\ No newline at end of file
diff --git a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
index f1acb65..d16de8c 100644
--- a/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
+++ b/security/crypto/src/androidTest/java/androidx/security/crypto/EncryptedSharedPreferencesTest.java
@@ -37,7 +37,6 @@
 import com.google.crypto.tink.aead.AesGcmKeyManager;
 import com.google.crypto.tink.daead.AesSivKeyManager;
 import com.google.crypto.tink.daead.DeterministicAeadConfig;
-import com.google.crypto.tink.daead.DeterministicAeadFactory;
 import com.google.crypto.tink.integration.android.AndroidKeysetManager;
 import com.google.crypto.tink.subtle.Base64;
 
@@ -376,8 +375,8 @@
                 .withMasterKeyUri(KEYSTORE_PATH_URI + "_androidx_security_master_key_")
                 .build().getKeysetHandle();
 
-        DeterministicAead deterministicAead = DeterministicAeadFactory.getPrimitive(
-                daeadKeysetHandle);
+        DeterministicAead deterministicAead =
+                daeadKeysetHandle.getPrimitive(DeterministicAead.class);
         byte[] encryptedKey = deterministicAead.encryptDeterministically(testKey.getBytes(UTF_8),
                 tinkTestPrefs.getBytes());
         String encodedKey = Base64.encode(encryptedKey);
diff --git a/settings.gradle b/settings.gradle
index c04ee3b..a688c1c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -222,6 +222,7 @@
 includeProject(":compose:material:material-icons-core", "compose/material/material-icons-core", [BuildType.COMPOSE])
 includeProject(":compose:material:material-icons-core:material-icons-core-samples", "compose/material/material-icons-core/samples", [BuildType.COMPOSE])
 includeProject(":compose:material:material-icons-extended", "compose/material/material-icons-extended", [BuildType.COMPOSE])
+includeProject(":compose:material:material-ripple", "compose/material/material-ripple", [BuildType.COMPOSE])
 includeProject(":compose:material:material:icons:generator", "compose/material/material/icons/generator", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-demos", "compose/material/material/integration-tests/material-demos", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-studies", "compose/material/material/integration-tests/material-studies", [BuildType.COMPOSE])
diff --git a/studiow b/studiow
new file mode 100755
index 0000000..577071b
--- /dev/null
+++ b/studiow
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+if [ -n "$1" ]; then
+  export ANDROIDX_PROJECTS=${1^^}
+else
+  export ANDROIDX_PROJECTS=MAIN
+  echo "Supported projects sets include:"
+  echo "- MAIN for non-Compose Jetpack libraries"
+  echo "- COMPOSE for Compose and dependencies"
+  echo "- FLAN for Fragment, Lifecycle, Activity, and Navigation"
+  echo "- ALL for all libraries"
+  echo
+  echo "No project set specified, using MAIN..."
+fi
+shift
+source gradlew studio "$@"
+
diff --git a/wear/wear-watchface-client/api/current.txt b/wear/wear-watchface-client/api/current.txt
index 5409f896..ee7fa64 100644
--- a/wear/wear-watchface-client/api/current.txt
+++ b/wear/wear-watchface-client/api/current.txt
@@ -35,8 +35,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/public_plus_experimental_current.txt b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
index 56fd228..5b9d604 100644
--- a/wear/wear-watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface-client/api/public_plus_experimental_current.txt
@@ -35,8 +35,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/api/restricted_current.txt b/wear/wear-watchface-client/api/restricted_current.txt
index 2870cb3..be317d9 100644
--- a/wear/wear-watchface-client/api/restricted_current.txt
+++ b/wear/wear-watchface-client/api/restricted_current.txt
@@ -35,8 +35,8 @@
     method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> getComplicationState();
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
-    method public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap? takeComplicationScreenshot(int complicationId, androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.complications.data.ComplicationData complicationData, androidx.wear.watchface.style.UserStyle? userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract long previewReferenceTimeMillis;
     property public abstract androidx.wear.watchface.style.UserStyleSchema userStyleSchema;
@@ -56,7 +56,7 @@
     method public void performAmbientTick();
     method public void sendTouchEvent(int xPosition, int yPosition, @androidx.wear.watchface.client.TapType int tapType);
     method public void setSystemState(androidx.wear.watchface.data.SystemState systemState);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     property public abstract java.util.List<androidx.wear.watchface.client.InteractiveWatchFaceSysUiClient.ContentDescriptionLabel> contentDescriptionLabels;
     property public abstract String instanceId;
     property public abstract long previewReferenceTimeMillis;
@@ -90,7 +90,8 @@
     method public long getPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
     method public void setUserStyle(androidx.wear.watchface.style.UserStyle userStyle);
-    method public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
+    method public void setUserStyle(java.util.Map<java.lang.String,java.lang.String> userStyle);
+    method @RequiresApi(27) public android.graphics.Bitmap takeWatchFaceScreenshot(androidx.wear.watchface.RenderParameters renderParameters, @IntRange(from=0, to=100) int compressionQuality, long calendarTimeMillis, androidx.wear.watchface.style.UserStyle? userStyle, java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData>? idAndComplicationData);
     method public void updateComplicationData(java.util.Map<java.lang.Integer,? extends androidx.wear.complications.data.ComplicationData> idToComplicationData);
     property public abstract java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationState> complicationState;
     property public abstract String instanceId;
diff --git a/wear/wear-watchface-client/build.gradle b/wear/wear-watchface-client/build.gradle
index 4b4eaef..b5c1d3f 100644
--- a/wear/wear-watchface-client/build.gradle
+++ b/wear/wear-watchface-client/build.gradle
@@ -51,7 +51,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 27
+        minSdkVersion 25
     }
     sourceSets.androidTest.assets.srcDirs +=
             project.rootDir.absolutePath + "/../../golden/wear/wear-watchface-client"
diff --git a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index 96889ba..7a6a332 100644
--- a/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/wear-watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -40,9 +40,16 @@
 import androidx.wear.watchface.client.WatchFaceControlClientImpl
 import androidx.wear.watchface.control.WatchFaceControlService
 import androidx.wear.watchface.data.ComplicationBoundsType
+import androidx.wear.watchface.samples.BLUE_STYLE
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
+import androidx.wear.watchface.samples.COMPLICATIONS_STYLE_SETTING
+import androidx.wear.watchface.samples.DRAW_HOUR_PIPS_STYLE_SETTING
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
 import androidx.wear.watchface.samples.ExampleCanvasAnalogWatchFaceService
+import androidx.wear.watchface.samples.GREEN_STYLE
+import androidx.wear.watchface.samples.NO_COMPLICATIONS
+import androidx.wear.watchface.samples.WATCH_HAND_LENGTH_STYLE_SETTING
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Assert.assertFalse
@@ -486,6 +493,57 @@
         assertThat(contentDescriptionLabels[2].getTextAt(context.resources, 0))
             .isEqualTo("ID Right")
     }
+
+    @Test
+    fun setUserStyle() {
+        val interactiveInstanceFuture =
+            service.getOrCreateWallpaperServiceBackedInteractiveWatchFaceWcsClient(
+                "testId",
+                deviceConfig,
+                systemState,
+                mapOf(
+                    COLOR_STYLE_SETTING to GREEN_STYLE,
+                    WATCH_HAND_LENGTH_STYLE_SETTING to "0.25",
+                    DRAW_HOUR_PIPS_STYLE_SETTING to "false",
+                    COMPLICATIONS_STYLE_SETTING to NO_COMPLICATIONS
+                ),
+                complications
+            )
+
+        Mockito.`when`(surfaceHolder.surfaceFrame)
+            .thenReturn(Rect(0, 0, 400, 400))
+
+        // Create the engine which triggers creation of InteractiveWatchFaceWcsClient.
+        createEngine()
+
+        // Wait for the instance to be created.
+        val interactiveInstance =
+            interactiveInstanceFuture.get(CONNECT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)!!
+
+        // Note this map doesn't include all the categories, which is fine the others will be set
+        // to their defaults.
+        interactiveInstance.setUserStyle(
+            mapOf(
+                COLOR_STYLE_SETTING to BLUE_STYLE,
+                WATCH_HAND_LENGTH_STYLE_SETTING to "0.9",
+            )
+        )
+
+        val bitmap = interactiveInstance.takeWatchFaceScreenshot(
+            RenderParameters(DrawMode.INTERACTIVE, RenderParameters.DRAW_ALL_LAYERS, null),
+            100,
+            1234567,
+            null,
+            complications
+        )
+
+        try {
+            // Note the hour hand pips and both complications should be visible in this image.
+            bitmap.assertAgainstGolden(screenshotRule, "setUserStyle")
+        } finally {
+            interactiveInstance.close()
+        }
+    }
 }
 
 internal class TestExampleCanvasAnalogWatchFaceService(
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
index 889a3a5..847643a 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/ComplicationState.kt
@@ -31,7 +31,7 @@
     /** The type of the complication's bounds. */
     @ComplicationBoundsType public val boundsType: Int,
 
-    /** The [ComplicationType]s supported for this complication. */
+    /** The [ComplicationType]s supported by this complication. */
     public val supportedTypes: List<ComplicationType>,
 
     /** The [DefaultComplicationProviderPolicy] for this complication. */
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
index e432cc0..521ef55 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/HeadlessWatchFaceClient.kt
@@ -20,6 +20,7 @@
 import android.os.IBinder
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.control.IHeadlessWatchFace
@@ -29,7 +30,13 @@
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
 
-/** Controls a stateless remote headless watch face. */
+/**
+ * Controls a stateless remote headless watch face.  This is mostly intended for use by watch face
+ * editor UIs which need to generate screenshots for various styling configurations without
+ * affecting the current watchface.
+ *
+ * Note clients should call [close] when finished.
+ */
 public interface HeadlessWatchFaceClient : AutoCloseable {
     /** The UTC reference preview time for this watch face in milliseconds since the epoch. */
     public val previewReferenceTimeMillis: Long
@@ -63,6 +70,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -85,6 +93,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings, or `null` if [complicationId] is unrecognized.
      */
+    @RequiresApi(27)
     public fun takeComplicationScreenshot(
         complicationId: Int,
         renderParameters: RenderParameters,
@@ -117,6 +126,7 @@
             { ComplicationState(it.complicationState) }
         )
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -141,6 +151,7 @@
         )
     )
 
+    @RequiresApi(27)
     override fun takeComplicationScreenshot(
         complicationId: Int,
         renderParameters: RenderParameters,
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
index 10b2b1b..46bb1a9 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceSysUiClient.kt
@@ -25,6 +25,7 @@
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntDef
 import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
@@ -50,6 +51,8 @@
 /**
  * Controls a stateful remote interactive watch face with an interface tailored for SysUI the
  * WearOS 3.0 launcher app. Typically this will be used for the current active watch face.
+ *
+ * Note clients should call [close] when finished.
  */
 public interface InteractiveWatchFaceSysUiClient : AutoCloseable {
 
@@ -71,7 +74,10 @@
          */
         public const val TAP_TYPE_TAP: Int = IInteractiveWatchFaceSysUI.TAP_TYPE_TAP
 
-        /** Constructs a [InteractiveWatchFaceSysUiClient] from an [IBinder]. */
+        /**
+         * Constructs an [InteractiveWatchFaceSysUiClient] from the [IBinder] returned by
+         * [asBinder].
+         */
         @JvmStatic
         public fun createFromBinder(binder: IBinder): InteractiveWatchFaceSysUiClient =
             InteractiveWatchFaceSysUiClientImpl(binder)
@@ -136,6 +142,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -181,6 +188,7 @@
                 )
             }
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
index 71e7ecf..e8b1169 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceWcsClient.kt
@@ -20,6 +20,7 @@
 import android.os.IBinder
 import android.support.wearable.watchface.SharedMemoryImage
 import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.wear.complications.data.ComplicationData
 import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.control.IInteractiveWatchFaceWCS
@@ -27,16 +28,21 @@
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.style.UserStyle
 import androidx.wear.watchface.style.UserStyleSchema
+import androidx.wear.watchface.style.data.UserStyleWireFormat
 
 /**
  * Controls a stateful remote interactive watch face with an interface tailored for WCS the
  * WearOS 3.0 system server responsible for watch face management. Typically this will be used for
  * the current active watch face.
+ *
+ * Note clients should call [close] when finished.
  */
 public interface InteractiveWatchFaceWcsClient : AutoCloseable {
 
     public companion object {
-        /** Constructs a [InteractiveWatchFaceWcsClient] from an [IBinder]. */
+        /**
+         * Constructs an [InteractiveWatchFaceWcsClient] from the [IBinder] returned by [asBinder].
+         */
         @JvmStatic
         public fun createFromBinder(binder: IBinder): InteractiveWatchFaceWcsClient =
             InteractiveWatchFaceWcsClientImpl(binder)
@@ -62,6 +68,7 @@
      * @return A WebP compressed shared memory backed [Bitmap] containing a screenshot of the watch
      *     face with the given settings.
      */
+    @RequiresApi(27)
     public fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -79,6 +86,13 @@
      */
     public fun setUserStyle(userStyle: UserStyle)
 
+    /**
+     * Sets the watch face's current UserStyle represented as a Map<String, String>.  This can be
+     * helpful to avoid having to construct a [UserStyle] which requires the [UserStyleSchema]
+     * which is an additional IPC. Note this may alter [complicationState].
+     */
+    public fun setUserStyle(userStyle: Map<String, String>)
+
     /** Returns the ID of this watch face instance. */
     public val instanceId: String
 
@@ -110,6 +124,7 @@
         )
     }
 
+    @RequiresApi(27)
     override fun takeWatchFaceScreenshot(
         renderParameters: RenderParameters,
         @IntRange(from = 0, to = 100)
@@ -141,6 +156,10 @@
         iInteractiveWatchFaceWcs.setCurrentUserStyle(userStyle.toWireFormat())
     }
 
+    override fun setUserStyle(userStyle: Map<String, String>) {
+        iInteractiveWatchFaceWcs.setCurrentUserStyle(UserStyleWireFormat(userStyle))
+    }
+
     override val instanceId: String
         get() = iInteractiveWatchFaceWcs.instanceId
 
diff --git a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
index e051212..d3498be 100644
--- a/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
+++ b/wear/wear-watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceControlClient.kt
@@ -35,13 +35,17 @@
 import com.google.common.util.concurrent.ListenableFuture
 
 /**
- * Connects to a watch face's WatchFaceControlService which allows the user to control the
- * watch face.
+ * Connects to a watch face's WatchFaceControlService which allows the user to control the watch
+ * face.
  */
 public interface WatchFaceControlClient : AutoCloseable {
 
     public companion object {
-        /** Constructs a client which connects to a watch face in the given android package. */
+        /**
+         * Constructs a [WatchFaceControlClient] which attempts to connect to a watch face in the
+         * android package [watchFacePackageName]. If this fails the [ListenableFuture]s returned by
+         * WatchFaceControlClient methods will fail with [ServiceNotBoundException].
+         */
         @JvmStatic
         public fun createWatchFaceControlClient(
             /** Calling application's [Context]. */
diff --git a/wear/wear-watchface-complications-rendering/build.gradle b/wear/wear-watchface-complications-rendering/build.gradle
index 60b135a..2735d4b 100644
--- a/wear/wear-watchface-complications-rendering/build.gradle
+++ b/wear/wear-watchface-complications-rendering/build.gradle
@@ -54,7 +54,7 @@
         aidl = true
     }
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
     }
 
     // Use Robolectric 4.+
diff --git a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
index 95e4126..91c08a4 100644
--- a/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
+++ b/wear/wear-watchface-complications-rendering/src/test/java/androidx/wear/watchface/complications/rendering/ComplicationDrawableTest.java
@@ -34,7 +34,6 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.graphics.Bitmap;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.ColorFilter;
@@ -51,8 +50,8 @@
 import androidx.annotation.NonNull;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.wear.complications.ComplicationHelperActivity;
+import androidx.wear.watchface.CanvasType;
 import androidx.wear.watchface.ComplicationsManager;
-import androidx.wear.watchface.RenderParameters;
 import androidx.wear.watchface.Renderer;
 import androidx.wear.watchface.WatchFace;
 import androidx.wear.watchface.WatchFaceService;
@@ -720,18 +719,13 @@
                     WatchFaceType.ANALOG,
                     userStyleRepository,
                     new ComplicationsManager(new ArrayList<>(), userStyleRepository),
-                    new Renderer(surfaceHolder, userStyleRepository, watchState, 16L) {
-                        @NotNull
+                    new Renderer.CanvasRenderer(
+                            surfaceHolder, userStyleRepository, watchState, CanvasType.SOFTWARE,
+                            16L) {
                         @Override
-                        public Bitmap takeScreenshot$wear_watchface_debug(
-                                @NotNull Calendar calendar,
-                                @NonNull RenderParameters renderParameters) {
-                            return null;
-                        }
+                        public void render(@NonNull Canvas canvas, @NonNull Rect bounds,
+                                @NonNull Calendar calendar) {
 
-                        @Override
-                        public void renderInternal$wear_watchface_debug(
-                                @NotNull Calendar calendar) {
                         }
                     }
             );
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
index 1161815..ad56258 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleRepository.kt
@@ -88,8 +88,9 @@
 }
 
 /**
- * In memory storage for user style choices which allows listeners to be registered to observe
- * style changes.
+ * An in memory storage for user style choices represented as [UserStyle], listeners can be
+ * registered to observe style changes. The UserStyleRepository is initialized with a
+ * [UserStyleSchema].
  */
 public class UserStyleRepository(
     /**
@@ -98,37 +99,36 @@
      */
     public val schema: UserStyleSchema
 ) {
-    /** A listener for observing user style changes. */
+    /** A listener for observing [UserStyle] changes. */
     public interface UserStyleListener {
-        /** Called whenever the user style changes. */
+        /** Called whenever the [UserStyle] changes. */
         @UiThread
         public fun onUserStyleChanged(userStyle: UserStyle)
     }
 
     private val styleListeners = HashSet<UserStyleListener>()
 
-    // The current style state which is initialized from the userStyleSettings.
-    @SuppressWarnings("SyntheticAccessor")
-    private val _style = UserStyle(
+    /**
+     * The current [UserStyle]. Assigning to this property triggers immediate [UserStyleListener]
+     * callbacks if if any options have changed.
+     */
+    public var userStyle: UserStyle = UserStyle(
         HashMap<UserStyleSetting, UserStyleSetting.Option>().apply {
             for (setting in schema.userStyleSettings) {
                 this[setting] = setting.getDefaultOption()
             }
         }
     )
-
-    /** The current user controlled style for rendering etc... */
-    public var userStyle: UserStyle
         @UiThread
-        get() = _style
+        get
         @UiThread
         set(style) {
             var changed = false
             val hashmap =
-                _style.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
+                field.selectedOptions as HashMap<UserStyleSetting, UserStyleSetting.Option>
             for ((setting, option) in style.selectedOptions) {
                 // Ignore an unrecognized setting.
-                val styleSetting = _style.selectedOptions[setting] ?: continue
+                val styleSetting = field.selectedOptions[setting] ?: continue
                 if (styleSetting.id != option.id) {
                     changed = true
                 }
@@ -140,7 +140,7 @@
             }
 
             for (styleListener in styleListeners) {
-                styleListener.onUserStyleChanged(_style)
+                styleListener.onUserStyleChanged(field)
             }
         }
 
@@ -151,7 +151,7 @@
     @SuppressLint("ExecutorRegistration")
     public fun addUserStyleListener(userStyleListener: UserStyleListener) {
         styleListeners.add(userStyleListener)
-        userStyleListener.onUserStyleChanged(_style)
+        userStyleListener.onUserStyleChanged(userStyle)
     }
 
     /** Removes a [UserStyleListener] previously added by [addUserStyleListener]. */
diff --git a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
index 978e023..9e5c35a 100644
--- a/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
+++ b/wear/wear-watchface-style/src/main/java/androidx/wear/watchface/style/UserStyleSetting.kt
@@ -32,10 +32,12 @@
 import java.security.InvalidParameterException
 
 /**
- * Watch faces often have user configurable styles. The definition of what is a style is left up
- * to the watch face but it typically incorporates a variety of settings such as: color,
- * visual theme for watch hands, font, tick shape, complications, audio elements, etc...
- * A UserStyleSetting represents one of these dimensions.
+ * Watch faces often have user configurable styles, the definition of what is a style is left up to
+ * the watch face but it typically incorporates a variety of settings such as: color, visual theme
+ * for watch hands, font, tick shape, complications, audio elements, etc...
+ *
+ * A UserStyleSetting represents one of these dimensions. See also [UserStyleSchema] which defines
+ * the list of UserStyleSettings provided by the watch face.
  */
 public sealed class UserStyleSetting(
     /** Identifier for the element, must be unique. */
diff --git a/wear/wear-watchface/api/current.txt b/wear/wear-watchface/api/current.txt
index dd7e387..165eb26 100644
--- a/wear/wear-watchface/api/current.txt
+++ b/wear/wear-watchface/api/current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
     method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -147,8 +130,8 @@
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -174,8 +157,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +179,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +233,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +243,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +258,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/api/public_plus_experimental_current.txt b/wear/wear-watchface/api/public_plus_experimental_current.txt
index dd7e387..165eb26 100644
--- a/wear/wear-watchface/api/public_plus_experimental_current.txt
+++ b/wear/wear-watchface/api/public_plus_experimental_current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
     method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -147,8 +130,8 @@
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -174,8 +157,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -197,6 +179,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -234,7 +233,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method public final android.service.wallpaper.WallpaperService.Engine onCreateEngine();
@@ -244,11 +243,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -260,7 +258,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/api/restricted_current.txt b/wear/wear-watchface/api/restricted_current.txt
index c6773f25..72cd5dcd 100644
--- a/wear/wear-watchface/api/restricted_current.txt
+++ b/wear/wear-watchface/api/restricted_current.txt
@@ -15,7 +15,7 @@
 
   public class CanvasComplicationDrawable implements androidx.wear.watchface.CanvasComplication {
     ctor public CanvasComplicationDrawable(androidx.wear.watchface.complications.rendering.ComplicationDrawable drawable, androidx.wear.watchface.WatchState watchState);
-    method public void drawHighlight(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+    method public void drawOutline(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
     method public final androidx.wear.watchface.complications.rendering.ComplicationDrawable getDrawable();
     method public androidx.wear.complications.data.IdAndComplicationData? getIdAndData();
     method @UiThread public boolean isHighlighted();
@@ -30,11 +30,6 @@
     property @UiThread public boolean isHighlighted;
   }
 
-  public abstract class CanvasRenderer extends androidx.wear.watchface.Renderer {
-    ctor public CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
-  }
-
   public final class Complication {
     method public static androidx.wear.watchface.Complication.Builder createBackgroundComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy);
     method public static androidx.wear.watchface.Complication.Builder createRoundRectComplicationBuilder(int id, androidx.wear.watchface.CanvasComplication renderer, java.util.List<? extends androidx.wear.complications.data.ComplicationType> supportedTypes, androidx.wear.complications.DefaultComplicationProviderPolicy defaultProviderPolicy, android.graphics.RectF unitSquareBounds);
@@ -111,18 +106,6 @@
     enum_constant public static final androidx.wear.watchface.DrawMode MUTE;
   }
 
-  public abstract class GlesRenderer extends androidx.wear.watchface.Renderer {
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
-    ctor public GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
-    method @UiThread public void onGlContextCreated();
-    method @UiThread public void onGlSurfaceCreated(int width, int height);
-    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
-  }
-
-  public final class GlesRendererKt {
-  }
-
   public final class GlesTextureComplication {
     ctor public GlesTextureComplication(androidx.wear.watchface.CanvasComplication canvasComplication, @Px int textureWidth, @Px int textureHeight, int textureType);
     method public void bind();
@@ -151,7 +134,6 @@
     method public boolean getHasBurnInProtection();
     method public boolean getHasLowBitAmbient();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging();
     method public androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible();
@@ -160,7 +142,6 @@
     method public void setHasBurnInProtection(boolean p);
     method public void setHasLowBitAmbient(boolean p);
     method public void setInterruptionFilter(androidx.wear.watchface.MutableObservableWatchData<java.lang.Integer> p);
-    method public void setScreenShape(int p);
     property public final long analogPreviewReferenceTimeMillis;
     property public final long digitalPreviewReferenceTimeMillis;
     property public final boolean hasBurnInProtection;
@@ -169,14 +150,13 @@
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging;
     property public final androidx.wear.watchface.MutableObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
   public class ObservableWatchData<T> {
     method @UiThread public final void addObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread public T getValue();
-    method public final T getValueOr(T p);
-    method public final boolean hasValue();
+    method @UiThread public final T getValueOr(T p);
+    method @UiThread public final boolean hasValue();
     method @UiThread public final void removeObserver(androidx.wear.watchface.Observer<T> observer);
     method @UiThread protected void setValue(T v);
     property @UiThread public T value;
@@ -204,8 +184,7 @@
   public static final class RenderParameters.Companion {
   }
 
-  public abstract class Renderer {
-    ctor public Renderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+  public abstract sealed class Renderer {
     method public final float getCenterX();
     method public final float getCenterY();
     method public final long getInteractiveDrawModeUpdateDelayMillis();
@@ -227,6 +206,23 @@
     property public final android.view.SurfaceHolder surfaceHolder;
   }
 
+  public abstract static class Renderer.CanvasRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.CanvasRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, int canvasType, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public abstract void render(android.graphics.Canvas canvas, android.graphics.Rect bounds, android.icu.util.Calendar calendar);
+  }
+
+  public abstract static class Renderer.GlesRenderer extends androidx.wear.watchface.Renderer {
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList, int[] eglSurfaceAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis, int[] eglConfigAttribList);
+    ctor public Renderer.GlesRenderer(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.WatchState watchState, @IntRange(from=0, to=10000) long interactiveDrawModeUpdateDelayMillis);
+    method @UiThread public void onGlContextCreated();
+    method @UiThread public void onGlSurfaceCreated(int width, int height);
+    method @UiThread public abstract void render(android.icu.util.Calendar calendar);
+  }
+
+  public final class RendererKt {
+  }
+
   public final class WatchFace {
     ctor public WatchFace(int watchFaceType, androidx.wear.watchface.style.UserStyleRepository userStyleRepository, androidx.wear.watchface.ComplicationsManager complicationsManager, androidx.wear.watchface.Renderer renderer);
     method public androidx.wear.watchface.WatchFace.LegacyWatchFaceOverlayStyle getLegacyWatchFaceStyle();
@@ -281,7 +277,7 @@
   public final class WatchFaceKt {
   }
 
-  @RequiresApi(26) public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
+  public abstract class WatchFaceService extends android.service.wallpaper.WallpaperService {
     ctor public WatchFaceService();
     method protected abstract androidx.wear.watchface.WatchFace createWatchFace(android.view.SurfaceHolder surfaceHolder, androidx.wear.watchface.WatchState watchState);
     method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP) public android.view.SurfaceHolder? getWallpaperSurfaceHolderOverride();
@@ -292,11 +288,10 @@
   }
 
   public final class WatchState {
-    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, int screenShape, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
+    ctor public WatchState(androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isBatteryLowAndNotCharging, androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible, boolean hasLowBitAmbient, boolean hasBurnInProtection, long analogPreviewReferenceTimeMillis, long digitalPreviewReferenceTimeMillis);
     method public long getAnalogPreviewReferenceTimeMillis();
     method public long getDigitalPreviewReferenceTimeMillis();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Integer> getInterruptionFilter();
-    method public int getScreenShape();
     method public boolean hasBurnInProtection();
     method public boolean hasLowBitAmbient();
     method public androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient();
@@ -308,7 +303,6 @@
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Integer> interruptionFilter;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isAmbient;
     property public final androidx.wear.watchface.ObservableWatchData<java.lang.Boolean> isVisible;
-    property public final int screenShape;
   }
 
 }
diff --git a/wear/wear-watchface/build.gradle b/wear/wear-watchface/build.gradle
index beca881..f4a1682 100644
--- a/wear/wear-watchface/build.gradle
+++ b/wear/wear-watchface/build.gradle
@@ -61,7 +61,7 @@
 
 android {
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
         testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
     }
 
diff --git a/wear/wear-watchface/samples/build.gradle b/wear/wear-watchface/samples/build.gradle
index ec111e8..ec2f092 100644
--- a/wear/wear-watchface/samples/build.gradle
+++ b/wear/wear-watchface/samples/build.gradle
@@ -44,10 +44,10 @@
 
 android {
     defaultConfig {
-        minSdkVersion 26
+        minSdkVersion 25
     }
     compileOptions {
-        sourceCompatibility = JavaVersion.VERSION_1_7
-        targetCompatibility = JavaVersion.VERSION_1_7
+        sourceCompatibility 1.8
+        targetCompatibility 1.8
     }
 }
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
index 19dee75..2773588 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasAnalogWatchFaceService.kt
@@ -29,12 +29,12 @@
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -68,6 +68,16 @@
 
 private const val NUMBER_RADIUS_FRACTION = 0.45f
 
+val COLOR_STYLE_SETTING = "color_style_setting"
+val RED_STYLE = "red_style"
+val GREEN_STYLE = "green_style"
+val BLUE_STYLE = "blue_style"
+
+val DRAW_HOUR_PIPS_STYLE_SETTING = "draw_hour_pips_style_setting"
+
+val WATCH_HAND_LENGTH_STYLE_SETTING = "watch_hand_length_style_setting"
+
+val COMPLICATIONS_STYLE_SETTING = "complications_style_setting"
 val NO_COMPLICATIONS = "NO_COMPLICATIONS"
 val LEFT_COMPLICATION = "LEFT_COMPLICATION"
 val RIGHT_COMPLICATION = "RIGHT_COMPLICATION"
@@ -96,43 +106,43 @@
     surfaceHolder: SurfaceHolder,
     watchState: WatchState
 ): WatchFace {
-    val watchFaceStyle = WatchFaceColorStyle.create(context, "red_style")
+    val watchFaceStyle = WatchFaceColorStyle.create(context, RED_STYLE)
     val colorStyleSetting = ListUserStyleSetting(
-        "color_style_setting",
-        "Colors",
-        "Watchface colorization",
+        COLOR_STYLE_SETTING,
+        context.getString(R.string.colors_style_setting),
+        context.getString(R.string.colors_style_setting_description),
         icon = null,
         options = listOf(
             ListUserStyleSetting.ListOption(
-                "red_style",
-                "Red",
+                RED_STYLE,
+                context.getString(R.string.colors_style_red),
                 Icon.createWithResource(context, R.drawable.red_style)
             ),
             ListUserStyleSetting.ListOption(
-                "green_style",
-                "Green",
+                GREEN_STYLE,
+                context.getString(R.string.colors_style_green),
                 Icon.createWithResource(context, R.drawable.green_style)
             ),
             ListUserStyleSetting.ListOption(
-                "blue_style",
-                "Blue",
+                BLUE_STYLE,
+                context.getString(R.string.colors_style_blue),
                 Icon.createWithResource(context, R.drawable.blue_style)
             )
         ),
         listOf(Layer.BASE_LAYER, Layer.COMPLICATIONS, Layer.TOP_LAYER)
     )
     val drawHourPipsStyleSetting = BooleanUserStyleSetting(
-        "draw_hour_pips_style_setting",
-        "Hour Pips",
-        "Whether to draw or not",
+        DRAW_HOUR_PIPS_STYLE_SETTING,
+        context.getString(R.string.watchface_pips_setting),
+        context.getString(R.string.watchface_pips_setting_description),
         null,
         true,
         listOf(Layer.BASE_LAYER)
     )
     val watchHandLengthStyleSetting = DoubleRangeUserStyleSetting(
-        "watch_hand_length_style_setting",
-        "Hand length",
-        "Scale of watch hands",
+        WATCH_HAND_LENGTH_STYLE_SETTING,
+        context.getString(R.string.watchface_hand_length_setting),
+        context.getString(R.string.watchface_hand_length_setting_description),
         null,
         0.25,
         1.0,
@@ -142,14 +152,14 @@
     // These are style overrides applied on top of the complications passed into
     // complicationsManager below.
     val complicationsStyleSetting = ComplicationsUserStyleSetting(
-        "complications_style_setting",
-        "Complications",
-        "Number and position",
+        COMPLICATIONS_STYLE_SETTING,
+        context.getString(R.string.watchface_complications_setting),
+        context.getString(R.string.watchface_complications_setting_description),
         icon = null,
         complicationConfig = listOf(
             ComplicationsUserStyleSetting.ComplicationsOption(
                 LEFT_AND_RIGHT_COMPLICATIONS,
-                "Both",
+                context.getString(R.string.watchface_complications_setting_both),
                 null,
                 // NB this list is empty because each [ComplicationOverlay] is applied on top of
                 // the initial config.
@@ -157,7 +167,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 NO_COMPLICATIONS,
-                "None",
+                context.getString(R.string.watchface_complications_setting_none),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -172,7 +182,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 LEFT_COMPLICATION,
-                "Left",
+                context.getString(R.string.watchface_complications_setting_left),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -183,7 +193,7 @@
             ),
             ComplicationsUserStyleSetting.ComplicationsOption(
                 RIGHT_COMPLICATION,
-                "Right",
+                context.getString(R.string.watchface_complications_setting_right),
                 null,
                 listOf(
                     ComplicationOverlay(
@@ -266,7 +276,7 @@
     private val drawPipsStyleSetting: BooleanUserStyleSetting,
     private val watchHandLengthStyleSettingDouble: DoubleRangeUserStyleSetting,
     private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
index 94cc130..66a46ec 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleCanvasDigitalWatchFaceService.kt
@@ -38,15 +38,16 @@
 import android.view.SurfaceHolder
 import android.view.animation.AnimationUtils
 import android.view.animation.PathInterpolator
+import androidx.annotation.ColorInt
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -193,6 +194,19 @@
 internal val CENTERING_ADJUSTMENT_INTERPOLATOR =
     PathInterpolator(0.4f, 0f, 0.2f, 1f)
 
+@ColorInt
+internal fun colorRgb(red: Float, green: Float, blue: Float) =
+    0xff000000.toInt() or
+        ((red * 255.0f + 0.5f).toInt() shl 16) or
+        ((green * 255.0f + 0.5f).toInt() shl 8) or
+        (blue * 255.0f + 0.5f).toInt()
+
+internal fun redFraction(@ColorInt color: Int) = Color.red(color).toFloat() / 255.0f
+
+internal fun greenFraction(@ColorInt color: Int) = Color.green(color).toFloat() / 255.0f
+
+internal fun blueFraction(@ColorInt color: Int) = Color.blue(color).toFloat() / 255.0f
+
 /**
  * Returns an RGB color that has the same effect as drawing `color` with `alphaFraction` over a
  * `backgroundColor` background.
@@ -201,11 +215,15 @@
  * @param alphaFraction the fraction of the alpha value, range from 0 to 1
  * @param backgroundColor the background color
  */
-internal fun getRGBColor(color: Color, alphaFraction: Float, backgroundColor: Color): Int {
-    return Color.rgb(
-        lerp(backgroundColor.red(), color.red(), alphaFraction),
-        lerp(backgroundColor.green(), color.green(), alphaFraction),
-        lerp(backgroundColor.blue(), color.blue(), alphaFraction)
+internal fun getRGBColor(
+    @ColorInt color: Int,
+    alphaFraction: Float,
+    @ColorInt backgroundColor: Int
+): Int {
+    return colorRgb(
+        lerp(redFraction(backgroundColor), redFraction(color), alphaFraction),
+        lerp(greenFraction(backgroundColor), greenFraction(color), alphaFraction),
+        lerp(blueFraction(backgroundColor), blueFraction(color), alphaFraction)
     )
 }
 
@@ -417,12 +435,11 @@
 
 /** Applies a multiplier to a color, e.g. to darken if it's < 1.0 */
 internal fun multiplyColor(colorInt: Int, multiplier: Float): Int {
-    val color = Color.valueOf(colorInt)
-    // NB color.red() etc return a value in the range [0..1]
-    return Color.rgb(
-        color.red() * multiplier,
-        color.green() * multiplier,
-        color.blue() * multiplier
+    val adjustedMultiplier = multiplier / 255.0f
+    return colorRgb(
+        Color.red(colorInt).toFloat() * adjustedMultiplier,
+        Color.green(colorInt).toFloat() * adjustedMultiplier,
+        Color.blue(colorInt).toFloat() * adjustedMultiplier,
     )
 }
 
@@ -450,26 +467,26 @@
         surfaceHolder: SurfaceHolder,
         watchState: WatchState
     ): WatchFace {
-        val watchFaceStyle = WatchFaceColorStyle.create(this, "red_style")
+        val watchFaceStyle = WatchFaceColorStyle.create(this, RED_STYLE)
         val colorStyleSetting = UserStyleSetting.ListUserStyleSetting(
-            "color_style_setting",
-            "Colors",
-            "Watchface colorization",
+            COLOR_STYLE_SETTING,
+            getString(R.string.colors_style_setting),
+            getString(R.string.colors_style_setting_description),
             icon = null,
             options = listOf(
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "red_style",
-                    "Red",
+                    RED_STYLE,
+                    getString(R.string.colors_style_red),
                     Icon.createWithResource(this, R.drawable.red_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "green_style",
-                    "Green",
+                    GREEN_STYLE,
+                    getString(R.string.colors_style_green),
                     Icon.createWithResource(this, R.drawable.green_style)
                 ),
                 UserStyleSetting.ListUserStyleSetting.ListOption(
-                    "blue_style",
-                    "Blue",
+                    BLUE_STYLE,
+                    getString(R.string.colors_style_blue),
                     Icon.createWithResource(this, R.drawable.blue_style)
                 )
             ),
@@ -620,7 +637,7 @@
     private val watchState: WatchState,
     private val colorStyleSetting: UserStyleSetting.ListUserStyleSetting,
     private val complicationsManager: ComplicationsManager
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
@@ -1050,9 +1067,9 @@
         }
         canvas.drawColor(
             getRGBColor(
-                Color.valueOf(backgroundColor),
+                backgroundColor,
                 drawProperties.backgroundAlpha,
-                Color.valueOf(Color.BLACK)
+                Color.BLACK
             )
         )
     }
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
index 458071d..b942f55 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/ExampleOpenGLWatchFaceService.kt
@@ -33,9 +33,9 @@
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.DrawMode
-import androidx.wear.watchface.GlesRenderer
 import androidx.wear.watchface.GlesTextureComplication
 import androidx.wear.watchface.LayerMode
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -155,7 +155,7 @@
     watchState: WatchState,
     private val colorStyleSetting: ListUserStyleSetting,
     private val complication: Complication
-) : GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
+) : Renderer.GlesRenderer(surfaceHolder, userStyleRepository, watchState, FRAME_PERIOD_MS) {
 
     /** Projection transformation matrix. Converts from 3D to 2D.  */
     private val projectionMatrix = FloatArray(16)
diff --git a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
index 8da96bb..e294c01 100644
--- a/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
+++ b/wear/wear-watchface/samples/src/main/java/androidx/wear/watchface/samples/KDocExampleWatchFace.kt
@@ -28,11 +28,11 @@
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
-import androidx.wear.watchface.CanvasRenderer
 import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.CanvasComplicationDrawable
 import androidx.wear.watchface.ComplicationsManager
+import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFace
 import androidx.wear.watchface.WatchFaceService
 import androidx.wear.watchface.WatchFaceType
@@ -141,7 +141,7 @@
                 userStyleRepository
             )
 
-            val renderer = object : CanvasRenderer(
+            val renderer = object : Renderer.CanvasRenderer(
                 surfaceHolder,
                 userStyleRepository,
                 watchState,
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
deleted file mode 100644
index ed1e13d..0000000
--- a/wear/wear-watchface/samples/src/main/res/values/strings.xm.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  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.
-  -->
-
-<resources>
-    <string name="app_name" translatable="false">Example Watchfaces</string>
-    <string name="canvas_analog_watch_face_name"
-        translatable="false">Example Analog Watchface</string>
-    <string name="canvas_digital_watch_face_name"
-        translatable="false">Example Digital Watchface</string>
-    <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
-
-    <!-- Name of watchface style [CHAR LIMIT=80] -->
-    <string name="red_style_name">Red Style</string>
-
-    <!-- Name of watchface style [CHAR LIMIT=80] -->
-    <string name="green_style_name">Green Style</string>
-</resources>
diff --git a/wear/wear-watchface/samples/src/main/res/values/strings.xml b/wear/wear-watchface/samples/src/main/res/values/strings.xml
new file mode 100644
index 0000000..90f3dd8
--- /dev/null
+++ b/wear/wear-watchface/samples/src/main/res/values/strings.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<resources>
+    <string name="app_name" translatable="false">Example Watchfaces</string>
+    <string name="canvas_analog_watch_face_name"
+        translatable="false">Example Analog Watchface</string>
+    <string name="canvas_digital_watch_face_name"
+        translatable="false">Example Digital Watchface</string>
+    <string name="gl_watch_face_name" translatable="false">Example OpenGL Watchface</string>
+
+    <!-- Name of watchface style [CHAR LIMIT=20] -->
+    <string name="red_style_name">Red Style</string>
+
+    <!-- Name of watchface style [CHAR LIMIT=20] -->
+    <string name="green_style_name">Green Style</string>
+
+    <!-- Name of watchface style category for selecting the color theme [CHAR LIMIT=20] -->
+    <string name="colors_style_setting">Colors</string>
+
+    <!-- Subtitle for the menu option to select the watch face color theme [CHAR LIMIT=20] -->
+    <string name="colors_style_setting_description">Watchface colorization</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_red">Red</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_green">Green</string>
+
+    <!-- An option within the watch face color style theme settings [CHAR LIMIT=20] -->
+    <string name="colors_style_blue">Blue</string>
+
+    <!-- An option within the analog watch face to draw pips to mark each hour [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting">Hour Pips</string>
+
+    <!-- Subtitle for the menu option to configure if we should draw pips to mark each hour
+     on the watch face [CHAR LIMIT=20] -->
+    <string name="watchface_pips_setting_description">Whether to draw or not</string>
+
+    <!-- A menu option to select a widget that lets us configure the length of the watch hands
+    [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting">Hand length</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the length of the
+    watch hands [CHAR LIMIT=20] -->
+    <string name="watchface_hand_length_setting_description">Scale of watch hands</string>
+
+    <!-- A menu option to select a widget that lets us configure the Complications (a watch
+    making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting">Complications</string>
+
+    <!-- Sub title for the menu option to select a widget that lets us configure the Complications
+    (a watch making term) [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_description">Number and position</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select both
+    the left and the right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_both">Both</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select no
+    complications for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_none">None</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the left
+    complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_left">Left</string>
+
+    <!-- Menu option within the complications configuration widget that lets us select only the
+    right complication for rendering. [CHAR LIMIT=20] -->
+    <string name="watchface_complications_setting_right">Right</string>
+</resources>
diff --git a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
index 5042dba..2c9375c 100644
--- a/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
+++ b/wear/wear-watchface/src/androidTest/java/androidx/wear/watchface/test/WatchFaceServiceImageTest.kt
@@ -46,8 +46,10 @@
 import androidx.wear.watchface.data.DeviceConfig
 import androidx.wear.watchface.data.IdAndComplicationDataWireFormat
 import androidx.wear.watchface.data.SystemState
+import androidx.wear.watchface.samples.COLOR_STYLE_SETTING
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_LEFT_COMPLICATION_ID
 import androidx.wear.watchface.samples.EXAMPLE_CANVAS_WATCHFACE_RIGHT_COMPLICATION_ID
+import androidx.wear.watchface.samples.GREEN_STYLE
 import androidx.wear.watchface.style.Layer
 import androidx.wear.watchface.style.data.UserStyleWireFormat
 import org.junit.After
@@ -304,7 +306,7 @@
         initLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)
         handler.post {
             interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
-                UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
             )
             engineWrapper.draw()
         }
@@ -442,7 +444,7 @@
         handler.post {
             // Change the style
             interactiveWatchFaceInstanceWCS.setCurrentUserStyle(
-                UserStyleWireFormat(mapOf("color_style_setting" to "green_style"))
+                UserStyleWireFormat(mapOf(COLOR_STYLE_SETTING to GREEN_STYLE))
             )
 
             // Simulate device shutting down.
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
deleted file mode 100644
index 3b81fea..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/CanvasRenderer.kt
+++ /dev/null
@@ -1,132 +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.wear.watchface
-
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Rect
-import android.icu.util.Calendar
-import android.view.SurfaceHolder
-import androidx.annotation.IntDef
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-/** @hide */
-@IntDef(
-    value = [
-        CanvasType.SOFTWARE,
-        CanvasType.HARDWARE
-    ]
-)
-public annotation class CanvasType {
-    public companion object {
-        /** A software canvas will be requested. */
-        public const val SOFTWARE: Int = 0
-
-        /**
-         * A hardware canvas will be requested. This is usually faster than software rendering,
-         * however it can sometimes increase battery usage by rendering at a higher frame rate.
-         */
-        public const val HARDWARE: Int = 1
-    }
-}
-
-/**
- * Watch faces that require [Canvas] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class CanvasRenderer(
-    /** The [SurfaceHolder] that [render] will draw into. */
-    surfaceHolder: SurfaceHolder,
-
-    /** The associated [UserStyleRepository]. */
-    userStyleRepository: UserStyleRepository,
-
-    /** The associated [WatchState]. */
-    watchState: WatchState,
-
-    /** The type of canvas to use. */
-    @CanvasType private val canvasType: Int,
-
-    /**
-     * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
-     * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
-     * recommended to use lower frame rates if possible for better battery life. Variable frame
-     * rates can also help preserve battery life, e.g. if a watch face has a short animation once
-     * per second it can adjust the frame rate inorder to sleep when not animating.
-     */
-    @IntRange(from = 0, to = 10000)
-    interactiveDrawModeUpdateDelayMillis: Long
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
-
-    internal override fun renderInternal(
-        calendar: Calendar
-    ) {
-        val canvas = (
-            if (canvasType == CanvasType.HARDWARE) {
-                surfaceHolder.lockHardwareCanvas()
-            } else {
-                surfaceHolder.lockCanvas()
-            }
-            ) ?: return
-        try {
-            if (watchState.isVisible.value) {
-                render(canvas, surfaceHolder.surfaceFrame, calendar)
-            } else {
-                canvas.drawColor(Color.BLACK)
-            }
-        } finally {
-            surfaceHolder.unlockCanvasAndPost(canvas)
-        }
-    }
-
-    /** {@inheritDoc} */
-    internal override fun takeScreenshot(
-        calendar: Calendar,
-        renderParameters: RenderParameters
-    ): Bitmap {
-        val bitmap = Bitmap.createBitmap(
-            screenBounds.width(),
-            screenBounds.height(),
-            Bitmap.Config.ARGB_8888
-        )
-        val prevRenderParameters = this.renderParameters
-        this.renderParameters = renderParameters
-        render(Canvas(bitmap), screenBounds, calendar)
-        this.renderParameters = prevRenderParameters
-        return bitmap
-    }
-
-    /**
-     * Sub-classes should override this to implement their rendering logic which should respect
-     * the current [DrawMode]. For correct functioning watch faces must use the supplied
-     * [Calendar] and avoid using any other ways of getting the time.
-     *
-     * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
-     *     the [SurfaceHolder] backing the display
-     * @param bounds A [Rect] describing the bonds of the canvas to draw into
-     * @param calendar The current [Calendar]
-     */
-    @UiThread
-    public abstract fun render(
-        canvas: Canvas,
-        bounds: Rect,
-        calendar: Calendar
-    )
-}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
index 374b3fb..b1b4326 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Complication.kt
@@ -24,6 +24,7 @@
 import android.icu.util.Calendar
 import android.support.wearable.complications.ComplicationData
 import androidx.annotation.UiThread
+import androidx.wear.complications.ComplicationHelperActivity
 import androidx.wear.complications.DefaultComplicationProviderPolicy
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.complications.data.IdAndComplicationData
@@ -31,25 +32,28 @@
 import androidx.wear.watchface.data.ComplicationBoundsType
 import androidx.wear.watchface.style.Layer
 
-/** Common interface for rendering complications onto a [Canvas]. */
+/** Interface for rendering complications onto a [Canvas]. */
 public interface CanvasComplication {
     /**
-     * Called when the CanvasComplication attaches to a [Complication].
+     * Called when the CanvasComplication attaches to a [Complication]. This will get called during
+     * [Complication] initialization and if [Complication.renderer] is assigned with this
+     * CanvasComplication.
      */
     @UiThread
     public fun onAttach(complication: Complication)
 
     /**
-     * Called when the CanvasComplication detaches from a [Complication].
+     * Called when the CanvasComplication detaches from a [Complication]. This will get called if
+     * [Complication.renderer] is assigned to a different CanvasComplication.
      */
     @UiThread
     public fun onDetach()
 
     /**
-     * Draws the complication into the canvas with the specified bounds. This will usually be
-     * called by user watch face drawing code, but the system may also call it for complication
-     * selection UI rendering. The width and height will be the same as that computed by
-     * computeBounds but the translation and canvas size may differ.
+     * Draws the complication defined by [idAndData] into the canvas with the specified bounds. This
+     * will usually be called by user watch face drawing code, but the system may also call it
+     * for complication selection UI rendering. The width and height will be the same as that
+     * computed by computeBounds but the translation and canvas size may differ.
      *
      * @param canvas The [Canvas] to render into
      * @param bounds A [Rect] describing the bounds of the complication
@@ -65,8 +69,8 @@
     )
 
     /**
-     * Whether the complication should be drawn highlighted. This is to provide visual
-     * feedback when the user taps on a complication.
+     * Whether the complication should be drawn highlighted. This is to provide visual feedback when
+     * the user taps on a complication.
      */
     @Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
     @get:JvmName("isHighlighted")
@@ -78,13 +82,17 @@
 }
 
 /**
- * A complication rendered with [ComplicationDrawable] which renders complications in a
- * material design style. This renderer can't be shared by multiple complications.
+ * A complication rendered with [ComplicationDrawable] which renders complications in a material
+ * design style. This renderer can't be shared by multiple complications.
  */
 public open class CanvasComplicationDrawable(
     /** The [ComplicationDrawable] to render with. */
     drawable: ComplicationDrawable,
 
+    /**
+     * The watch's [WatchState] which contains details pertaining to (low-bit) ambient mode and
+     * burn in protection needed to render correctly.
+     */
     private val watchState: WatchState
 ) : CanvasComplication {
 
@@ -153,15 +161,15 @@
                 if (renderParameters.highlightedComplicationId == null ||
                     renderParameters.highlightedComplicationId == idAndData?.complicationId
                 ) {
-                    drawHighlight(canvas, bounds, calendar)
+                    drawOutline(canvas, bounds, calendar)
                 }
             }
             LayerMode.HIDE -> return
         }
     }
 
-    /** Used (indirectly) by the editor, draws a highlight around the complication. */
-    public open fun drawHighlight(
+    /** Used (indirectly) by the editor, draws a dashed line around the complication. */
+    public open fun drawOutline(
         canvas: Canvas,
         bounds: Rect,
         calendar: Calendar
@@ -170,8 +178,8 @@
     }
 
     /**
-     * Whether or not the complication should be drawn highlighted. Used to provide visual
-     * feedback when the complication is tapped.
+     * Whether or not the complication should be drawn highlighted. Used to provide visual feedback
+     * when the complication is tapped.
      */
     override var isHighlighted: Boolean
         @Suppress("INAPPLICABLE_JVM_NAME") // https://stackoverflow.com/questions/47504279
@@ -185,9 +193,7 @@
             drawable.isHighlighted = value
         }
 
-    /**
-     * The [IdAndComplicationData] to use when rendering the complication.
-     */
+    /** The [IdAndComplicationData] to use when rendering the complication. */
     override var idAndData: IdAndComplicationData? = null
         @UiThread
         set(value) {
@@ -220,27 +226,34 @@
          */
         @JvmStatic
         public fun createRoundRectComplicationBuilder(
-            /** The watch face's ID for this complication. */
+            /**
+             * The watch face's ID for this complication. Can be any integer but should be unique
+             * within the watch face.
+             */
             id: Int,
 
             /**
-             * The renderer for this Complication. Renderers may not be sharable between complications.
+             * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+             * between complications.
              */
             renderer: CanvasComplication,
 
             /**
              * The types of complication supported by this Complication. Passed into
              * [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
-             * configuration.
+             * configuration. This list should be non-empty.
              */
             supportedTypes: List<ComplicationType>,
 
-            /** The [DefaultComplicationProviderPolicy] to use. */
+            /**
+             * The [DefaultComplicationProviderPolicy] used to select the initial complication
+             * provider.
+             */
             defaultProviderPolicy: DefaultComplicationProviderPolicy,
 
             /**
-             * The fractional bounds for the complication which are clamped to the unit square
-             * [0..1], and subsequently converted to screen space coordinates. NB 0 and 1 are
+             * The initial fractional bounds for the complication which are clamped to the unit
+             * square [0..1], and subsequently converted to screen space coordinates. NB 0 and 1 are
              * included in the unit square.
              */
             unitSquareBounds: RectF
@@ -267,22 +280,29 @@
          */
         @JvmStatic
         public fun createBackgroundComplicationBuilder(
-            /** The watch face's ID for this complication. */
+            /**
+             * The watch face's ID for this complication. Can be any integer but should be unique
+             * within the watch face.
+             */
             id: Int,
 
             /**
-             * The renderer for this Complication. Renderers may not be sharable between complications.
+             * The [CanvasComplication] to use for rendering. Note renderers should not be shared
+             * between complications.
              */
             renderer: CanvasComplication,
 
             /**
              * The types of complication supported by this Complication. Passed into
              * [ComplicationHelperActivity.createProviderChooserHelperIntent] during complication
-             * configuration.
+             * configuration. This list should be non-empty.
              */
             supportedTypes: List<ComplicationType>,
 
-            /** The [DefaultComplicationProviderPolicy] to use. */
+            /**
+             * The [DefaultComplicationProviderPolicy] used to select the initial complication
+             * provider.
+             */
             defaultProviderPolicy: DefaultComplicationProviderPolicy
         ): Builder = Builder(
             id,
@@ -306,7 +326,9 @@
         private var defaultProviderType = ComplicationType.NOT_CONFIGURED
 
         /**
-         * Sets the default complication provider data type.
+         * Sets the initial [ComplicationType] to use with the initial complication provider.
+         * Note care should be taken to ensure [defaultProviderType] is compatible with the
+         * [DefaultComplicationProviderPolicy].
          */
         public fun setDefaultProviderType(
             defaultProviderType: ComplicationType
@@ -340,22 +362,25 @@
     private lateinit var complicationsManager: ComplicationsManager
     private lateinit var invalidateListener: InvalidateListener
 
-    private var _unitSquareBounds = unitSquareBounds
     internal var unitSquareBoundsDirty = true
 
     /**
      * The screen space unit-square bounds of the complication. This is converted to pixels during
      * rendering.
+     *
+     * Note it's not allowed to change the bounds of a background complication because
+     * they are assumed to always cover the entire screen.
      */
-    public var unitSquareBounds: RectF
+    public var unitSquareBounds: RectF = unitSquareBounds
         @UiThread
-        get() = _unitSquareBounds
+        get
         @UiThread
         set(value) {
-            if (_unitSquareBounds == value) {
+            require(boundsType != ComplicationBoundsType.BACKGROUND)
+            if (field == value) {
                 return
             }
-            _unitSquareBounds = value
+            field = value
             unitSquareBoundsDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to coalesce
@@ -363,22 +388,19 @@
             complicationsManager.scheduleUpdate()
         }
 
-    private var _enabled = true
     internal var enabledDirty = true
 
-    /**
-     * Whether or not the complication should be drawn and accept taps.
-     */
-    public var enabled: Boolean
+    /** Whether or not the complication should be drawn and accept taps. */
+    public var enabled: Boolean = true
         @JvmName("isEnabled")
         @UiThread
-        get() = _enabled
+        get
         @UiThread
         set(value) {
-            if (_enabled == value) {
+            if (field == value) {
                 return
             }
-            _enabled = value
+            field = value
             enabledDirty = true
 
             // The caller might enable/disable a number of complications. For efficiency we need
@@ -388,40 +410,34 @@
             }
         }
 
-    private var _renderer = canvasComplication
-
-    /**
-     * The [CanvasComplication] used to render the complication.
-     */
-    public var renderer: CanvasComplication
+    /** The [CanvasComplication] used to render the complication. */
+    public var renderer: CanvasComplication = canvasComplication
         @UiThread
-        get() = _renderer
+        get
         @UiThread
         set(value) {
-            if (_renderer == value) {
+            if (field == value) {
                 return
             }
             renderer.onDetach()
             value.idAndData = renderer.idAndData
-            _renderer = value
+            field = value
             value.onAttach(this)
         }
 
-    private var _supportedTypes = supportedTypes
     internal var supportedTypesDirty = true
 
-    /**
-     * The types of complications the complication supports.
-     */
-    public var supportedTypes: List<ComplicationType>
+    /** The types of complications the complication supports. Must be non-empty. */
+    public var supportedTypes: List<ComplicationType> = supportedTypes
         @UiThread
-        get() = _supportedTypes
+        get
         @UiThread
         set(value) {
-            if (_supportedTypes == value) {
+            if (field == value) {
                 return
             }
-            _supportedTypes = value
+            require(value.isNotEmpty())
+            field = value
             supportedTypesDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to
@@ -431,22 +447,21 @@
             }
         }
 
-    private var _defaultProviderPolicy = defaultProviderPolicy
     internal var defaultProviderPolicyDirty = true
 
     /**
      * The [DefaultComplicationProviderPolicy] which defines the default complications providers
-     * selected when the user hasn't yet made a choice. See also [.defaultProviderType].
+     * selected when the user hasn't yet made a choice. See also [defaultProviderType].
      */
-    public var defaultProviderPolicy: DefaultComplicationProviderPolicy
+    public var defaultProviderPolicy: DefaultComplicationProviderPolicy = defaultProviderPolicy
         @UiThread
-        get() = _defaultProviderPolicy
+        get
         @UiThread
         set(value) {
-            if (_defaultProviderPolicy == value) {
+            if (field == value) {
                 return
             }
-            _defaultProviderPolicy = value
+            field = value
             defaultProviderPolicyDirty = true
 
             // The caller might modify a number of complications. For efficiency we need to
@@ -459,11 +474,11 @@
     internal var defaultProviderTypeDirty = true
 
     /**
-     * The default [ComplicationData.ComplicationType] to use alongside [.defaultProviderPolicy].
+     * The default [ComplicationData.ComplicationType] to use alongside [defaultProviderPolicy].
      */
     public var defaultProviderType: ComplicationType = defaultProviderType
         @UiThread
-        get() = field
+        get
         @UiThread
         set(value) {
             if (field == value) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
index 57699ba..4d5bb5d 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationOutlineRenderer.kt
@@ -30,9 +30,9 @@
 public class ComplicationOutlineRenderer {
     public companion object {
         // Dashed lines are used for complication selection.
-        internal val DASH_WIDTH = 10.0f
-        internal var DASH_GAP = 2.0f
-        internal var DASH_LENGTH = 5.0f
+        internal const val DASH_WIDTH = 10.0f
+        internal const val DASH_GAP = 2.0f
+        internal const val DASH_LENGTH = 5.0f
 
         internal val dashPaint = Paint().apply {
             strokeWidth = DASH_WIDTH
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
index b9d9486..05e66c6 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ComplicationsManager.kt
@@ -39,18 +39,17 @@
 
 private fun getComponentName(context: Context) = ComponentName(
     context.packageName,
-    context.javaClass.typeName
+    context.javaClass.name
 )
 
 /**
- * The [Complication]s associated with the [WatchFace]. Dynamic creation of
- * complications isn't supported, however complications can be enabled and disabled, perhaps as
- * part of a user style see [androidx.wear.watchface.style.UserStyleSetting].
+ * The [Complication]s associated with the [WatchFace]. Dynamic creation of complications isn't
+ * supported, however complications can be enabled and disabled, perhaps as part of a
+ * user style see [androidx.wear.watchface.style.UserStyleSetting] and
+ * [ComplicationsUserStyleSetting].
  */
 public class ComplicationsManager(
-    /**
-     * The complications associated with the watch face, may be empty.
-     */
+    /** The complications associated with the watch face, may be empty. */
     complicationCollection: Collection<Complication>,
 
     /**
@@ -59,6 +58,10 @@
      */
     private val userStyleRepository: UserStyleRepository
 ) {
+    /**
+     * Interface used to report user taps on the complication. See [addTapListener] and
+     * [removeTapListener].
+     */
     public interface TapCallback {
         /**
          * Called when the user single taps on a complication.
@@ -81,12 +84,11 @@
     private lateinit var renderer: Renderer
     private lateinit var pendingUpdate: CancellableUniqueTask
 
-    // A map of IDs to complications.
+    /** A map of complication IDs to complications. */
     public val complications: Map<Int, Complication> =
         complicationCollection.associateBy(Complication::id)
 
     private class InitialComplicationConfig(
-        val id: Int,
         val unitSquareBounds: RectF,
         val enabled: Boolean,
         val supportedTypes: List<ComplicationType>,
@@ -102,7 +104,6 @@
             { it.id },
             {
                 InitialComplicationConfig(
-                    it.id,
                     it.unitSquareBounds,
                     it.enabled,
                     it.supportedTypes,
@@ -188,7 +189,7 @@
         }
     }
 
-    /** Returns the [Complication] corresponding to id or null. */
+    /** Returns the [Complication] corresponding to [id], if there is one, or `null`. */
     public operator fun get(id: Int): Complication? = complications[id]
 
     internal fun scheduleUpdate() {
@@ -299,8 +300,8 @@
     }
 
     /**
-     * Brings attention to the complication by briefly highlighting it to provide visual
-     * feedback when the user has tapped on it.
+     * Brings attention to the complication by briefly highlighting it to provide visual feedback
+     * when the user has tapped on it.
      *
      * @param complicationId The watch face's ID of the complication to briefly highlight
      */
@@ -324,7 +325,7 @@
     }
 
     /**
-     * Returns the id of the complication at coordinates x, y or {@code null} if there isn't one.
+     * Returns the id of the complication at coordinates x, y or `null` if there isn't one.
      *
      * @param x The x coordinate of the point to perform a hit test
      * @param y The y coordinate of the point to perform a hit test
@@ -337,9 +338,9 @@
         }?.value
 
     /**
-     * Returns the background complication if there is one or {@code null} otherwise.
+     * Returns the background complication if there is one or `null` otherwise.
      *
-     * @return The background complication if there is one or {@code null} otherwise
+     * @return The background complication if there is one or `null` otherwise
      */
     public fun getBackgroundComplication(): Complication? =
         complications.entries.firstOrNull {
@@ -410,8 +411,7 @@
     }
 
     /**
-     * Adds a [TapCallback] which is called whenever the user interacts with a
-     * complication.
+     * Adds a [TapCallback] which is called whenever the user interacts with a complication.
      */
     @UiThread
     @SuppressLint("ExecutorRegistration")
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
deleted file mode 100644
index 8e7cde4..0000000
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesRenderer.kt
+++ /dev/null
@@ -1,312 +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.wear.watchface
-
-import android.annotation.SuppressLint
-import android.graphics.Bitmap
-import android.icu.util.Calendar
-import android.opengl.EGL14
-import android.opengl.EGLConfig
-import android.opengl.EGLContext
-import android.opengl.EGLDisplay
-import android.opengl.EGLSurface
-import android.opengl.GLES20
-import android.util.Log
-import android.view.SurfaceHolder
-import androidx.annotation.CallSuper
-import androidx.annotation.IntRange
-import androidx.annotation.UiThread
-import androidx.wear.watchface.style.UserStyleRepository
-
-import java.nio.ByteBuffer
-
-internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
-    EGL14.EGL_RENDERABLE_TYPE,
-    EGL14.EGL_OPENGL_ES2_BIT,
-    EGL14.EGL_RED_SIZE,
-    8,
-    EGL14.EGL_GREEN_SIZE,
-    8,
-    EGL14.EGL_BLUE_SIZE,
-    8,
-    EGL14.EGL_ALPHA_SIZE,
-    8,
-    EGL14.EGL_NONE
-)
-
-private val EGL_CONTEXT_ATTRIB_LIST =
-    intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
-
-internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
-
-/**
- * Watch faces that require [GLES20] rendering should extend their [Renderer] from this
- * class.
- */
-public abstract class GlesRenderer @JvmOverloads constructor(
-    /** The [SurfaceHolder] that [render] will draw into. */
-    surfaceHolder: SurfaceHolder,
-
-    /** The associated [UserStyleRepository]. */
-    userStyleRepository: UserStyleRepository,
-
-    /** The associated [WatchState]. */
-    watchState: WatchState,
-
-    /**
-     * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
-     * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces are
-     * recommended to use lower frame rates if possible for better battery life. Variable frame
-     * rates can also help preserve battery life, e.g. if a watch face has a short animation once
-     * per second it can adjust the frame rate inorder to sleep when not animating.
-     */
-    @IntRange(from = 0, to = 10000)
-    interactiveDrawModeUpdateDelayMillis: Long,
-
-    /** Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBAB8888 back buffer. */
-    private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
-
-    /** The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty. */
-    private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
-) : Renderer(surfaceHolder, userStyleRepository, watchState, interactiveDrawModeUpdateDelayMillis) {
-    /** @hide */
-    private companion object {
-        private const val TAG = "Gles2WatchFace"
-    }
-
-    private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
-        if (this == EGL14.EGL_NO_DISPLAY) {
-            throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
-        }
-        // Initialize the display. The major and minor version numbers are passed back.
-        val version = IntArray(2)
-        if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
-            throw RuntimeException("eglInitialize failed")
-        }
-    }
-
-    private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
-
-    @SuppressWarnings("SyntheticAccessor")
-    private var eglContext: EGLContext? = EGL14.eglCreateContext(
-        eglDisplay,
-        eglConfig,
-        EGL14.EGL_NO_CONTEXT,
-        EGL_CONTEXT_ATTRIB_LIST,
-        0
-    )
-
-    init {
-        if (eglContext == EGL14.EGL_NO_CONTEXT) {
-            throw RuntimeException("eglCreateContext failed")
-        }
-    }
-
-    private var eglSurface: EGLSurface? = null
-    private var calledOnGlContextCreated = false
-
-    /**
-     * Chooses the EGLConfig to use.
-     * @throws RuntimeException if [EGL14.eglChooseConfig] fails
-     */
-    private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
-        val numEglConfigs = IntArray(1)
-        val eglConfigs = arrayOfNulls<EGLConfig>(1)
-        if (!EGL14.eglChooseConfig(
-                eglDisplay,
-                eglConfigAttribList,
-                0,
-                eglConfigs,
-                0,
-                eglConfigs.size,
-                numEglConfigs,
-                0
-            )
-        ) {
-            throw RuntimeException("eglChooseConfig failed")
-        }
-        if (numEglConfigs[0] == 0) {
-            throw RuntimeException("no matching EGL configs")
-        }
-        return eglConfigs[0]!!
-    }
-
-    private fun createWindowSurface(width: Int, height: Int) {
-        if (eglSurface != null) {
-            if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                Log.w(TAG, "eglDestroySurface failed")
-            }
-        }
-        eglSurface = EGL14.eglCreateWindowSurface(
-            eglDisplay,
-            eglConfig,
-            surfaceHolder.surface,
-            eglSurfaceAttribList,
-            0
-        )
-        if (eglSurface == EGL14.EGL_NO_SURFACE) {
-            throw RuntimeException("eglCreateWindowSurface failed")
-        }
-
-        makeContextCurrent()
-        GLES20.glViewport(0, 0, width, height)
-        if (!calledOnGlContextCreated) {
-            calledOnGlContextCreated = true
-            onGlContextCreated()
-        }
-        onGlSurfaceCreated(width, height)
-    }
-
-    @CallSuper
-    override fun onDestroy() {
-        if (eglSurface != null) {
-            if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                Log.w(TAG, "eglDestroySurface failed")
-            }
-            eglSurface = null
-        }
-        if (eglContext != null) {
-            if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
-                Log.w(TAG, "eglDestroyContext failed")
-            }
-            eglContext = null
-        }
-        if (eglDisplay != null) {
-            if (!EGL14.eglTerminate(eglDisplay)) {
-                Log.w(TAG, "eglTerminate failed")
-            }
-            eglDisplay = null
-        }
-    }
-
-    /**
-     * Sets our GL context to be the current one. This method *must* be called before any
-     * OpenGL APIs are used.
-     */
-    private fun makeContextCurrent() {
-        if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
-            throw RuntimeException("eglMakeCurrent failed")
-        }
-    }
-
-    internal override fun onPostCreate() {
-        surfaceHolder.addCallback(object : SurfaceHolder.Callback {
-            @SuppressLint("SyntheticAccessor")
-            override fun surfaceChanged(
-                holder: SurfaceHolder,
-                format: Int,
-                width: Int,
-                height: Int
-            ) {
-                createWindowSurface(width, height)
-            }
-
-            @SuppressLint("SyntheticAccessor")
-            override fun surfaceDestroyed(holder: SurfaceHolder) {
-                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
-                    Log.w(TAG, "eglDestroySurface failed")
-                }
-                eglSurface = null
-            }
-
-            override fun surfaceCreated(holder: SurfaceHolder) {
-            }
-        })
-
-        createWindowSurface(
-            surfaceHolder.surfaceFrame.width(),
-            surfaceHolder.surfaceFrame.height()
-        )
-    }
-
-    /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
-    @UiThread
-    public open fun onGlContextCreated() {}
-
-    /**
-     * Called when a new GL surface is created. It's safe to use GL APIs in this method.
-     *
-     * @param width width of surface in pixels
-     * @param height height of surface in pixels
-     */
-    @UiThread
-    public open fun onGlSurfaceCreated(width: Int, height: Int) {}
-
-    internal override fun renderInternal(
-        calendar: Calendar
-    ) {
-        makeContextCurrent()
-        render(calendar)
-        if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
-            Log.w(TAG, "eglSwapBuffers failed")
-        }
-    }
-
-    /** {@inheritDoc} */
-    internal override fun takeScreenshot(
-        calendar: Calendar,
-        renderParameters: RenderParameters
-    ): Bitmap {
-        val width = screenBounds.width()
-        val height = screenBounds.height()
-        val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
-        makeContextCurrent()
-        val prevRenderParameters = this.renderParameters
-        this.renderParameters = renderParameters
-        render(calendar)
-        this.renderParameters = prevRenderParameters
-        GLES20.glFinish()
-        GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf)
-        // The image is flipped when using read pixels because the first pixel in the OpenGL buffer
-        // is in bottom left.
-        verticalFlip(pixelBuf, width, height)
-        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
-        bitmap.copyPixelsFromBuffer(pixelBuf)
-        return bitmap
-    }
-
-    private fun verticalFlip(
-        buffer: ByteBuffer,
-        width: Int,
-        height: Int
-    ) {
-        var i = 0
-        val tmp = ByteArray(width * 4)
-        while (i++ < height / 2) {
-            buffer[tmp]
-            System.arraycopy(
-                buffer.array(),
-                buffer.limit() - buffer.position(),
-                buffer.array(),
-                buffer.position() - width * 4,
-                width * 4
-            )
-            System.arraycopy(tmp, 0, buffer.array(), buffer.limit() - buffer.position(), width * 4)
-        }
-        buffer.rewind()
-    }
-
-    /**
-     * Sub-classes should override this to implement their rendering logic which should respect
-     * the current [DrawMode]. For correct functioning watch faces must use the supplied
-     * [Calendar] and avoid using any other ways of getting the time.
-     *
-     * @param calendar The current [Calendar]
-     */
-    @UiThread
-    public abstract fun render(calendar: Calendar)
-}
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
index 1a8cf62..4caca47 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/GlesTextureComplication.kt
@@ -25,7 +25,10 @@
 import android.opengl.GLUtils
 import androidx.annotation.Px
 
-/** Helper for rendering a complication to a GLES20 texture. */
+/**
+ * Helper for rendering a [CanvasComplication] to a GLES20 texture. To use call [renderToTexture]
+ * and then [bind] before drawing.
+ */
 public class GlesTextureComplication(
     /** The [CanvasComplication] to render to texture. */
     public val canvasComplication: CanvasComplication,
@@ -50,7 +53,7 @@
     private val canvas = Canvas(bitmap)
     private val bounds = Rect(0, 0, textureWidth, textureHeight)
 
-    /** Renders the complication to an OpenGL texture. */
+    /** Renders [canvasComplication] to an OpenGL texture. */
     public fun renderToTexture(calendar: Calendar, renderParameters: RenderParameters) {
         canvas.drawColor(Color.BLACK)
         canvasComplication.render(canvas, bounds, calendar, renderParameters)
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
index d7f7ada..52ab4b0 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/ObservableWatchData.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.UiThread
 
 /**
- * An observable UI thread only data holder class.
+ * An observable UI thread only data holder class (see [Observer]).
  *
  * @param <T> The type of data hold by this instance
  */
@@ -30,11 +30,13 @@
     private val toBeRemoved = HashSet<Observer<T>>()
 
     /** Whether or not this ObservableWatchData contains a value. */
+    @UiThread
     public fun hasValue(): Boolean = _value != null
 
     /**
      * Returns the value contained within this ObservableWatchData or default if there isn't one.
      */
+    @UiThread
     public fun getValueOr(default: T): T = if (_value != null) {
         _value!!
     } else {
@@ -67,9 +69,9 @@
         }
 
     /**
-     * Adds the given observer to the observers list. The events are dispatched on the ui thread.
-     * If there's any data held within the ObservableWatchData it will be immediately delivered to
-     * the observer.
+     * Adds the given [Observer] to the observers list. If [hasValue] would return true then
+     * [Observer.onChanged] will be called. Subsequently [Observer.onChanged] will also be called
+     * any time [value] changes. All of these callbacks are assumed to occur on the UI thread.
      */
     @UiThread
     public fun addObserver(observer: Observer<T>) {
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
index 8a6ff58..bb4754b 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/Renderer.kt
@@ -16,17 +16,78 @@
 
 package androidx.wear.watchface
 
+import android.annotation.SuppressLint
 import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
 import android.graphics.Rect
 import android.icu.util.Calendar
+import android.opengl.EGL14
+import android.opengl.EGLConfig
+import android.opengl.EGLContext
+import android.opengl.EGLDisplay
+import android.opengl.EGLSurface
+import android.opengl.GLES20
+import android.util.Log
 import android.view.SurfaceHolder
+import androidx.annotation.CallSuper
+import androidx.annotation.IntDef
 import androidx.annotation.IntRange
 import androidx.annotation.Px
 import androidx.annotation.UiThread
+import androidx.wear.watchface.Renderer.CanvasRenderer
+import androidx.wear.watchface.Renderer.GlesRenderer
 import androidx.wear.watchface.style.UserStyleRepository
+import java.nio.ByteBuffer
+
+/**
+ * Describes the type of [Canvas] a [CanvasRenderer] should request from a [SurfaceHolder].
+ *
+ * @hide
+ */
+@IntDef(
+    value = [
+        CanvasType.SOFTWARE,
+        CanvasType.HARDWARE
+    ]
+)
+public annotation class CanvasType {
+    public companion object {
+        /** A software canvas will be requested. */
+        public const val SOFTWARE: Int = 0
+
+        /**
+         * A hardware canvas will be requested. This is usually faster than software rendering,
+         * however it can sometimes increase battery usage by rendering at a higher frame rate.
+         *
+         * NOTE this is only supported on API level 26 and above. On lower API levels we fall back
+         * to a software canvas.
+         */
+        public const val HARDWARE: Int = 1
+    }
+}
+
+internal val EGL_CONFIG_ATTRIB_LIST = intArrayOf(
+    EGL14.EGL_RENDERABLE_TYPE,
+    EGL14.EGL_OPENGL_ES2_BIT,
+    EGL14.EGL_RED_SIZE,
+    8,
+    EGL14.EGL_GREEN_SIZE,
+    8,
+    EGL14.EGL_BLUE_SIZE,
+    8,
+    EGL14.EGL_ALPHA_SIZE,
+    8,
+    EGL14.EGL_NONE
+)
+
+private val EGL_CONTEXT_ATTRIB_LIST =
+    intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
+
+internal val EGL_SURFACE_ATTRIB_LIST = intArrayOf(EGL14.EGL_NONE)
 
 /** The base class for [CanvasRenderer] and [GlesRenderer]. */
-public abstract class Renderer(
+public sealed class Renderer(
     /** The [SurfaceHolder] that [renderInternal] will draw into. */
     public val surfaceHolder: SurfaceHolder,
 
@@ -135,8 +196,8 @@
     ): Bitmap
 
     /**
-     * Called when the [DrawMode] has been updated. Will always be called before the first
-     * call to onDraw().
+     * Called when the [RenderParameters] has been updated. Will always be called before the first
+     * call to [CanvasRenderer.render] or [GlesRenderer.render].
      */
     @UiThread
     protected open fun onRenderParametersChanged(renderParameters: RenderParameters) {
@@ -176,10 +237,15 @@
     public open fun shouldAnimate(): Boolean =
         watchState.isVisible.value && !watchState.isAmbient.value
 
-    /** Schedules a call to [renderInternal] to draw the next frame. */
+    /**
+     * Schedules a call to either [CanvasRenderer.render] or [GlesRenderer.render] to draw the next
+     * frame.
+     */
     @UiThread
     public fun invalidate() {
-        watchFaceHostApi.invalidate()
+        if (this::watchFaceHostApi.isInitialized) {
+            watchFaceHostApi.invalidate()
+        }
     }
 
     /**
@@ -187,6 +253,383 @@
      * [invalidate], this method is thread-safe and may be called on any thread.
      */
     public fun postInvalidate() {
-        watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+        if (this::watchFaceHostApi.isInitialized) {
+            watchFaceHostApi.getHandler().post { watchFaceHostApi.invalidate() }
+        }
+    }
+
+    /**
+     * Watch faces that require [Canvas] rendering should extend their [Renderer] from this class.
+     */
+    public abstract class CanvasRenderer(
+        /**
+         * The [SurfaceHolder] from which a [Canvas] to will be obtained and passed into [render].
+         */
+        surfaceHolder: SurfaceHolder,
+
+        /** The watch face's associated [UserStyleRepository]. */
+        userStyleRepository: UserStyleRepository,
+
+        /** The watch face's associated [WatchState]. */
+        watchState: WatchState,
+
+        /** The type of canvas to request. */
+        @CanvasType private val canvasType: Int,
+
+        /**
+         * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+         * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+         * are recommended to use lower frame rates if possible for better battery life. Variable
+         * frame  rates can also help preserve battery life, e.g. if a watch face has a short
+         * animation once per second it can adjust the frame rate inorder to sleep when not
+         * animating.
+         */
+        @IntRange(from = 0, to = 10000)
+        interactiveDrawModeUpdateDelayMillis: Long
+    ) : Renderer(
+        surfaceHolder,
+        userStyleRepository,
+        watchState,
+        interactiveDrawModeUpdateDelayMillis
+    ) {
+
+        @SuppressWarnings("UnsafeNewApiCall") // We check if the SDK is new enough.
+        internal override fun renderInternal(
+            calendar: Calendar
+        ) {
+            val canvas = (
+                if (canvasType == CanvasType.HARDWARE && android.os.Build.VERSION.SDK_INT >= 26) {
+                    surfaceHolder.lockHardwareCanvas() // Requires API level 26.
+                } else {
+                    surfaceHolder.lockCanvas()
+                }
+                ) ?: return
+            try {
+                if (watchState.isVisible.value) {
+                    render(canvas, surfaceHolder.surfaceFrame, calendar)
+                } else {
+                    canvas.drawColor(Color.BLACK)
+                }
+            } finally {
+                surfaceHolder.unlockCanvasAndPost(canvas)
+            }
+        }
+
+        /** {@inheritDoc} */
+        internal override fun takeScreenshot(
+            calendar: Calendar,
+            renderParameters: RenderParameters
+        ): Bitmap {
+            val bitmap = Bitmap.createBitmap(
+                screenBounds.width(),
+                screenBounds.height(),
+                Bitmap.Config.ARGB_8888
+            )
+            val prevRenderParameters = this.renderParameters
+            this.renderParameters = renderParameters
+            render(Canvas(bitmap), screenBounds, calendar)
+            this.renderParameters = prevRenderParameters
+            return bitmap
+        }
+
+        /**
+         * Sub-classes should override this to implement their rendering logic which should respect
+         * the current [DrawMode]. For correct functioning the CanvasRenderer must use the supplied
+         * [Calendar] in favor of any other ways of getting the time.
+         *
+         * @param canvas The [Canvas] to render into. Don't assume this is always the canvas from
+         *     the [SurfaceHolder] backing the display
+         * @param bounds A [Rect] describing the bonds of the canvas to draw into
+         * @param calendar The current [Calendar]
+         */
+        @UiThread
+        public abstract fun render(
+            canvas: Canvas,
+            bounds: Rect,
+            calendar: Calendar
+        )
+    }
+
+    /**
+     * Watch faces that require [GLES20] rendering should extend their [Renderer] from this class.
+     */
+    public abstract class GlesRenderer @JvmOverloads constructor(
+        /** The [SurfaceHolder] whose [android.view.Surface] [render] will draw into. */
+        surfaceHolder: SurfaceHolder,
+
+        /** The associated [UserStyleRepository]. */
+        userStyleRepository: UserStyleRepository,
+
+        /** The associated [WatchState]. */
+        watchState: WatchState,
+
+        /**
+         * The interval in milliseconds between frames in interactive [DrawMode]s. To render at 60hz
+         * set to 16. Note when battery is low, the frame rate will be clamped to 10fps. Watch faces
+         * are recommended to use lower frame rates if possible for better battery life. Variable
+         * frame rates can also help preserve battery life, e.g. if a watch face has a short
+         * animation once per second it can adjust the frame rate inorder to sleep when not
+         * animating.
+         */
+        @IntRange(from = 0, to = 10000)
+        interactiveDrawModeUpdateDelayMillis: Long,
+
+        /**
+         * Attributes for [EGL14.eglChooseConfig]. By default this selects an RGBA8888 back buffer.
+         */
+        private val eglConfigAttribList: IntArray = EGL_CONFIG_ATTRIB_LIST,
+
+        /**
+         * The attributes to be passed to [EGL14.eglCreateWindowSurface]. By default this is empty.
+         */
+        private val eglSurfaceAttribList: IntArray = EGL_SURFACE_ATTRIB_LIST
+    ) : Renderer(
+        surfaceHolder,
+        userStyleRepository,
+        watchState,
+        interactiveDrawModeUpdateDelayMillis
+    ) {
+        /** @hide */
+        private companion object {
+            private const val TAG = "Gles2WatchFace"
+        }
+
+        private var eglDisplay: EGLDisplay? = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY).apply {
+            if (this == EGL14.EGL_NO_DISPLAY) {
+                throw RuntimeException("eglGetDisplay returned EGL_NO_DISPLAY")
+            }
+            // Initialize the display. The major and minor version numbers are passed back.
+            val version = IntArray(2)
+            if (!EGL14.eglInitialize(this, version, 0, version, 1)) {
+                throw RuntimeException("eglInitialize failed")
+            }
+        }
+
+        private var eglConfig: EGLConfig = chooseEglConfig(eglDisplay!!)
+
+        @SuppressWarnings("SyntheticAccessor")
+        private var eglContext: EGLContext? = EGL14.eglCreateContext(
+            eglDisplay,
+            eglConfig,
+            EGL14.EGL_NO_CONTEXT,
+            EGL_CONTEXT_ATTRIB_LIST,
+            0
+        )
+
+        init {
+            if (eglContext == EGL14.EGL_NO_CONTEXT) {
+                throw RuntimeException("eglCreateContext failed")
+            }
+        }
+
+        private var eglSurface: EGLSurface? = null
+        private var calledOnGlContextCreated = false
+
+        /**
+         * Chooses the EGLConfig to use.
+         * @throws RuntimeException if [EGL14.eglChooseConfig] fails
+         */
+        private fun chooseEglConfig(eglDisplay: EGLDisplay): EGLConfig {
+            val numEglConfigs = IntArray(1)
+            val eglConfigs = arrayOfNulls<EGLConfig>(1)
+            if (!EGL14.eglChooseConfig(
+                    eglDisplay,
+                    eglConfigAttribList,
+                    0,
+                    eglConfigs,
+                    0,
+                    eglConfigs.size,
+                    numEglConfigs,
+                    0
+                )
+            ) {
+                throw RuntimeException("eglChooseConfig failed")
+            }
+            if (numEglConfigs[0] == 0) {
+                throw RuntimeException("no matching EGL configs")
+            }
+            return eglConfigs[0]!!
+        }
+
+        private fun createWindowSurface(width: Int, height: Int) {
+            if (eglSurface != null) {
+                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                    Log.w(TAG, "eglDestroySurface failed")
+                }
+            }
+            eglSurface = EGL14.eglCreateWindowSurface(
+                eglDisplay,
+                eglConfig,
+                surfaceHolder.surface,
+                eglSurfaceAttribList,
+                0
+            )
+            if (eglSurface == EGL14.EGL_NO_SURFACE) {
+                throw RuntimeException("eglCreateWindowSurface failed")
+            }
+
+            makeContextCurrent()
+            GLES20.glViewport(0, 0, width, height)
+            if (!calledOnGlContextCreated) {
+                calledOnGlContextCreated = true
+                onGlContextCreated()
+            }
+            onGlSurfaceCreated(width, height)
+        }
+
+        @CallSuper
+        override fun onDestroy() {
+            if (eglSurface != null) {
+                if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                    Log.w(TAG, "eglDestroySurface failed")
+                }
+                eglSurface = null
+            }
+            if (eglContext != null) {
+                if (!EGL14.eglDestroyContext(eglDisplay, eglContext)) {
+                    Log.w(TAG, "eglDestroyContext failed")
+                }
+                eglContext = null
+            }
+            if (eglDisplay != null) {
+                if (!EGL14.eglTerminate(eglDisplay)) {
+                    Log.w(TAG, "eglTerminate failed")
+                }
+                eglDisplay = null
+            }
+        }
+
+        /**
+         * Sets our GL context to be the current one. This method *must* be called before any OpenGL
+         * APIs are used.
+         */
+        private fun makeContextCurrent() {
+            if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
+                throw RuntimeException("eglMakeCurrent failed")
+            }
+        }
+
+        internal override fun onPostCreate() {
+            surfaceHolder.addCallback(object : SurfaceHolder.Callback {
+                @SuppressLint("SyntheticAccessor")
+                override fun surfaceChanged(
+                    holder: SurfaceHolder,
+                    format: Int,
+                    width: Int,
+                    height: Int
+                ) {
+                    createWindowSurface(width, height)
+                }
+
+                @SuppressLint("SyntheticAccessor")
+                override fun surfaceDestroyed(holder: SurfaceHolder) {
+                    if (!EGL14.eglDestroySurface(eglDisplay, eglSurface)) {
+                        Log.w(TAG, "eglDestroySurface failed")
+                    }
+                    eglSurface = null
+                }
+
+                override fun surfaceCreated(holder: SurfaceHolder) {
+                }
+            })
+
+            createWindowSurface(
+                surfaceHolder.surfaceFrame.width(),
+                surfaceHolder.surfaceFrame.height()
+            )
+        }
+
+        /** Called when a new GL context is created. It's safe to use GL APIs in this method. */
+        @UiThread
+        public open fun onGlContextCreated() {
+        }
+
+        /**
+         * Called when a new GL surface is created. It's safe to use GL APIs in this method.
+         *
+         * @param width width of surface in pixels
+         * @param height height of surface in pixels
+         */
+        @UiThread
+        public open fun onGlSurfaceCreated(width: Int, height: Int) {
+        }
+
+        internal override fun renderInternal(
+            calendar: Calendar
+        ) {
+            makeContextCurrent()
+            render(calendar)
+            if (!EGL14.eglSwapBuffers(eglDisplay, eglSurface)) {
+                Log.w(TAG, "eglSwapBuffers failed")
+            }
+        }
+
+        /** {@inheritDoc} */
+        internal override fun takeScreenshot(
+            calendar: Calendar,
+            renderParameters: RenderParameters
+        ): Bitmap {
+            val width = screenBounds.width()
+            val height = screenBounds.height()
+            val pixelBuf = ByteBuffer.allocateDirect(width * height * 4)
+            makeContextCurrent()
+            val prevRenderParameters = this.renderParameters
+            this.renderParameters = renderParameters
+            render(calendar)
+            this.renderParameters = prevRenderParameters
+            GLES20.glFinish()
+            GLES20.glReadPixels(
+                0,
+                0,
+                width,
+                height,
+                GLES20.GL_RGBA,
+                GLES20.GL_UNSIGNED_BYTE,
+                pixelBuf
+            )
+            // The image is flipped when using read pixels because the first pixel in the OpenGL
+            // buffer is in bottom left.
+            verticalFlip(pixelBuf, width, height)
+            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+            bitmap.copyPixelsFromBuffer(pixelBuf)
+            return bitmap
+        }
+
+        private fun verticalFlip(
+            buffer: ByteBuffer,
+            width: Int,
+            height: Int
+        ) {
+            var i = 0
+            val tmp = ByteArray(width * 4)
+            while (i++ < height / 2) {
+                buffer[tmp]
+                System.arraycopy(
+                    buffer.array(),
+                    buffer.limit() - buffer.position(),
+                    buffer.array(),
+                    buffer.position() - width * 4,
+                    width * 4
+                )
+                System.arraycopy(
+                    tmp,
+                    0,
+                    buffer.array(),
+                    buffer.limit() - buffer.position(),
+                    width * 4
+                )
+            }
+            buffer.rewind()
+        }
+
+        /**
+         * Sub-classes should override this to implement their rendering logic which should respect
+         * the current [DrawMode]. For correct functioning the GlesRenderer must use the supplied
+         * [Calendar] in favor of any other ways of getting the time.
+         *
+         * @param calendar The current [Calendar]
+         */
+        @UiThread
+        public abstract fun render(calendar: Calendar)
     }
 }
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
index cdb2fff..a101a9c 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFace.kt
@@ -105,8 +105,8 @@
 }
 
 /**
- * A WatchFace is constructed by a user's [WatchFaceService] and brings together rendering,
- * styling, complications and state observers.
+ * The return value of [WatchFaceService.createWatchFace] which brings together rendering, styling,
+ * complications and state observers.
  */
 public class WatchFace(
     /**
@@ -115,13 +115,13 @@
      */
     @WatchFaceType internal var watchFaceType: Int,
 
-    /** The {@UserStyleRepository} for this WatchFaceImpl. */
+    /** The [UserStyleRepository] for this WatchFace. */
     internal val userStyleRepository: UserStyleRepository,
 
-    /** The [ComplicationsManager] for this WatchFaceImpl. */
+    /** The [ComplicationsManager] for this WatchFace. */
     internal var complicationsManager: ComplicationsManager,
 
-    /** The [Renderer] for this WatchFaceImpl. */
+    /** The [Renderer] for this WatchFace. */
     internal val renderer: Renderer
 ) {
     internal var tapListener: TapListener? = null
@@ -210,7 +210,7 @@
         }
     }
 
-    /** The preview time in milliseconds since the epoch, or null if not set. */
+    /** The UTC preview time in milliseconds since the epoch, or null if not set. */
     @get:SuppressWarnings("AutoBoxing")
     @IntRange(from = 0)
     public var overridePreviewReferenceTimeMillis: Long? = null
@@ -363,7 +363,7 @@
     private val componentName =
         ComponentName(
             watchFaceHostApi.getContext().packageName,
-            watchFaceHostApi.getContext().javaClass.typeName
+            watchFaceHostApi.getContext().javaClass.name
         )
 
     internal fun getWatchFaceStyle() = WatchFaceStyle(
@@ -419,7 +419,7 @@
         } else {
             // The system doesn't support preference persistence we need to do it ourselves.
             val preferencesFile =
-                "watchface_prefs_${watchFaceHostApi.getContext().javaClass.typeName}.txt"
+                "watchface_prefs_${watchFaceHostApi.getContext().javaClass.name}.txt"
 
             userStyleRepository.userStyle = UserStyle(
                 readPrefs(watchFaceHostApi.getContext(), preferencesFile),
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
index 4b41029..8067f95 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchFaceService.kt
@@ -121,22 +121,19 @@
 /**
  * WatchFaceService and [WatchFace] are a pair of classes intended to handle much of
  * the boilerplate needed to implement a watch face without being too opinionated. The suggested
- * structure of an WatchFaceService based watch face is:
+ * structure of a WatchFaceService based watch face is:
  *
  * @sample androidx.wear.watchface.samples.kDocCreateExampleWatchFaceService
  *
- * Base classes for complications and styles are provided along with a default UI for configuring
- * them. Complications are optional, however if required, WatchFaceService assumes all
- * complications can be enumerated up front and passed as a collection into [ComplicationsManager]'s
- * constructor which is passed to [WatchFace]'s constructor. Some watch faces support different
- * configurations (number & position) of complications and this can be achieved by rendering a
- * subset and only marking the ones you need as enabled (see
- * [UserStyleSetting.ComplicationsUserStyleSetting]).
+ * Sub classes of WatchFaceService are expected to implement [createWatchFace] which is the
+ * factory for making [WatchFace]s. All [Complication]s are assumed to be enumerated up upfront and
+ * passed as a collection into [ComplicationsManager]'s constructor which is in turn passed to
+ * [WatchFace]'s constructor. Complications can be enabled and disabled as needed although it's
+ * recommended to use [UserStyleSetting.ComplicationsUserStyleSetting] for this.
  *
- * Many watch faces support styles, typically controlling the color and visual look of watch face
- * elements such as numeric fonts, watch hands and ticks. WatchFaceService doesn't take an
- * an opinion on what comprises a style beyond it should be representable as a map of categories to
- * options.
+ * Watch face styling (color and visual look of watch face elements such as numeric fonts, watch
+ * hands and ticks, etc...) is directly supported via [UserStyleSetting] and
+ * [UserStyleRepository].
  *
  * To aid debugging watch face animations, WatchFaceService allows you to speed up or slow down
  * time, and to loop between two instants.  This is controlled by MOCK_TIME_INTENT intents
@@ -199,7 +196,6 @@
  *
  * Multiple watch faces can be defined in the same package, requiring multiple <service> tags.
  */
-@RequiresApi(26)
 public abstract class WatchFaceService : WallpaperService() {
 
     /** @hide */
diff --git a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
index f9888b2..26df645 100644
--- a/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
+++ b/wear/wear-watchface/src/main/java/androidx/wear/watchface/WatchState.kt
@@ -19,25 +19,10 @@
 import android.app.NotificationManager
 import androidx.annotation.RestrictTo
 
-import androidx.annotation.IntDef
-
-/** @hide */
-@IntDef(
-    value = [
-        ScreenShape.ROUND,
-        ScreenShape.RECTANGULAR
-    ]
-)
-public annotation class ScreenShape {
-    public companion object {
-        /** The watch screen has a circular shape. */
-        public const val ROUND: Int = 1
-
-        /** The watch screen has a rectangular or square shape. */
-        public const val RECTANGULAR: Int = 2
-    }
-}
-
+/**
+ * Describes the current state of the wearable including some hardware details such as whether or
+ * not it supports burn in prevention and low-bit ambient.
+ */
 public class WatchState(
     /**
      * The current user interruption settings. See [NotificationManager]. Based on the value
@@ -79,10 +64,6 @@
     @get:JvmName("hasBurnInProtection")
     public val hasBurnInProtection: Boolean,
 
-    /** The physical shape of the screen. */
-    @ScreenShape
-    public val screenShape: Int,
-
     /** UTC reference time for previews of analog watch faces in milliseconds since the epoch. */
     public val analogPreviewReferenceTimeMillis: Long,
 
@@ -100,8 +81,6 @@
     public val isVisible: MutableObservableWatchData<Boolean> = MutableObservableWatchData()
     public var hasLowBitAmbient: Boolean = false
     public var hasBurnInProtection: Boolean = false
-    @ScreenShape
-    public var screenShape: Int = 0
     public var analogPreviewReferenceTimeMillis: Long = 0
     public var digitalPreviewReferenceTimeMillis: Long = 0
 
@@ -112,7 +91,6 @@
         isVisible = isVisible,
         hasLowBitAmbient = hasLowBitAmbient,
         hasBurnInProtection = hasBurnInProtection,
-        screenShape = screenShape,
         analogPreviewReferenceTimeMillis = analogPreviewReferenceTimeMillis,
         digitalPreviewReferenceTimeMillis = digitalPreviewReferenceTimeMillis
     )
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
index e8a1f0d..1165fbe9 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/TestCommon.kt
@@ -169,7 +169,7 @@
     userStyleRepository: UserStyleRepository,
     watchState: WatchState,
     interactiveFrameRateMs: Long
-) : CanvasRenderer(
+) : Renderer.CanvasRenderer(
     surfaceHolder,
     userStyleRepository,
     watchState,
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index 1ad9e17..da685aa 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -1852,4 +1852,17 @@
         assertThat(leftComplication.isActiveAt(2000000)).isTrue()
         assertThat(leftComplication.isActiveAt(2000001)).isFalse()
     }
+
+    @Test
+    fun invalidateRendererBeforeFullInit() {
+        renderer = TestRenderer(
+            surfaceHolder,
+            UserStyleRepository(UserStyleSchema(emptyList())),
+            watchState.asWatchState(),
+            INTERACTIVE_UPDATE_RATE_MS
+        )
+
+        // This should not throw an exception.
+        renderer.invalidate()
+    }
 }
diff --git a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
index 685668c..0c48269 100644
--- a/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
+++ b/wear/wear-watchface/src/test/java/androidx/wear/watchface/ui/WatchFaceConfigUiTest.kt
@@ -18,7 +18,7 @@
 
 import android.content.ComponentName
 import android.content.Context
-import android.graphics.Bitmap
+import android.graphics.Canvas
 import android.graphics.Rect
 import android.graphics.RectF
 import android.icu.util.Calendar
@@ -28,10 +28,10 @@
 import androidx.wear.complications.SystemProviders
 import androidx.wear.complications.data.ComplicationType
 import androidx.wear.watchface.CanvasComplicationDrawable
+import androidx.wear.watchface.CanvasType
 import androidx.wear.watchface.Complication
 import androidx.wear.watchface.ComplicationsManager
 import androidx.wear.watchface.MutableWatchState
-import androidx.wear.watchface.RenderParameters
 import androidx.wear.watchface.Renderer
 import androidx.wear.watchface.WatchFaceTestRunner
 import androidx.wear.watchface.complications.rendering.ComplicationDrawable
@@ -196,20 +196,14 @@
         val complicationSet = ComplicationsManager(
             complications,
             userStyleRepository,
-            object : Renderer(
+            object : Renderer.CanvasRenderer(
                 surfaceHolder,
                 userStyleRepository,
                 watchState.asWatchState(),
+                CanvasType.SOFTWARE,
                 INTERACTIVE_UPDATE_RATE_MS
             ) {
-                override fun renderInternal(calendar: Calendar) {}
-
-                override fun takeScreenshot(
-                    calendar: Calendar,
-                    renderParameters: RenderParameters
-                ): Bitmap {
-                    throw RuntimeException("Not Implemented!")
-                }
+                override fun render(canvas: Canvas, bounds: Rect, calendar: Calendar) {}
             }
         )
 
diff --git a/wear/wear/api/current.txt b/wear/wear/api/current.txt
index 2e3073a..c6f4bf9 100644
--- a/wear/wear/api/current.txt
+++ b/wear/wear/api/current.txt
@@ -317,14 +317,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +341,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -358,7 +357,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(int);
diff --git a/wear/wear/api/public_plus_experimental_current.txt b/wear/wear/api/public_plus_experimental_current.txt
index 13c916ff..ebd25b4 100644
--- a/wear/wear/api/public_plus_experimental_current.txt
+++ b/wear/wear/api/public_plus_experimental_current.txt
@@ -317,14 +317,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -342,7 +341,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -358,7 +357,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(int);
diff --git a/wear/wear/api/restricted_current.txt b/wear/wear/api/restricted_current.txt
index 07cb190..5640c27 100644
--- a/wear/wear/api/restricted_current.txt
+++ b/wear/wear/api/restricted_current.txt
@@ -321,14 +321,13 @@
   }
 
   public static interface WearArcLayout.ArcLayoutWidget {
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getSweepAngleDegrees();
     method public int getThicknessPx();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
   }
 
-  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.LayoutParams {
+  public static class WearArcLayout.LayoutParams extends android.view.ViewGroup.MarginLayoutParams {
     ctor public WearArcLayout.LayoutParams(android.content.Context, android.util.AttributeSet?);
     ctor public WearArcLayout.LayoutParams(int, int);
     ctor public WearArcLayout.LayoutParams(android.view.ViewGroup.LayoutParams);
@@ -349,7 +348,7 @@
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int);
     ctor public WearCurvedTextView(android.content.Context, android.util.AttributeSet?, int, int);
-    method public void checkInvalidAttributeAsChild(boolean);
+    method public void checkInvalidAttributeAsChild();
     method public float getAnchorAngleDegrees();
     method public int getAnchorType();
     method public boolean getClockwise();
@@ -365,7 +364,6 @@
     method public float getTextSize();
     method public int getThicknessPx();
     method public android.graphics.Typeface? getTypeface();
-    method public boolean handleLayoutRotate(float);
     method public boolean insideClickArea(float, float);
     method public void setAnchorAngleDegrees(float);
     method public void setAnchorType(@androidx.wear.widget.WearArcLayout.AnchorType int);
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
index d6b8bb7..39f35a4 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTest.java
@@ -23,19 +23,21 @@
 import static androidx.wear.widget.util.AsyncViewActions.waitForMatchingView;
 
 import static org.hamcrest.Matchers.allOf;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.view.KeyEvent;
 import android.view.View;
 
 import androidx.annotation.IdRes;
 import androidx.annotation.Nullable;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.matcher.ViewMatchers;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.WakeLockRule;
 
@@ -53,131 +55,198 @@
     @Rule
     public final WakeLockRule wakeLock = new WakeLockRule();
 
-    @Rule
-    public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
-            new ActivityTestRule<>(
-                    DismissibleFrameLayoutTestActivity.class,
-                    true, /** initial touch mode */
-                    false /** launchActivity */
-            );
-
     @Test
     public void testBackDismiss() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(true, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, true, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed, and not pass to the activity
+            scenario.onActivity(activity -> {
+                assertFalse(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testBackNotDismissIfDisabled() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND the layout is still nor hidden
-        assertNotHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND the layout is still not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // Back button up event is not consumed, and continue to pass to the activity
+            scenario.onActivity(activity -> {
+                assertTrue(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testSwipeDismiss() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, true, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, true, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+        }
     }
 
     @Test
     public void testSwipeNotDismissIfDisabled() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayout(false, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND the layout is still nor hidden
-        assertNotHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            configureDismissibleLayout(scenario, false, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND the layout is still nor hidden
+            assertNotHidden(R.id.dismissible_root);
+        }
+    }
+
+    @Test
+    public void testDisableThenEnableBackDismiss() {
+        // GIVEN a freshly setup DismissibleFrameLayout
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutIntent())) {
+            final DismissibleFrameLayout[] testLayout = new DismissibleFrameLayout[1];
+            final DismissibleFrameLayoutTestActivity[] testActivity =
+                    new DismissibleFrameLayoutTestActivity[1];
+            scenario.onActivity(activity -> {
+                testActivity[0] = activity;
+                testLayout[0] =
+                        (DismissibleFrameLayout) activity.findViewById(R.id.dismissible_root);
+                testLayout[0].registerCallback(mDismissCallback);
+                // Disable back button dismiss
+                testLayout[0].setBackButtonDismissible(false);
+            });
+
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // The layout is not focused
+            assertFalse(testActivity[0].getCurrentFocus() == testLayout[0]);
+            // WHEN back button pressed
+            testActivity[0].mConsumeBackButtonUp = false;
+            sendBackKey();
+            // AND the layout is still not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // Back button up event is not consumed, and continue to pass to the activity
+            assertTrue(testActivity[0].mConsumeBackButtonUp);
+
+            // Enable backButton dismiss, we have to run this on the main thread
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setBackButtonDismissible(true);
+                }
+            });
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // The layout is focused
+            assertTrue(testActivity[0].getCurrentFocus() == testLayout[0]);
+            // WHEN back button pressed
+            testActivity[0].mConsumeBackButtonUp = false;
+            sendBackKey();
+            // AND the layout is hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed without passing up to the activity
+            assertFalse(testActivity[0].mConsumeBackButtonUp);
+        }
     }
 
 
     @Test
     public void testBackDismissWithRecyclerView() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayoutWithRecyclerView(
-                true, false, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN back button pressed
-        sendBackKey();
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+            configureDismissibleLayout(scenario, true, false, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN back button pressed
+            sendBackKey();
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+            // Back button up event is consumed, and not pass to the activity
+            scenario.onActivity(activity -> {
+                assertFalse(activity.mConsumeBackButtonUp);
+            });
+        }
     }
 
     @Test
     public void testSwipeDismissWithRecyclerView() {
         // GIVEN a freshly setup DismissibleFrameLayout
-        setUpDismissibleLayoutWithRecyclerView(
-                false, true, mDismissCallback);
-        // CHECK the layout is not hidden
-        assertNotHidden(R.id.dismissible_root);
-        // WHEN perform a swipe dismiss
-        onView(withId(R.id.dismissible_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.dismissible_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createDismissibleLayoutWithRecyclerViewIntent())) {
+            configureDismissibleLayout(scenario, false, true, mDismissCallback);
+            // CHECK the layout is not hidden
+            assertNotHidden(R.id.dismissible_root);
+            // WHEN perform a swipe dismiss
+            onView(withId(R.id.dismissible_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.dismissible_root);
+        }
     }
 
     /**
-     * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
-     * a single static child.
+     * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+     * with a single static child.
      */
-    private void setUpDismissibleLayout(
-            boolean backDismissible,
-            boolean swipeable,
-            @Nullable DismissibleFrameLayout.Callback callback) {
-        activityRule.launchActivity(
-                new Intent()
-                        .putExtra(
-                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                                androidx.wear.test.R.layout.dismissible_frame_layout_testcase));
-
-        configureDismissibleLayout(backDismissible, swipeable, callback);
+    private Intent createDismissibleLayoutIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.dismissible_frame_layout_testcase);
     }
 
-    private void setUpDismissibleLayoutWithRecyclerView(
-            boolean backDismissible,
-            boolean swipeable,
-            @Nullable DismissibleFrameLayout.Callback callback) {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.dismissible_frame_layout_recyclerview_testcase);
-        launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
-        activityRule.launchActivity(launchIntent);
-
-        configureDismissibleLayout(backDismissible, swipeable, callback);
+    /**
+     * Creates intent for launching an activity for test cases - a {@link SwipeDismissFrameLayout}
+     * with a child of scrollable container.
+     */
+    private Intent createDismissibleLayoutWithRecyclerViewIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.dismissible_frame_layout_recyclerview_testcase)
+                .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
     }
 
     private void configureDismissibleLayout(
+            ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
             boolean backDismissible,
             boolean swipeable,
             @Nullable DismissibleFrameLayout.Callback callback) {
-        Activity activity = activityRule.getActivity();
-        DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
-        testLayout.setBackButtonDismissible(backDismissible);
-        testLayout.setSwipeDismissible(swipeable);
-
-        if (callback != null) {
-            testLayout.registerCallback(callback);
-        }
+        scenario.onActivity(activity -> {
+            DismissibleFrameLayout testLayout = activity.findViewById(R.id.dismissible_root);
+            testLayout.setBackButtonDismissible(backDismissible);
+            testLayout.setSwipeDismissible(swipeable);
+            if (callback != null) {
+                testLayout.registerCallback(callback);
+            }
+        });
     }
 
     private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
index f9132b3..2b2158c 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/DismissibleFrameLayoutTestActivity.java
@@ -18,6 +18,7 @@
 
 import android.os.Bundle;
 import android.view.Gravity;
+import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
@@ -29,6 +30,7 @@
 public class DismissibleFrameLayoutTestActivity extends LayoutTestActivity {
 
     public static final String EXTRA_LAYOUT_HORIZONTAL = "layout_horizontal";
+    public boolean mConsumeBackButtonUp = false;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -54,6 +56,14 @@
         recyclerView.setAdapter(new MyRecyclerViewAdapter());
     }
 
+    public boolean onKeyUp(int keyCode, KeyEvent evnet) {
+        if (keyCode == KeyEvent.KEYCODE_BACK) {
+            mConsumeBackButtonUp = true;
+            return true;
+        }
+        return false;
+    }
+
     private static class MyRecyclerViewAdapter
             extends RecyclerView.Adapter<MyRecyclerViewAdapter.CustomViewHolder> {
         @Override
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
index e493360..472b86e 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/SwipeDismissFrameLayoutTest.java
@@ -27,13 +27,14 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
-import android.app.Activity;
 import android.content.Intent;
 import android.graphics.RectF;
 import android.view.View;
 
 import androidx.annotation.IdRes;
 import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.core.app.ApplicationProvider;
 import androidx.test.espresso.ViewAction;
 import androidx.test.espresso.action.GeneralLocation;
 import androidx.test.espresso.action.GeneralSwipeAction;
@@ -44,7 +45,6 @@
 import androidx.test.filters.FlakyTest;
 import androidx.test.filters.LargeTest;
 import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
 import androidx.wear.test.R;
 import androidx.wear.widget.util.ArcSwipe;
 import androidx.wear.widget.util.FrameLocationAvoidingEdges;
@@ -65,14 +65,6 @@
     @Rule
     public final WakeLockRule wakeLock = new WakeLockRule();
 
-    @Rule
-    public final ActivityTestRule<DismissibleFrameLayoutTestActivity> activityRule =
-            new ActivityTestRule<>(
-                    DismissibleFrameLayoutTestActivity.class,
-                    true, /** initial touch mode */
-                    false /** launchActivity */
-            );
-
     private int mLayoutWidth;
     private int mLayoutHeight;
     private int mXPositionOnScreen;
@@ -81,228 +73,265 @@
     @Test
     public void testCanScrollHorizontally() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        testLayout.setSwipeable(true);
-        // WHEN we check that the layout is horizontally scrollable from left to right.
-        // THEN the layout is found to be horizontally swipeable from left to right.
-        assertTrue(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                testLayout.setSwipeable(true);
+                // WHEN we check that the layout is horizontally scrollable from left to right.
+                // THEN the layout is found to be horizontally swipeable from left to right.
+                assertTrue(testLayout.canScrollHorizontally(-20));
+                // AND the layout is found to NOT be horizontally swipeable from right to left.
+                assertFalse(testLayout.canScrollHorizontally(20));
 
-        // WHEN we switch off the swipe-to-dismiss functionality for the layout
-        testLayout.setSwipeable(false);
-        // THEN the layout is found NOT to be horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+                // WHEN we switch off the swipe-to-dismiss functionality for the layout
+                testLayout.setSwipeable(false);
+                // THEN the layout is found NOT to be horizontally swipeable from left to right.
+                assertFalse(testLayout.canScrollHorizontally(-20));
+                // AND the layout is found to NOT be horizontally swipeable from right to left.
+                assertFalse(testLayout.canScrollHorizontally(20));
+            });
+        }
     }
 
     @Test
     public void canScrollHorizontallyShouldBeFalseWhenInvisible() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        final SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
-        // GIVEN the layout is invisible
-        // Note: We have to run this on the main thread, because of thread checks in View.java.
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                testLayout.setVisibility(View.INVISIBLE);
-            }
-        });
-        // WHEN we check that the layout is horizontally scrollable
-        // THEN the layout is found to be NOT horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+            scenario.onActivity(activity -> {
+                testLayout[0] =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            });
+            // GIVEN the layout is invisible
+            // Note: We have to run this on the main thread, because of thread checks in View.java.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setVisibility(View.INVISIBLE);
+                }
+            });
+            // WHEN we check that the layout is horizontally scrollable
+            // THEN the layout is found to be NOT horizontally swipeable from left to right.
+            assertFalse(testLayout[0].canScrollHorizontally(-20));
+            // AND the layout is found to NOT be horizontally swipeable from right to left.
+            assertFalse(testLayout[0].canScrollHorizontally(20));
+        }
     }
 
     @Test
     public void canScrollHorizontallyShouldBeFalseWhenGone() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        final SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        // GIVEN the layout is gone
-        // Note: We have to run this on the main thread, because of thread checks in View.java.
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
-            @Override
-            public void run() {
-                testLayout.setVisibility(View.GONE);
-            }
-        });
-        // WHEN we check that the layout is horizontally scrollable
-        // THEN the layout is found to be NOT horizontally swipeable from left to right.
-        assertFalse(testLayout.canScrollHorizontally(-20));
-        // AND the layout is found to NOT be horizontally swipeable from right to left.
-        assertFalse(testLayout.canScrollHorizontally(20));
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            final SwipeDismissFrameLayout[] testLayout = new SwipeDismissFrameLayout[1];
+            scenario.onActivity(activity -> {
+                testLayout[0] =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            });
+            // GIVEN the layout is gone
+            // Note: We have to run this on the main thread, because of thread checks in View.java.
+            InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+                @Override
+                public void run() {
+                    testLayout[0].setVisibility(View.GONE);
+                }
+            });
+            // WHEN we check that the layout is horizontally scrollable
+            // THEN the layout is found to be NOT horizontally swipeable from left to right.
+            assertFalse(testLayout[0].canScrollHorizontally(-20));
+            // AND the layout is found to NOT be horizontally swipeable from right to left.
+            assertFalse(testLayout[0].canScrollHorizontally(20));
+        }
     }
 
     @Test
     public void testSwipeDismissDisabledByDefault() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        // WHEN we check that the layout is dismissible
-        // THEN the layout is find to be dismissible
-        assertFalse(testLayout.isSwipeable());
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                // WHEN we check that the layout is dismissible
+                // THEN the layout is find to be dismissible
+                assertFalse(testLayout.isSwipeable());
+            });
+        }
     }
 
     @Test
     public void testSwipeDismissesViewIfEnabled() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
-                .setSwipeable(true);
-        // WHEN we perform a swipe to dismiss
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // AND hidden
-        assertHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we perform a swipe to dismiss
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // AND hidden
+            assertHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testSwipeDoesNotDismissViewIfDisabled() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout =
-                (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
-        testLayout.setSwipeable(false);
-        // WHEN we perform a swipe to dismiss
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, false);
+            // WHEN we perform a swipe to dismiss
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testAddRemoveCallback() {
         // GIVEN a freshly setup SwipeDismissFrameLayout
-        setUpSimpleLayout();
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
-        // WHEN we remove the swipe callback
-        testLayout.removeCallback(mDismissCallback);
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(createSimpleLayoutLaunchIntent())) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we remove the swipe callback
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                testLayout.removeCallback(mDismissCallback);
+            });
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRight());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     public void testSwipeDoesNotDismissViewIfScrollable() throws Throwable {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSwipeDismissWithHorizontalRecyclerView();
-        activityRule.runOnUiThread(new Runnable() {
-            @Override
-            public void run() {
-                Activity activity = activityRule.getActivity();
-                RecyclerView testLayout = activity.findViewById(R.id.recycler_container);
-                // Scroll to a position from which the child is scrollable.
-                testLayout.scrollToPosition(50);
-            }
-        });
-
-        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
-        // WHEN we perform a swipe to dismiss from the center of the screen.
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
-        // THEN the layout is not hidden
-        assertNotHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            scenario.onActivity(activity -> {
+                SwipeDismissFrameLayout testLayout =
+                        (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+                RecyclerView testRecyclerView = activity.findViewById(R.id.recycler_container);
+                testRecyclerView.scrollToPosition(50);
+            });
+            InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+            // WHEN we perform a swipe to dismiss from the center of the screen.
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromCenter());
+            // THEN the layout is not hidden
+            assertNotHidden(R.id.swipe_dismiss_root);
+        }
     }
 
 
     @Test
     public void testEdgeSwipeDoesDismissViewIfScrollable() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with dismiss turned off.
-        setUpSwipeDismissWithHorizontalRecyclerView();
-        Activity activity = activityRule.getActivity();
-        ((SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root))
-                .setSwipeable(true);
-        // WHEN we perform a swipe to dismiss from the left edge of the screen.
-        onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
-        // THEN the layout is hidden
-        assertHidden(R.id.swipe_dismiss_root);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithHorizontalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            // WHEN we perform a swipe to dismiss from the left edge of the screen.
+            onView(withId(R.id.swipe_dismiss_root)).perform(swipeRightFromLeftCenterAvoidingEdge());
+            // THEN the layout is hidden
+            assertHidden(R.id.swipe_dismiss_root);
+        }
     }
 
     @Test
     @FlakyTest
     public void testArcSwipeDoesNotTriggerDismiss() {
         // GIVEN a freshly setup SwipeDismissFrameLayout with vertically scrollable content
-        setUpSwipeDismissWithVerticalRecyclerView();
-        int center = mLayoutHeight / 2;
-        int halfBound = mLayoutWidth / 2;
-        RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
-        // WHEN the view is scrolled on an arc from top to bottom.
-        onView(withId(R.id.swipe_dismiss_root)).perform(
-                swipeTopFromBottomOnArcAvoidingEdge(bounds));
-        // THEN the layout is not dismissed and not hidden.
-        assertNotHidden(R.id.swipe_dismiss_root);
-        // AND the content view is scrolled.
-        assertScrolledY(R.id.recycler_container);
+        try (ActivityScenario<DismissibleFrameLayoutTestActivity> scenario =
+                     ActivityScenario.launch(
+                             createSwipeDismissWithVerticalRecyclerViewLaunchIntent()
+                     )) {
+            setUpSwipeableAndCallback(scenario, true);
+            int center = mLayoutHeight / 2;
+            int halfBound = mLayoutWidth / 2;
+            RectF bounds = new RectF(0, center - halfBound, mLayoutWidth, center + halfBound);
+            // WHEN the view is scrolled on an arc from top to bottom.
+            onView(withId(R.id.swipe_dismiss_root)).perform(
+                    swipeTopFromBottomOnArcAvoidingEdge(bounds));
+            // THEN the layout is not dismissed and not hidden.
+            assertNotHidden(R.id.swipe_dismiss_root);
+            // AND the content view is scrolled.
+            assertScrolledY(R.id.recycler_container);
+        }
     }
 
     /**
-     * Set ups the simplest possible layout for test cases - a {@link SwipeDismissFrameLayout} with
-     * a single static child.
+     * Creates intent for launching the simplest possible layout for test cases - a
+     * {@link SwipeDismissFrameLayout} with a single static child.
      */
-    private void setUpSimpleLayout() {
-        activityRule.launchActivity(
-                new Intent()
-                        .putExtra(
-                                LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                                R.layout.swipe_dismiss_layout_testcase_1));
-        setDismissCallback();
+    private Intent createSimpleLayoutLaunchIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_1);
     }
 
 
     /**
-     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
-     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a horizontal {@link
-     * RecyclerView} as a child, ready to accept an adapter.
+     * Creates intent for launching a slightly more involved layout for testing swipe-to-dismiss
+     * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+     * horizontal {@link RecyclerView} as a child, ready to accept an adapter.
      */
-    private void setUpSwipeDismissWithHorizontalRecyclerView() {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.swipe_dismiss_layout_testcase_2);
-        launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
-        activityRule.launchActivity(launchIntent);
-        setDismissCallback();
+    private Intent createSwipeDismissWithHorizontalRecyclerViewLaunchIntent() {
+        return new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_2)
+                .putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, true);
     }
 
     /**
-     * Sets up a slightly more involved layout for testing swipe-to-dismiss with scrollable
-     * containers. This layout contains a {@link SwipeDismissFrameLayout} with a vertical {@link
-     * WearableRecyclerView} as a child, ready to accept an adapter.
+     * Creates intent for launching slightly more involved layout for testing swipe-to-dismiss
+     * with scrollable containers. This layout contains a {@link SwipeDismissFrameLayout} with a
+     * vertical {@link WearableRecyclerView} as a child, ready to accept an adapter.
      */
-    private void setUpSwipeDismissWithVerticalRecyclerView() {
-        Intent launchIntent = new Intent();
-        launchIntent.putExtra(LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
-                R.layout.swipe_dismiss_layout_testcase_2);
+    private Intent createSwipeDismissWithVerticalRecyclerViewLaunchIntent() {
+        Intent launchIntent = new Intent()
+                .setClass(ApplicationProvider.getApplicationContext(),
+                        DismissibleFrameLayoutTestActivity.class)
+                .putExtra(
+                        LayoutTestActivity.EXTRA_LAYOUT_RESOURCE_ID,
+                        R.layout.swipe_dismiss_layout_testcase_2);
         launchIntent.putExtra(DismissibleFrameLayoutTestActivity.EXTRA_LAYOUT_HORIZONTAL, false);
-        activityRule.launchActivity(launchIntent);
-        setDismissCallback();
+
+        return launchIntent;
     }
 
-    private void setDismissCallback() {
-        setCallback(mDismissCallback);
-    }
-
-    private void setCallback(SwipeDismissFrameLayout.Callback callback) {
-        Activity activity = activityRule.getActivity();
-        SwipeDismissFrameLayout testLayout = activity.findViewById(R.id.swipe_dismiss_root);
+    private void setDismissCallback(SwipeDismissFrameLayout testLayout) {
         int[] locationOnScreen = new int[2];
         testLayout.getLocationOnScreen(locationOnScreen);
         mXPositionOnScreen = locationOnScreen[0];
         mYPositionOnScreen = locationOnScreen[1];
         mLayoutWidth = testLayout.getWidth();
         mLayoutHeight = testLayout.getHeight();
-        testLayout.addCallback(callback);
+        testLayout.addCallback(mDismissCallback);
+    }
+
+    private void setUpSwipeableAndCallback(
+            ActivityScenario<DismissibleFrameLayoutTestActivity> scenario,
+            boolean swipeable
+    ) {
+        scenario.onActivity(activity -> {
+            SwipeDismissFrameLayout testLayout =
+                    (SwipeDismissFrameLayout) activity.findViewById(R.id.swipe_dismiss_root);
+            setDismissCallback(testLayout);
+            testLayout.setSwipeable(swipeable);
+        });
     }
 
     private static void assertHidden(@IdRes int layoutId) {
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
index 465ec78..57f0901 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearArcLayoutTest.kt
@@ -31,6 +31,7 @@
 import android.view.ViewGroup
 import android.widget.FrameLayout
 import android.widget.TextView
+import androidx.core.view.children
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.espresso.Espresso
@@ -49,6 +50,9 @@
 import androidx.test.screenshot.assertAgainstGolden
 import androidx.wear.test.R
 import androidx.wear.widget.util.AsyncViewActions.waitForMatchingView
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_CENTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_OUTER
+import androidx.wear.widget.WearArcLayout.LayoutParams.VALIGN_INNER
 import org.hamcrest.CoreMatchers.allOf
 import org.hamcrest.CoreMatchers.any
 import org.hamcrest.Matcher
@@ -223,6 +227,100 @@
         )
     }
 
+    // Extension functions to make the margin test more readable.
+    fun WearArcLayout.addSeparator() {
+        addView(
+            WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+                text = " "
+                minSweepDegrees = 10f
+                setBackgroundColor(Color.rgb(100, 100, 100))
+                clockwise = true
+                textSize = 40f
+            }
+        )
+    }
+
+    fun WearArcLayout.addText(
+        text0: String,
+        color: Int,
+        marginLeft: Int = 0,
+        marginTop: Int = 0,
+        marginRight: Int = 0,
+        marginBottom: Int = 0,
+        vAlign: Int = VALIGN_CENTER
+    ) {
+        addView(
+            WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
+                text = text0
+                setBackgroundColor(color)
+                clockwise = true
+                textSize = 14f
+                layoutParams = WearArcLayout.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.MATCH_PARENT
+                ).apply {
+                    setMargins(marginLeft, marginTop, marginRight, marginBottom)
+                    verticalAlignment = vAlign
+                }
+            }
+        )
+    }
+
+    private fun createArcWithMargin() =
+        WearArcLayout(ApplicationProvider.getApplicationContext()).apply {
+            anchorType = WearArcLayout.ANCHOR_CENTER
+            addSeparator()
+            addText("RI", Color.RED, marginTop = 16, vAlign = VALIGN_INNER)
+            addText("GI", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_INNER)
+            addText("BI", Color.BLUE, marginBottom = 16, vAlign = VALIGN_INNER)
+            addSeparator()
+            addText("Red", Color.RED, marginTop = 16)
+            addText("Green", Color.GREEN, marginTop = 8, marginBottom = 8)
+            addText("Blue", Color.BLUE, marginBottom = 16)
+            addSeparator()
+            addText("RO", Color.RED, marginTop = 16, vAlign = VALIGN_OUTER)
+            addText("GO", Color.GREEN, marginTop = 8, marginBottom = 8, vAlign = VALIGN_OUTER)
+            addText("BO", Color.BLUE, marginBottom = 16, vAlign = VALIGN_OUTER)
+            addSeparator()
+            addText("L", Color.WHITE, marginRight = 20)
+            addSeparator()
+            addText("C", Color.WHITE, marginRight = 10, marginLeft = 10)
+            addSeparator()
+            addText("R", Color.WHITE, marginLeft = 20)
+            addSeparator()
+        }
+
+    private fun createTwoArcsWithMargin() = listOf(
+        // First arc goes on top
+        createArcWithMargin(),
+
+        // Second arc in the bottom, and we change al children to go counterclockwise.
+        createArcWithMargin().apply {
+            anchorAngleDegrees = 180f
+            children.forEach {
+                (it as? WearCurvedTextView) ?.clockwise = false
+            }
+        }
+    )
+
+    @Test
+    fun testMargins() {
+        doOneTest(
+            "margin_test",
+            createTwoArcsWithMargin()
+        )
+    }
+
+    @Test
+    fun testMarginsCcw() {
+        doOneTest(
+            "margin_ccw_test",
+            createTwoArcsWithMargin().map {
+                it.apply { clockwise = false }
+            }
+        )
+    }
+
     // Generates a click in the x,y coordinates in the view's coordinate system.
     fun customClick(x: Float, y: Float) = ViewActions.actionWithAssertions(
         GeneralClickAction(
diff --git a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
index 20ff4f1..c09c06a 100644
--- a/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
+++ b/wear/wear/src/androidTest/java/androidx/wear/widget/WearCurvedTextViewTest.kt
@@ -227,12 +227,14 @@
                     text = "This is a clockwise text for testing background"
                     clockwise = true
                     anchorAngleDegrees = 170.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 },
                 WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
                     text = "Another clockwise text"
                     clockwise = true
                     anchorAngleDegrees = 70.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 }
             )
@@ -249,12 +251,14 @@
                     text = "This is a counterclockwise text for testing background"
                     clockwise = false
                     anchorAngleDegrees = 100.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 },
                 WearCurvedTextView(ApplicationProvider.getApplicationContext()).apply {
                     text = "Another counterclockwise text"
                     clockwise = false
                     anchorAngleDegrees = 230.0f
+                    anchorType = WearArcLayout.ANCHOR_START
                     setBackgroundColor(Color.rgb(0, 100, 100))
                 }
             )
diff --git a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
index 2a78640..7648800 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/BackButtonDismissController.java
@@ -25,6 +25,8 @@
 import androidx.annotation.UiThread;
 import androidx.wear.utils.ActivityAnimationUtil;
 
+import org.jetbrains.annotations.NotNull;
+
 /**
  * Controller that handles the back button click for dismiss the frame layout
  *
@@ -37,22 +39,26 @@
     BackButtonDismissController(Context context, DismissibleFrameLayout layout) {
         super(context, layout);
 
-        // Dismiss upon back button press
+        // set this to true will also ensure that this view is focusable
         layout.setFocusableInTouchMode(true);
+        // Dismiss upon back button press
         layout.requestFocus();
         layout.setOnKeyListener(
-                (view, keyCode, event) -> {
-                    if (keyCode == KeyEvent.KEYCODE_BACK
-                            && event.getAction() == KeyEvent.ACTION_UP) {
-                        dismiss();
-                        return true;
-                    }
-                    return false;
-                });
+                (view, keyCode, event) -> keyCode == KeyEvent.KEYCODE_BACK
+                        && event.getAction() == KeyEvent.ACTION_UP
+                        && dismiss());
     }
 
-    private void dismiss() {
-        if (mDismissListener == null) return;
+    void disable(@NotNull DismissibleFrameLayout layout) {
+        setOnDismissListener(null);
+        layout.setOnKeyListener(null);
+        // setting this to false will also ensure that this view is not focusable in touch mode
+        layout.setFocusable(false);
+        layout.clearFocus();
+    }
+
+    private boolean dismiss() {
+        if (mDismissListener == null) return false;
 
         Animation exitAnimation = ActivityAnimationUtil.getStandardActivityAnimation(
                 mContext, ActivityAnimationUtil.CLOSE_EXIT,
@@ -79,5 +85,6 @@
             mDismissListener.onDismissStarted();
             mDismissListener.onDismissed();
         }
+        return true;
     }
 }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
index ce76639..448df50 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/DismissibleFrameLayout.java
@@ -192,7 +192,7 @@
                 mBackButtonDismissController.setOnDismissListener(mDismissListener);
             }
         } else if (mBackButtonDismissController != null) {
-            mBackButtonDismissController.setOnDismissListener(null);
+            mBackButtonDismissController.disable(this);
             mBackButtonDismissController = null;
         }
     }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
index 4535a4c..3a5cc6c 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearArcLayout.java
@@ -92,17 +92,8 @@
 
         /**
          * Check whether the widget contains invalid attributes as a child of WearArcLayout
-         *
-         * @param clockwise the layout direction of the container
          */
-        void checkInvalidAttributeAsChild(boolean clockwise);
-
-        /**
-         * Return whether the widget will handle the layout rotation requested by the container
-         * If return true, make sure that the layout rotation is done inside the widget since the
-         * container will skip this process.
-         */
-        boolean handleLayoutRotate(float angle);
+        void checkInvalidAttributeAsChild();
 
         /**
          * Return true when the given point is in the clickable area of the child widget.
@@ -120,7 +111,7 @@
      *
      * <p>Note that the {@code rotate} parameter is ignored when drawing "Fullscreen" elements.
      */
-    public static class LayoutParams extends ViewGroup.LayoutParams {
+    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
 
         /** Vertical alignment of elements within the arc. */
         /** @hide */
@@ -265,6 +256,8 @@
     // Temporary variables using during a draw cycle.
     private float mCurrentCumulativeAngle = 0;
     private int mAnglesIndex = 0;
+    @SuppressWarnings("SyntheticAccessor")
+    private final ChildArcAngles mChildArcAngles = new ChildArcAngles();
 
     public WearArcLayout(@NonNull Context context) {
         this(context, null);
@@ -358,18 +351,22 @@
 
             // ArcLayoutWidget is a special case. Because of how it draws, fit it to the size
             // of the whole widget.
+            int childMeasuredHeight;
             if (child instanceof ArcLayoutWidget) {
-                ArcLayoutWidget widget = (ArcLayoutWidget) child;
-                maxChildHeightPx = max(maxChildHeightPx, widget.getThicknessPx());
+                childMeasuredHeight = ((ArcLayoutWidget) child).getThicknessPx();
             } else {
                 measureChild(
                         child,
                         getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().width),
                         getChildMeasureSpec(childMeasureSpec, 0, child.getLayoutParams().height)
                 );
-                maxChildHeightPx = max(maxChildHeightPx, child.getMeasuredHeight());
+                childMeasuredHeight = child.getMeasuredHeight();
                 childState = combineMeasuredStates(childState, child.getMeasuredState());
+
             }
+            LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
+            maxChildHeightPx = max(maxChildHeightPx, childMeasuredHeight
+                    + childLayoutParams.topMargin +  childLayoutParams.bottomMargin);
         }
 
         mThicknessPx = maxChildHeightPx;
@@ -383,23 +380,14 @@
             }
 
             if (child instanceof ArcLayoutWidget) {
-                ArcLayoutWidget curvedContainerChild = (ArcLayoutWidget) child;
                 LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
 
-                int childThicknessPx = curvedContainerChild.getThicknessPx();
-                int thicknessDiffPx = mThicknessPx - childThicknessPx;
-
-                float insetPx = 0;
-
-                if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_CENTER) {
-                    insetPx = thicknessDiffPx / 2f;
-                } else if (childLayoutParams.getVerticalAlignment() == LayoutParams.VALIGN_INNER) {
-                    insetPx = thicknessDiffPx;
-                }
+                float insetPx = getChildTopInset(child);
 
                 int innerChildMeasureSpec =
                         MeasureSpec.makeMeasureSpec(
                                 maxChildDimension * 2 - round(insetPx * 2), MeasureSpec.EXACTLY);
+
                 measureChild(
                         child,
                         getChildMeasureSpec(innerChildMeasureSpec, 0, childLayoutParams.width),
@@ -444,7 +432,7 @@
                 // vertical position.
                 int leftPx =
                         round((getMeasuredWidth() / 2f) - (child.getMeasuredWidth() / 2f));
-                int topPx = getChildTopInset(child);
+                int topPx = round(getChildTopInset(child));
 
                 child.layout(
                         leftPx,
@@ -551,60 +539,49 @@
         // Rotate the canvas to make the children render in the right place.
         canvas.save();
 
-        float arcAngle = calculateArcAngle(child);
-        float preRotation = arcAngle / 2f;
+        calculateArcAngle(child, mChildArcAngles);
+        float preRotation = mChildArcAngles.leftMarginAsAngle
+                + mChildArcAngles.actualChildAngle / 2f;
         float multiplier = mClockwise ? 1f : -1f;
 
         // Store the center angle of each child to handle touch events.
-        float middleAngle = multiplier * (mCurrentCumulativeAngle + arcAngle / 2);
+        float middleAngle = multiplier * (mCurrentCumulativeAngle + preRotation);
         mAngles[mAnglesIndex++] = middleAngle;
         if (child == mTouchedView) {
             // We keep this updated, in case the view has changed angle.
             mTouchedViewAngle = middleAngle;
         }
 
+        // Rotate the child widget.
+        canvas.rotate(
+                multiplier * (mCurrentCumulativeAngle + preRotation),
+                getMeasuredWidth() / 2f,
+                getMeasuredHeight() / 2f);
+
         if (child instanceof ArcLayoutWidget) {
-            ArcLayoutWidget childWidget = (ArcLayoutWidget) child;
-            childWidget.checkInvalidAttributeAsChild(mClockwise);
-
-            // Special case for ArcLayoutWidget. This doesn't need pre-rotating to get the center
-            // of canvas lines up, as it should already know how to draw itself correctly from
-            // the "current" rotation. The layout rotation is always passed to the child widget,
-            // if the child has not handled this rotation by itself, the parent will have to
-            // rotate the canvas to apply this layout.
-            if (!childWidget.handleLayoutRotate(multiplier * mCurrentCumulativeAngle)) {
-                canvas.rotate(
-                        multiplier * mCurrentCumulativeAngle,
-                        getMeasuredWidth() / 2f,
-                        getMeasuredHeight() / 2f
-                );
-            }
+            ((ArcLayoutWidget) child).checkInvalidAttributeAsChild();
         } else {
-            canvas.rotate(
-                    multiplier * (mCurrentCumulativeAngle + preRotation),
-                    getMeasuredWidth() / 2f,
-                    getMeasuredHeight() / 2f);
-
             // Do we need to do some counter rotation?
             LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
 
-            // For counterclockwise layout, especially when mixing standard Android widget with
-            // ArcLayoutWidget as children, we might need to rotate the standard widget to make
-            // them with the same upwards direction.
             float angleToRotate = 0f;
-            if (layoutParams.getRotate() && !mClockwise) {
-                angleToRotate = 180f;
-            }
 
-            // Un-rotate about the top of the canvas, around the center of the actual child.
-            // This compounds with the initial rotation into a translation.
-            if (!layoutParams.getRotate()) {
+            if (layoutParams.getRotate()) {
+                // For counterclockwise layout, especially when mixing standard Android widget with
+                // ArcLayoutWidget as children, we might need to rotate the standard widget to make
+                // them have the same upwards direction.
+                if (!mClockwise) {
+                    angleToRotate = 180f;
+                }
+            } else {
+                // Un-rotate about the top of the canvas, around the center of the actual child.
+                // This compounds with the initial rotation into a translation.
                 angleToRotate = -multiplier * (mCurrentCumulativeAngle + preRotation);
             }
 
             // Do the actual rotation. Note that the strange rotation center is because the child
             // view is x-centered but at the top of this container.
-            int childInset = getChildTopInset(child);
+            float childInset = getChildTopInset(child);
             canvas.rotate(
                     angleToRotate,
                     getMeasuredWidth() / 2f,
@@ -612,7 +589,7 @@
             );
         }
 
-        mCurrentCumulativeAngle += arcAngle;
+        mCurrentCumulativeAngle += mChildArcAngles.getTotalAngle();
 
         boolean wasInvalidateIssued = super.drawChild(canvas, child, drawingTime);
 
@@ -630,7 +607,8 @@
         float totalArcAngle = 0;
 
         for (int i = 0; i < getChildCount(); i++) {
-            totalArcAngle += calculateArcAngle(getChildAt(i));
+            calculateArcAngle(getChildAt(i), mChildArcAngles);
+            totalArcAngle += mChildArcAngles.getTotalAngle();
         }
 
         if (mAnchorType == ANCHOR_CENTER) {
@@ -642,31 +620,55 @@
         return 0;
     }
 
-    private float calculateArcAngle(@NonNull View view) {
+    private static float widthToAngleDegrees(float widthPx, float radiusPx) {
+        return (float) Math.toDegrees(2 * asin(widthPx / radiusPx / 2f));
+    }
+
+    private void calculateArcAngle(@NonNull View view, @NonNull ChildArcAngles childAngles) {
         if (view.getVisibility() == GONE) {
-            return 0f;
+            childAngles.leftMarginAsAngle = 0;
+            childAngles.rightMarginAsAngle = 0;
+            childAngles.actualChildAngle = 0;
+            return;
         }
 
+        float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
+
+        LayoutParams childLayoutParams = (LayoutParams) view.getLayoutParams();
+
+        childAngles.leftMarginAsAngle =
+                widthToAngleDegrees(childLayoutParams.leftMargin, radiusPx);
+        childAngles.rightMarginAsAngle =
+                widthToAngleDegrees(childLayoutParams.rightMargin, radiusPx);
+
         if (view instanceof ArcLayoutWidget) {
-            return ((ArcLayoutWidget) view).getSweepAngleDegrees();
+            childAngles.actualChildAngle = ((ArcLayoutWidget) view).getSweepAngleDegrees();
         } else {
-            float radiusPx = (getMeasuredWidth() / 2f) - mThicknessPx;
-            return (float) Math.toDegrees(2 * asin(view.getMeasuredWidth() / radiusPx / 2f));
+            childAngles.actualChildAngle =
+                    widthToAngleDegrees(view.getMeasuredWidth(), radiusPx);
         }
     }
 
-    private int getChildTopInset(@NonNull View child) {
+    private float getChildTopInset(@NonNull View child) {
         LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
 
-        int thicknessDiffPx = mThicknessPx - child.getMeasuredHeight();
+        int childHeight = child instanceof ArcLayoutWidget
+                ? ((ArcLayoutWidget) child).getThicknessPx()
+                : child.getMeasuredHeight();
+
+        int thicknessDiffPx =
+                mThicknessPx - childLayoutParams.topMargin - childLayoutParams.bottomMargin
+                        - childHeight;
+
+        int margin = mClockwise ? childLayoutParams.topMargin : childLayoutParams.bottomMargin;
 
         switch (childLayoutParams.getVerticalAlignment()) {
             case LayoutParams.VALIGN_OUTER:
-                return 0;
+                return margin;
             case LayoutParams.VALIGN_CENTER:
-                return round(thicknessDiffPx / 2f);
+                return margin + thicknessDiffPx / 2f;
             case LayoutParams.VALIGN_INNER:
-                return thicknessDiffPx;
+                return margin + thicknessDiffPx;
             default:
                 // Nortmally unreachable...
                 return 0;
@@ -734,4 +736,14 @@
         mClockwise = clockwise;
         invalidate();
     }
+
+    private static class ChildArcAngles {
+        public float leftMarginAsAngle;
+        public float rightMarginAsAngle;
+        public float actualChildAngle;
+
+        public float getTotalAngle() {
+            return leftMarginAsAngle + rightMarginAsAngle + actualChildAngle;
+        }
+    }
 }
diff --git a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
index 5a6a7fb..b2e659e 100644
--- a/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
+++ b/wear/wear/src/main/java/androidx/wear/widget/WearCurvedTextView.java
@@ -77,12 +77,8 @@
     private float mPathRadius = 0f;
     private float mTextSweepDegrees = 0f;
     private float mBackgroundSweepDegrees = MAX_SWEEP_DEGREE;
-    private boolean mHasParentArcLayout = false;
-    private boolean mParentClockwise = true;
     private int mLastUsedTextAlignment = -1;
     private float mLocalRotateAngle = 0f;
-    private float mParentRotateAngle = 0f;
-    private boolean mParentRotateAngleSet = false;
 
     private int mAnchorType = UNSET_ANCHOR_TYPE;
     private float mAnchorAngleDegrees = UNSET_ANCHOR_DEGREE;
@@ -215,10 +211,7 @@
      *                                  were set for a widget in WearArcLayout
      */
     @Override
-    public void checkInvalidAttributeAsChild(boolean parentClockwise) {
-        this.mHasParentArcLayout = true;
-        this.mParentClockwise = parentClockwise;
-
+    public void checkInvalidAttributeAsChild() {
         if (mAnchorType != UNSET_ANCHOR_TYPE) {
             throw new IllegalArgumentException(
                     "WearCurvedTextView shall not set anchorType value when added into"
@@ -235,18 +228,6 @@
     }
 
     @Override
-    public boolean handleLayoutRotate(float angle) {
-        mParentRotateAngleSet = true;
-
-        // Ensure we are redrawn when the parent rotates.
-        if (mParentRotateAngle != angle) {
-            doRedraw();
-        }
-        mParentRotateAngle = angle;
-        return true;
-    }
-
-    @Override
     public boolean insideClickArea(float x, float y) {
         float radius2 = min(getWidth(), getHeight()) / 2f
                 - (mClockwise ? getPaddingTop() : getPaddingBottom());
@@ -340,8 +321,6 @@
         }
 
         float clockwiseFactor = mClockwise ? 1f : -1f;
-        float parentClockwiseFactor =
-                mHasParentArcLayout ? (mParentClockwise ? 1f : -1f) : clockwiseFactor;
 
         float alignmentFactor = 0.5f;
         switch (getTextAlignment()) {
@@ -357,31 +336,25 @@
                 alignmentFactor = 0.5f; // TEXT_ALIGNMENT_CENTER
         }
 
-        float anchorTypeFactor = 0f;
+        float anchorTypeFactor;
         switch (mAnchorType) {
             case WearArcLayout.ANCHOR_START:
-                anchorTypeFactor = 0f;
-                break;
-            case WearArcLayout.ANCHOR_CENTER:
                 anchorTypeFactor = 0.5f;
                 break;
             case WearArcLayout.ANCHOR_END:
-                anchorTypeFactor = 1f;
+                anchorTypeFactor = -0.5f;
                 break;
+            case WearArcLayout.ANCHOR_CENTER: // Center is the default.
             default:
-                anchorTypeFactor = parentClockwiseFactor == clockwiseFactor ? 0f : -1f;
+                anchorTypeFactor = 0f;
         }
 
-        float actualAnchorDegree =
-                (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
-                        + ANCHOR_DEGREE_OFFSET;
+        mLocalRotateAngle = (mAnchorAngleDegrees == UNSET_ANCHOR_DEGREE ? 0f : mAnchorAngleDegrees)
+                + clockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
 
         // Always draw the curved text on top center, then rotate the canvas to the right position
         float backgroundStartAngle =
                 -clockwiseFactor * 0.5f * mBackgroundSweepDegrees + ANCHOR_DEGREE_OFFSET;
-        mLocalRotateAngle =
-                actualAnchorDegree - backgroundStartAngle
-                        - parentClockwiseFactor * anchorTypeFactor * mBackgroundSweepDegrees;
 
         float textStartAngle =
                 backgroundStartAngle + clockwiseFactor * (float) (
@@ -462,20 +435,15 @@
             return false;
         }
 
-        float x0 = event.getX();
-        float y0 = event.getY();
-        if (!mParentRotateAngleSet) {
-            // If we are a stand-alone widget, we have to handle our rotation / anchor placement,
-            // if we are part of an arc container, it's handled by it.
-            double rotAngle = -Math.toRadians(mLocalRotateAngle);
+        float x0 = event.getX() - getWidth() / 2;
+        float y0 = event.getY() - getHeight() / 2;
 
-            x0 -= getWidth() / 2;
-            y0 -= getHeight() / 2;
-            float tempX = (float)
-                    ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
-            y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
-            x0 = tempX;
-        }
+        double rotAngle = -Math.toRadians(mLocalRotateAngle);
+
+        float tempX = (float)
+                ((x0 * cos(rotAngle) - y0 * sin(rotAngle)) + getWidth() / 2);
+        y0 = (float) ((x0 * sin(rotAngle) + y0 * cos(rotAngle)) + getHeight() / 2);
+        x0 = tempX;
 
         // Should we start handling the touch events?
         if (!mHandlingTouch && insideClickArea(x0, y0)) {
@@ -503,7 +471,7 @@
         boolean withBackground = getBackground() != null;
         updatePathsIfNeeded(withBackground);
         canvas.rotate(
-                mLocalRotateAngle + mParentRotateAngle,
+                mLocalRotateAngle,
                 getWidth() / 2f,
                 getHeight() / 2f);