Merge "Add explicit check for Step.lanesImage type validity" into androidx-main
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index f2553e3..8b81217 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -33,11 +33,17 @@
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api(projectOrArtifact(":activity:activity-ktx"))
     api("androidx.compose.ui:ui:1.0.1")
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation(project(":lifecycle:lifecycle-common-java8"))
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(":compose:material:material")
     androidTestImplementation projectOrArtifact(":compose:test-utils")
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-runtime-testing")
     androidTestImplementation(libs.testRunner)
     androidTestImplementation(libs.testExtJunitKtx)
     androidTestImplementation(libs.junit)
diff --git a/activity/activity-compose/samples/build.gradle b/activity/activity-compose/samples/build.gradle
index 74db5f1..0e3628e 100644
--- a/activity/activity-compose/samples/build.gradle
+++ b/activity/activity-compose/samples/build.gradle
@@ -33,6 +33,12 @@
     implementation projectOrArtifact(":activity:activity-compose")
     implementation projectOrArtifact(":activity:activity-ktx")
     implementation "androidx.compose.material:material:1.0.1"
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
 }
 
 androidx {
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index cc7ef29..8b86605 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -29,16 +29,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.1") {
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
     api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
     api(libs.kotlinStdlib)
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(libs.junit)
     androidTestImplementation(libs.truth)
     androidTestImplementation(libs.testExtJunit)
@@ -60,12 +60,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
 }
\ No newline at end of file
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index a2fa9b7..a5c92a8 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -18,14 +18,14 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api(projectOrArtifact(":core:core"))
-    api("androidx.lifecycle:lifecycle-runtime:2.3.1")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel"))
     api(projectOrArtifact(":savedstate:savedstate"))
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation("androidx.tracing:tracing:1.0.0")
     api(libs.kotlinStdlib)
 
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(libs.kotlinStdlib)
     androidTestImplementation(libs.espressoCore, excludes.espresso)
     androidTestImplementation(libs.leakcanary)
diff --git a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
index 814c66f..4d722e5 100644
--- a/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
+++ b/activity/activity/src/androidTest/java/androidx/activity/ComponentActivityCallbacksTest.kt
@@ -196,21 +196,17 @@
             }
             withActivity {
                 addOnNewIntentListener(listener)
-                startActivity(
-                    Intent(this, SingleTopActivity::class.java).apply {
-                        putExtra("newExtra", 5)
-                    }
-                )
+                onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+                    putExtra("newExtra", 5)
+                })
             }
 
-            withActivity {
-                assertWithMessage("Should have received one intent")
-                    .that(receivedIntents)
-                    .hasSize(1)
-                val receivedIntent = receivedIntents.first()
-                assertThat(receivedIntent.getIntExtra("newExtra", -1))
-                    .isEqualTo(5)
-            }
+            assertWithMessage("Should have received one intent")
+                .that(receivedIntents)
+                .hasSize(1)
+            val receivedIntent = receivedIntents.first()
+            assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                .isEqualTo(5)
         }
     }
 
@@ -224,36 +220,28 @@
             }
             withActivity {
                 addOnNewIntentListener(listener)
-                startActivity(
-                    Intent(this, SingleTopActivity::class.java).apply {
-                        putExtra("newExtra", 5)
-                    }
-                )
+                onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+                    putExtra("newExtra", 5)
+                })
             }
 
-            withActivity {
-                assertWithMessage("Should have received one intent")
-                    .that(receivedIntents)
-                    .hasSize(1)
-                val receivedIntent = receivedIntents.first()
-                assertThat(receivedIntent.getIntExtra("newExtra", -1))
-                    .isEqualTo(5)
-            }
+            assertWithMessage("Should have received one intent")
+                .that(receivedIntents)
+                .hasSize(1)
+            val receivedIntent = receivedIntents.first()
+            assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                .isEqualTo(5)
 
             withActivity {
                 removeOnNewIntentListener(listener)
-                startActivity(
-                    Intent(this, SingleTopActivity::class.java).apply {
-                        putExtra("newExtra", 10)
-                    }
-                )
+                onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+                    putExtra("newExtra", 5)
+                })
             }
 
-            withActivity {
-                assertWithMessage("Should have received only one intent")
-                    .that(receivedIntents)
-                    .hasSize(1)
-            }
+            assertWithMessage("Should have received only one intent")
+                .that(receivedIntents)
+                .hasSize(1)
         }
     }
 
@@ -274,32 +262,27 @@
                 // Add a second listener to force a ConcurrentModificationException
                 // if not properly handled by ComponentActivity
                 addOnNewIntentListener { }
-                startActivity(
-                    Intent(this, SingleTopActivity::class.java).apply {
-                        putExtra("newExtra", 5)
-                    }
-                )
+                onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+                    putExtra("newExtra", 5)
+                })
+                onNewIntent(Intent(this, SingleTopActivity::class.java).apply {
+                    putExtra("newExtra", 10)
+                })
             }
 
-            withActivity {
-                startActivity(
-                    Intent(this, SingleTopActivity::class.java).apply {
-                        putExtra("newExtra", 10)
-                    }
-                )
-            }
-
-            withActivity {
-                // Only the first Intent should be received
-                assertWithMessage("Should have received only one intent")
-                    .that(receivedIntents)
-                    .hasSize(1)
-                val receivedIntent = receivedIntents.first()
-                assertThat(receivedIntent.getIntExtra("newExtra", -1))
-                    .isEqualTo(5)
-            }
+            // Only the first Intent should be received
+            assertWithMessage("Should have received only one intent")
+                .that(receivedIntents)
+                .hasSize(1)
+            val receivedIntent = receivedIntents.first()
+            assertThat(receivedIntent.getIntExtra("newExtra", -1))
+                .isEqualTo(5)
         }
     }
 }
 
-class SingleTopActivity : ComponentActivity()
+class SingleTopActivity : ComponentActivity() {
+    public override fun onNewIntent(intent: Intent?) {
+        super.onNewIntent(intent)
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
index b5490be..dc7499a 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/AppSearchImplTest.java
@@ -36,14 +36,13 @@
 import androidx.appsearch.app.SetSchemaResponse;
 import androidx.appsearch.app.StorageInfo;
 import androidx.appsearch.exceptions.AppSearchException;
-import androidx.appsearch.localstorage.converter.GenericDocumentToProtoConverter;
 import androidx.appsearch.localstorage.stats.InitializeStats;
 import androidx.appsearch.localstorage.stats.OptimizeStats;
 import androidx.appsearch.localstorage.util.PrefixUtil;
 import androidx.collection.ArrayMap;
 import androidx.collection.ArraySet;
-import androidx.test.filters.FlakyTest;
 import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.FlakyTest;
 
 import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.GetOptimizeInfoResultProto;
@@ -53,14 +52,11 @@
 import com.google.android.icing.proto.PutResultProto;
 import com.google.android.icing.proto.SchemaProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
-import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.StatusProto;
 import com.google.android.icing.proto.StorageInfoProto;
 import com.google.android.icing.proto.StringIndexingConfig;
 import com.google.android.icing.proto.TermMatchType;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 
 import org.junit.After;
@@ -534,123 +530,6 @@
     }
 
     @Test
-    public void testRewriteSearchSpec_oneInstance() throws Exception {
-        SearchSpecProto.Builder searchSpecProto =
-                SearchSpecProto.newBuilder().setQuery("");
-
-        // Insert schema
-        List<AppSearchSchema> schemas =
-                Collections.singletonList(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema(
-                "package",
-                "database",
-                schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
-                /* setSchemaStatsBuilder= */ null);
-
-        // Insert document
-        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
-                "type").build();
-        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
-
-        // Rewrite SearchSpec
-        mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                Collections.singleton(createPrefix("package", "database")),
-                ImmutableSet.of("package$database/type"));
-        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
-                "package$database/type");
-        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
-                "package$database/namespace");
-    }
-
-    @Test
-    public void testRewriteSearchSpec_twoInstances() throws Exception {
-        SearchSpecProto.Builder searchSpecProto =
-                SearchSpecProto.newBuilder().setQuery("");
-
-        // Insert schema
-        List<AppSearchSchema> schemas = ImmutableList.of(
-                new AppSearchSchema.Builder("typeA").build(),
-                new AppSearchSchema.Builder("typeB").build());
-        mAppSearchImpl.setSchema(
-                "package",
-                "database1",
-                schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
-                /* setSchemaStatsBuilder= */ null);
-        mAppSearchImpl.setSchema(
-                "package",
-                "database2",
-                schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
-                /* setSchemaStatsBuilder= */ null);
-
-        // Insert documents
-        GenericDocument document1 = new GenericDocument.Builder<>("namespace", "id",
-                "typeA").build();
-        mAppSearchImpl.putDocument("package", "database1", document1, /*logger=*/ null);
-
-        GenericDocument document2 = new GenericDocument.Builder<>("namespace", "id",
-                "typeB").build();
-        mAppSearchImpl.putDocument("package", "database2", document2, /*logger=*/ null);
-
-        // Rewrite SearchSpec
-        mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                ImmutableSet.of(createPrefix("package", "database1"),
-                        createPrefix("package", "database2")), ImmutableSet.of(
-                        "package$database1/typeA", "package$database1/typeB",
-                        "package$database2/typeA", "package$database2/typeB"));
-        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
-                "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
-                "package$database2/typeB");
-        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
-                "package$database1/namespace", "package$database2/namespace");
-    }
-
-    @Test
-    public void testRewriteSearchSpec_ignoresSearchSpecSchemaFilters() throws Exception {
-        SearchSpecProto.Builder searchSpecProto =
-                SearchSpecProto.newBuilder().setQuery("").addSchemaTypeFilters("type");
-
-        // Insert schema
-        List<AppSearchSchema> schemas =
-                Collections.singletonList(new AppSearchSchema.Builder("type").build());
-        mAppSearchImpl.setSchema(
-                "package",
-                "database",
-                schemas,
-                /*visibilityStore=*/ null,
-                /*schemasNotDisplayedBySystem=*/ Collections.emptyList(),
-                /*schemasVisibleToPackages=*/ Collections.emptyMap(),
-                /*forceOverride=*/ false,
-                /*version=*/ 0,
-                /* setSchemaStatsBuilder= */ null);
-
-        // Insert document
-        GenericDocument document = new GenericDocument.Builder<>("namespace", "id",
-                "type").build();
-        mAppSearchImpl.putDocument("package", "database", document, /*logger=*/ null);
-
-        // If 'allowedPrefixedSchemas' is empty, this returns false since there's nothing to
-        // search over. Despite the searchSpecProto having schema type filters.
-        assertThat(mAppSearchImpl.rewriteSearchSpecForPrefixesLocked(searchSpecProto,
-                Collections.singleton(createPrefix("package", "database")),
-                /*allowedPrefixedSchemas=*/ Collections.emptySet())).isFalse();
-    }
-
-    @Test
     public void testQueryEmptyDatabase() throws Exception {
         SearchSpec searchSpec =
                 new SearchSpec.Builder().setTermMatch(TermMatchType.Code.PREFIX_VALUE).build();
@@ -1623,47 +1502,6 @@
                 expectedMapping);
     }
 
-    @Test
-    public void testRewriteSearchResultProto() throws Exception {
-        final String prefix =
-                "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
-                        + PrefixUtil.DATABASE_DELIMITER;
-        final String id = "id";
-        final String namespace = prefix + "namespace";
-        final String schemaType = prefix + "schema";
-
-        // Building the SearchResult received from query.
-        DocumentProto documentProto = DocumentProto.newBuilder()
-                .setUri(id)
-                .setNamespace(namespace)
-                .setSchema(schemaType)
-                .build();
-        SearchResultProto.ResultProto resultProto = SearchResultProto.ResultProto.newBuilder()
-                .setDocument(documentProto)
-                .build();
-        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
-                .addResults(resultProto)
-                .build();
-        SchemaTypeConfigProto schemaTypeConfigProto =
-                SchemaTypeConfigProto.newBuilder()
-                        .setSchemaType(schemaType)
-                        .build();
-        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
-                ImmutableMap.of(schemaType, schemaTypeConfigProto));
-
-        DocumentProto.Builder strippedDocumentProto = documentProto.toBuilder();
-        removePrefixesFromDocument(strippedDocumentProto);
-        SearchResultPage searchResultPage =
-                AppSearchImpl.rewriteSearchResultProto(searchResultProto, schemaMap);
-        for (SearchResult result : searchResultPage.getResults()) {
-            assertThat(result.getPackageName()).isEqualTo("com.package.foo");
-            assertThat(result.getDatabaseName()).isEqualTo("databaseName");
-            assertThat(result.getGenericDocument()).isEqualTo(
-                    GenericDocumentToProtoConverter.toGenericDocument(
-                            strippedDocumentProto.build(), prefix, schemaMap.get(prefix)));
-        }
-    }
-
     @FlakyTest(bugId = 204186664)
     @Test
     public void testReportUsage() throws Exception {
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
new file mode 100644
index 0000000..47462dc
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverterTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.converter;
+
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.appsearch.app.SearchResult;
+import androidx.appsearch.app.SearchResultPage;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+
+import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.SearchResultProto;
+import com.google.common.collect.ImmutableMap;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+public class SearchResultToProtoConverterTest {
+    @Test
+    public void testToSearchResultProto() throws Exception {
+        final String prefix =
+                "com.package.foo" + PrefixUtil.PACKAGE_DELIMITER + "databaseName"
+                        + PrefixUtil.DATABASE_DELIMITER;
+        final String id = "id";
+        final String namespace = prefix + "namespace";
+        final String schemaType = prefix + "schema";
+
+        // Building the SearchResult received from query.
+        DocumentProto.Builder documentProtoBuilder = DocumentProto.newBuilder()
+                .setUri(id)
+                .setNamespace(namespace)
+                .setSchema(schemaType);
+        SearchResultProto searchResultProto = SearchResultProto.newBuilder()
+                .addResults(SearchResultProto.ResultProto.newBuilder()
+                        .setDocument(documentProtoBuilder))
+                .build();
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder()
+                        .setSchemaType(schemaType)
+                        .build();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(prefix,
+                ImmutableMap.of(schemaType, schemaTypeConfigProto));
+
+        removePrefixesFromDocument(documentProtoBuilder);
+        SearchResultPage searchResultPage =
+                SearchResultToProtoConverter.toSearchResultPage(searchResultProto, schemaMap);
+        assertThat(searchResultPage.getResults()).hasSize(1);
+        SearchResult result = searchResultPage.getResults().get(0);
+        assertThat(result.getPackageName()).isEqualTo("com.package.foo");
+        assertThat(result.getDatabaseName()).isEqualTo("databaseName");
+        assertThat(result.getGenericDocument()).isEqualTo(
+                GenericDocumentToProtoConverter.toGenericDocument(
+                        documentProtoBuilder.build(), prefix, schemaMap.get(prefix)));
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
new file mode 100644
index 0000000..29aeb96
--- /dev/null
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverterTest.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.appsearch.localstorage.converter;
+
+import static androidx.appsearch.app.SearchSpec.GROUPING_TYPE_PER_PACKAGE;
+import static androidx.appsearch.app.SearchSpec.ORDER_ASCENDING;
+import static androidx.appsearch.app.SearchSpec.RANKING_STRATEGY_CREATION_TIMESTAMP;
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.PackageIdentifier;
+import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.util.PrefixUtil;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+
+import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
+import com.google.android.icing.proto.ScoringSpecProto;
+import com.google.android.icing.proto.SearchSpecProto;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class SearchSpecToProtoConverterTest {
+
+    @Test
+    public void testToSearchSpecProto() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+        SchemaTypeConfigProto configProto = SchemaTypeConfigProto.getDefaultInstance();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of(
+                                prefix1 + "namespace1",
+                                prefix1 + "namespace2"),
+                        prefix2, ImmutableSet.of(
+                                prefix2 + "namespace1",
+                                prefix2 + "namespace2")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                prefix1 + "typeA", configProto,
+                                prefix1 + "typeB", configProto),
+                        prefix2, ImmutableMap.of(
+                                prefix2 + "typeA", configProto,
+                                prefix2 + "typeB", configProto)));
+        // Convert SearchSpec to proto.
+        SearchSpecProto searchSpecProto = converter.toSearchSpecProto(
+                /*queryExpression=*/"query");
+
+        assertThat(searchSpecProto.getQuery()).isEqualTo("query");
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database1/typeA", "package$database1/typeB", "package$database2/typeA",
+                "package$database2/typeB");
+        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+                "package$database1/namespace1", "package$database1/namespace2",
+                "package$database2/namespace1", "package$database2/namespace2");
+    }
+
+    @Test
+    public void testToScoringSpecProto()  {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setOrder(ORDER_ASCENDING)
+                .setRankingStrategy(RANKING_STRATEGY_CREATION_TIMESTAMP).build();
+
+        ScoringSpecProto scoringSpecProto = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(),
+                /*namespaceMap=*/ImmutableMap.of(),
+                /*schemaMap=*/ImmutableMap.of()).toScoringSpecProto();
+
+        assertThat(scoringSpecProto.getOrderBy().getNumber())
+                .isEqualTo(ScoringSpecProto.Order.Code.ASC_VALUE);
+        assertThat(scoringSpecProto.getRankBy().getNumber())
+                .isEqualTo(ScoringSpecProto.RankingStrategy.Code.CREATION_TIMESTAMP_VALUE);
+    }
+
+    @Test
+    public void testToResultSpecProto()  {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultCountPerPage(123)
+                .setSnippetCount(234)
+                .setSnippetCountPerProperty(345)
+                .setMaxSnippetSize(456)
+                .build();
+
+        SearchSpecToProtoConverter convert = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(),
+                /*namespaceMap=*/ImmutableMap.of(),
+                /*schemaMap=*/ImmutableMap.of());
+        ResultSpecProto resultSpecProto = convert.toResultSpecProto(
+                /*namespaceMap=*/ImmutableMap.of());
+
+        assertThat(resultSpecProto.getNumPerPage()).isEqualTo(123);
+        assertThat(resultSpecProto.getSnippetSpec().getNumToSnippet()).isEqualTo(234);
+        assertThat(resultSpecProto.getSnippetSpec().getNumMatchesPerProperty()).isEqualTo(345);
+        assertThat(resultSpecProto.getSnippetSpec().getMaxWindowBytes()).isEqualTo(456);
+    }
+
+    @Test
+    public void testToResultSpecProto_groupByPackage()  {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultGrouping(GROUPING_TYPE_PER_PACKAGE, 5)
+                .build();
+
+        String prefix1 = PrefixUtil.createPrefix("package1", "database");
+        String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                /*namespaceMap=*/ImmutableMap.of(),
+                /*schemaMap=*/ImmutableMap.of());
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of(
+                                prefix1 + "namespaceA",
+                                prefix1 + "namespaceB"),
+                        prefix2, ImmutableSet.of(
+                                prefix2 + "namespaceA",
+                                prefix2 + "namespaceB")));
+
+        assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
+        // First grouping should have same package name.
+        ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+        assertThat(grouping1.getMaxResults()).isEqualTo(5);
+        assertThat(grouping1.getNamespacesCount()).isEqualTo(2);
+        assertThat(
+                PrefixUtil.getPackageName(grouping1.getNamespaces(0)))
+                .isEqualTo(
+                        PrefixUtil.getPackageName(grouping1.getNamespaces(1)));
+
+        // Second grouping should have same package name.
+        ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+        assertThat(grouping2.getMaxResults()).isEqualTo(5);
+        assertThat(grouping2.getNamespacesCount()).isEqualTo(2);
+        assertThat(
+                PrefixUtil.getPackageName(grouping2.getNamespaces(0)))
+                .isEqualTo(
+                        PrefixUtil.getPackageName(grouping2.getNamespaces(1)));
+    }
+
+    @Test
+    public void testToResultSpecProto_groupByNamespace() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultGrouping(SearchSpec.GROUPING_TYPE_PER_NAMESPACE, 5)
+                .build();
+
+        String prefix1 = PrefixUtil.createPrefix("package1", "database");
+        String prefix2 = PrefixUtil.createPrefix("package2", "database");
+
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix1, ImmutableSet.of(
+                        prefix1 + "namespaceA",
+                        prefix1 + "namespaceB"),
+                prefix2, ImmutableSet.of(
+                        prefix2 + "namespaceA",
+                        prefix2 + "namespaceB"));
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                namespaceMap,
+                /*schemaMap=*/ImmutableMap.of());
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+
+        assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(2);
+        // First grouping should have same namespace.
+        ResultSpecProto.ResultGrouping grouping1 = resultSpecProto.getResultGroupings(0);
+        assertThat(grouping1.getNamespacesCount()).isEqualTo(2);
+        assertThat(
+                PrefixUtil.removePrefix(grouping1.getNamespaces(0)))
+                .isEqualTo(
+                        PrefixUtil.removePrefix(grouping1.getNamespaces(1)));
+
+        // Second grouping should have same namespace.
+        ResultSpecProto.ResultGrouping grouping2 = resultSpecProto.getResultGroupings(1);
+        assertThat(grouping2.getNamespacesCount()).isEqualTo(2);
+        assertThat(
+                PrefixUtil.removePrefix(grouping1.getNamespaces(0)))
+                .isEqualTo(
+                        PrefixUtil.removePrefix(grouping1.getNamespaces(1)));
+    }
+
+    @Test
+    public void testToResultSpecProto_groupByNamespaceAndPackage() throws Exception {
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .setResultGrouping(GROUPING_TYPE_PER_PACKAGE
+                        | SearchSpec.GROUPING_TYPE_PER_NAMESPACE , 5)
+                .build();
+
+        String prefix1 = PrefixUtil.createPrefix("package1", "database");
+        String prefix2 = PrefixUtil.createPrefix("package2", "database");
+        Map<String, Set<String>> namespaceMap = /*namespaceMap=*/ImmutableMap.of(
+                prefix1, ImmutableSet.of(
+                        prefix1 + "namespaceA",
+                        prefix1 + "namespaceB"),
+                prefix2, ImmutableSet.of(
+                        prefix2 + "namespaceA",
+                        prefix2 + "namespaceB"));
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                namespaceMap, /*schemaMap=*/ImmutableMap.of());
+        ResultSpecProto resultSpecProto = converter.toResultSpecProto(namespaceMap);
+
+        // All namespace should be separated.
+        assertThat(resultSpecProto.getResultGroupingsCount()).isEqualTo(4);
+        assertThat(resultSpecProto.getResultGroupings(0).getNamespacesCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getResultGroupings(1).getNamespacesCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getResultGroupings(2).getNamespacesCount()).isEqualTo(1);
+        assertThat(resultSpecProto.getResultGroupings(3).getNamespacesCount()).isEqualTo(1);
+    }
+
+    @Test
+    public void testGetTargetNamespaceFilters_emptySearchingFilter() {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        String prefix2 = PrefixUtil.createPrefix("package", "database2");
+        // search both prefixes
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix1, ImmutableSet.of("package$database1/namespace1",
+                        "package$database1/namespace2"),
+                prefix2, ImmutableSet.of("package$database2/namespace3",
+                        "package$database2/namespace4"));
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                namespaceMap, /*schemaMap=*/ImmutableMap.of());
+
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+
+        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+                "package$database1/namespace1", "package$database1/namespace2",
+                "package$database2/namespace3", "package$database2/namespace4");
+    }
+
+    @Test
+    public void testGetTargetNamespaceFilters_searchPartialPrefix() {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+        String prefix2 = PrefixUtil.createPrefix("package", "database2");
+
+        // Only search for prefix1
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1",
+                            "package$database1/namespace2"),
+                        prefix2, ImmutableSet.of("package$database2/namespace3",
+                            "package$database2/namespace4")),
+                /*schemaMap=*/ImmutableMap.of());
+
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // Only search prefix1 will return namespace 1 and 2.
+        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+                "package$database1/namespace1", "package$database1/namespace2");
+    }
+
+    @Test
+    public void testGetTargetNamespaceFilters_intersectionWithSearchingFilter() {
+        // Put some searching namespaces.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterNamespaces("namespace1", "nonExist").build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1",
+                                "package$database1/namespace2")),
+                /*schemaMap=*/ImmutableMap.of());
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // If the searching namespace filter is not empty, the target namespace filter will be the
+        // intersection of the searching namespace filters that users want to search over and
+        // those candidates which are stored in AppSearch.
+        assertThat(searchSpecProto.getNamespaceFiltersList()).containsExactly(
+                "package$database1/namespace1");
+    }
+
+    @Test
+    public void testGetTargetNamespaceFilters_intersectionWithNonExistFilter() {
+        // Search in non-exist namespaces
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterNamespaces("nonExist").build();
+        String prefix1 = PrefixUtil.createPrefix("package", "database1");
+
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1",
+                                "package$database1/namespace2")),
+                /*schemaMap=*/ImmutableMap.of());
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // If the searching namespace filter is not empty, the target namespace filter will be the
+        // intersection of the searching namespace filters that users want to search over and
+        // those candidates which are stored in AppSearch.
+        assertThat(searchSpecProto.getNamespaceFiltersList()).isEmpty();
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_emptySearchingFilter() {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = createPrefix("package", "database1");
+        String prefix2 = createPrefix("package", "database2");
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1, prefix2),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto),
+                        prefix2, ImmutableMap.of(
+                                "package$database2/typeC", schemaTypeConfigProto,
+                                "package$database2/typeD", schemaTypeConfigProto)));
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // Empty searching filter will get all types for target filter
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database1/typeA", "package$database1/typeB",
+                "package$database2/typeC", "package$database2/typeD");
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_searchPartialFilter() {
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+        String prefix1 = createPrefix("package", "database1");
+        String prefix2 = createPrefix("package", "database2");
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        // only search in prefix1
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto),
+                        prefix2, ImmutableMap.of(
+                                "package$database2/typeC", schemaTypeConfigProto,
+                                "package$database2/typeD", schemaTypeConfigProto)));
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // Only search prefix1 will return typeA and B.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database1/typeA", "package$database1/typeB");
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_intersectionWithSearchingFilter() {
+        // Put some searching schemas.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("typeA", "nonExist").build();
+        String prefix1 = createPrefix("package", "database1");
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto)));
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // If the searching schema filter is not empty, the target schema filter will be the
+        // intersection of the schema filters that users want to search over and those candidates
+        // which are stored in AppSearch.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                "package$database1/typeA");
+    }
+
+    @Test
+    public void testGetTargetSchemaFilters_intersectionWithNonExistFilter() {
+        // Put non-exist searching schema.
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("nonExist").build();
+        String prefix1 = createPrefix("package", "database1");
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix1),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix1, ImmutableSet.of("package$database1/namespace1")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix1, ImmutableMap.of(
+                                "package$database1/typeA", schemaTypeConfigProto,
+                                "package$database1/typeB", schemaTypeConfigProto)));
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+
+        // If there is no intersection of the schema filters that user want to search over and
+        // those filters which are stored in AppSearch, return empty.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).isEmpty();
+    }
+
+    @Test
+    public void testRemoveInaccessibleSchemaFilter() {
+        String prefix = PrefixUtil.createPrefix("package", "database");
+
+        SearchSpec searchSpec = new SearchSpec.Builder().build();
+
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        SearchSpecToProtoConverter converter = new SearchSpecToProtoConverter(searchSpec,
+                /*prefixes=*/ImmutableSet.of(prefix),
+                /*namespaceMap=*/ImmutableMap.of(
+                        prefix, ImmutableSet.of("package$database/namespace1")),
+                /*schemaMap=*/ImmutableMap.of(
+                        prefix, ImmutableMap.of(
+                                "package$database/schema1", schemaTypeConfigProto,
+                                "package$database/schema2", schemaTypeConfigProto,
+                                "package$database/schema3", schemaTypeConfigProto)));
+
+        converter.removeInaccessibleSchemaFilter(
+                /*callerPackageName=*/"otherPackageName",
+                new VisibilityStore() {
+                    @Override
+                    public void setVisibility(@NonNull String packageName,
+                            @NonNull String databaseName,
+                            @NonNull Set<String> schemasNotDisplayedBySystem,
+                            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
+                            throws AppSearchException {
+
+                    }
+
+                    @Override
+                    public boolean isSchemaSearchableByCaller(@NonNull String packageName,
+                            @NonNull String databaseName, @NonNull String prefixedSchema,
+                            int callerUid, boolean callerHasSystemAccess) {
+                        // filter out schema 2 which is not searchable for user.
+                        return !prefixedSchema.equals(prefix + "schema2");
+                    }
+                },
+                /*callerUid=*/-1,
+                /*callerHasSystemAccess=*/true);
+
+        SearchSpecProto searchSpecProto =
+                converter.toSearchSpecProto(/*queryExpression=*/"");
+        // schema 2 is filtered out since it is not searchable for user.
+        assertThat(searchSpecProto.getSchemaTypeFiltersList()).containsExactly(
+                prefix + "schema1", prefix + "schema3");
+    }
+
+    @Test
+    public void testIsNothingToSearch() {
+        String prefix = PrefixUtil.createPrefix("package", "database");
+        SearchSpec searchSpec = new SearchSpec.Builder()
+                .addFilterSchemas("schema").addFilterNamespaces("namespace").build();
+
+        // build maps
+        SchemaTypeConfigProto schemaTypeConfigProto =
+                SchemaTypeConfigProto.newBuilder().getDefaultInstanceForType();
+        Map<String, Map<String, SchemaTypeConfigProto>> schemaMap = ImmutableMap.of(
+                prefix, ImmutableMap.of(
+                        "package$database/schema", schemaTypeConfigProto)
+        );
+        Map<String, Set<String>> namespaceMap = ImmutableMap.of(
+                prefix, ImmutableSet.of("package$database/namespace")
+        );
+
+        SearchSpecToProtoConverter emptySchemaConverter =
+                new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+                        /*namespaceMap=*/namespaceMap,
+                        /*schemaMap=*/ImmutableMap.of());
+        assertThat(emptySchemaConverter.isNothingToSearch()).isTrue();
+
+        SearchSpecToProtoConverter emptyNamespaceConverter =
+                new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+                        /*namespaceMap=*/ImmutableMap.of(),
+                        schemaMap);
+        assertThat(emptyNamespaceConverter.isNothingToSearch()).isTrue();
+
+        SearchSpecToProtoConverter nonEmptyConverter =
+                new SearchSpecToProtoConverter(searchSpec, /*prefixes=*/ImmutableSet.of(prefix),
+                        namespaceMap, schemaMap);
+        assertThat(nonEmptyConverter.isNothingToSearch()).isFalse();
+
+        // remove all target schema filter, and the query becomes nothing to search.
+        nonEmptyConverter.removeInaccessibleSchemaFilter(
+                /*callerPackageName=*/"otherPackageName",
+                new VisibilityStore() {
+                    @Override
+                    public void setVisibility(@NonNull String packageName,
+                            @NonNull String databaseName,
+                            @NonNull Set<String> schemasNotDisplayedBySystem,
+                            @NonNull Map<String, List<PackageIdentifier>> schemasVisibleToPackages)
+                            throws AppSearchException {
+
+                    }
+
+                    @Override
+                    public boolean isSchemaSearchableByCaller(@NonNull String packageName,
+                            @NonNull String databaseName, @NonNull String prefixedSchema,
+                            int callerUid, boolean callerHasSystemAccess) {
+                        // filter out all schema.
+                        return false;
+                    }
+                },
+                /*callerUid=*/-1,
+                /*callerHasSystemAccess=*/true);
+        assertThat(nonEmptyConverter.isNothingToSearch()).isTrue();
+    }
+}
diff --git a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
index 1f33ff0..fd150c0 100644
--- a/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
+++ b/appsearch/appsearch-local-storage/src/androidTest/java/androidx/appsearch/localstorage/converter/SnippetTest.java
@@ -35,21 +35,21 @@
 import java.util.Map;
 
 public class SnippetTest {
-    private static final String SCHEMA_TYPE = "schema1";
     private static final String PACKAGE_NAME = "packageName";
     private static final String DATABASE_NAME = "databaseName";
     private static final String PREFIX = PrefixUtil.createPrefix(PACKAGE_NAME, DATABASE_NAME);
+    private static final String PREFIXED_SCHEMA_TYPE = PREFIX + "schema1";
+    private static final String PREFIXED_NAMESPACE = PREFIX + "";
     private static final SchemaTypeConfigProto SCHEMA_TYPE_CONFIG_PROTO =
             SchemaTypeConfigProto.newBuilder()
-                    .setSchemaType(PREFIX + SCHEMA_TYPE)
+                    .setSchemaType(PREFIXED_SCHEMA_TYPE)
                     .build();
     private static final Map<String, Map<String, SchemaTypeConfigProto>> SCHEMA_MAP =
             Collections.singletonMap(PREFIX,
-                    Collections.singletonMap(PREFIX + SCHEMA_TYPE,
-                            SCHEMA_TYPE_CONFIG_PROTO));
+                    Collections.singletonMap(PREFIXED_SCHEMA_TYPE, SCHEMA_TYPE_CONFIG_PROTO));
 
     @Test
-    public void testSingleStringSnippet() {
+    public void testSingleStringSnippet() throws Exception {
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
@@ -61,7 +61,8 @@
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri(id)
-                .setSchema(SCHEMA_TYPE)
+                .setNamespace(PREFIXED_NAMESPACE)
+                .setSchema(PREFIXED_SCHEMA_TYPE)
                 .addProperties(PropertyProto.newBuilder()
                         .setName(propertyKeyString)
                         .addStringValues(propertyValueString))
@@ -90,8 +91,6 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                Collections.singletonList(PACKAGE_NAME),
-                Collections.singletonList(DATABASE_NAME),
                 SCHEMA_MAP);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -110,7 +109,7 @@
     }
 
     @Test
-    public void testNoSnippets() {
+    public void testNoSnippets() throws Exception {
         final String propertyKeyString = "content";
         final String propertyValueString = "A commonly used fake word is foo.\n"
                 + "   Another nonsense word that’s used a lot\n"
@@ -120,7 +119,8 @@
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri(id)
-                .setSchema(SCHEMA_TYPE)
+                .setNamespace(PREFIXED_NAMESPACE)
+                .setSchema(PREFIXED_SCHEMA_TYPE)
                 .addProperties(PropertyProto.newBuilder()
                         .setName(propertyKeyString)
                         .addStringValues(propertyValueString))
@@ -131,19 +131,18 @@
 
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                Collections.singletonList(PACKAGE_NAME),
-                Collections.singletonList(DATABASE_NAME),
                 SCHEMA_MAP);
         assertThat(searchResultPage.getResults()).hasSize(1);
         assertThat(searchResultPage.getResults().get(0).getMatchInfos()).isEmpty();
     }
 
     @Test
-    public void testMultipleStringSnippet() {
+    public void testMultipleStringSnippet() throws Exception {
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri("uri1")
-                .setSchema(SCHEMA_TYPE)
+                .setNamespace(PREFIXED_NAMESPACE)
+                .setSchema(PREFIXED_SCHEMA_TYPE)
                 .addProperties(PropertyProto.newBuilder()
                         .setName("senderName")
                         .addStringValues("Test Name Jr."))
@@ -188,8 +187,6 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                Collections.singletonList(PACKAGE_NAME),
-                Collections.singletonList(DATABASE_NAME),
                 SCHEMA_MAP);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
@@ -220,14 +217,17 @@
     }
 
     @Test
-    public void testNestedDocumentSnippet() {
+    public void testNestedDocumentSnippet() throws Exception {
         // Building the SearchResult received from query.
         DocumentProto documentProto = DocumentProto.newBuilder()
                 .setUri("id1")
-                .setSchema(SCHEMA_TYPE)
+                .setNamespace(PREFIXED_NAMESPACE)
+                .setSchema(PREFIXED_SCHEMA_TYPE)
                 .addProperties(PropertyProto.newBuilder()
                         .setName("sender")
                         .addDocumentValues(DocumentProto.newBuilder()
+                                .setNamespace(PREFIXED_NAMESPACE)
+                                .setSchema(PREFIXED_SCHEMA_TYPE)
                                 .addProperties(PropertyProto.newBuilder()
                                         .setName("name")
                                         .addStringValues("Test Name Jr."))
@@ -273,8 +273,6 @@
         // Making ResultReader and getting Snippet values.
         SearchResultPage searchResultPage = SearchResultToProtoConverter.toSearchResultPage(
                 searchResultProto,
-                Collections.singletonList(PACKAGE_NAME),
-                Collections.singletonList(DATABASE_NAME),
                 SCHEMA_MAP);
         assertThat(searchResultPage.getResults()).hasSize(1);
         SearchResult.MatchInfo match1 = searchResultPage.getResults().get(0).getMatchInfos().get(0);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
index 0b13263..d6978c4 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/AppSearchImpl.java
@@ -21,7 +21,6 @@
 import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
 import static androidx.appsearch.localstorage.util.PrefixUtil.getPrefix;
-import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
 import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
 import android.os.Bundle;
@@ -106,7 +105,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -783,22 +781,22 @@
         try {
             throwIfClosedLocked();
             String prefix = createPrefix(packageName, databaseName);
-            List<TypePropertyMask> nonPrefixedPropertyMasks =
-                    TypePropertyPathToProtoConverter.toTypePropertyMaskList(typePropertyPaths);
+            List<TypePropertyMask.Builder> nonPrefixedPropertyMaskBuilders =
+                    TypePropertyPathToProtoConverter
+                            .toTypePropertyMaskBuilderList(typePropertyPaths);
             List<TypePropertyMask> prefixedPropertyMasks =
-                    new ArrayList<>(nonPrefixedPropertyMasks.size());
-            for (int i = 0; i < nonPrefixedPropertyMasks.size(); ++i) {
-                TypePropertyMask typePropertyMask = nonPrefixedPropertyMasks.get(i);
-                String nonPrefixedType = typePropertyMask.getSchemaType();
+                    new ArrayList<>(nonPrefixedPropertyMaskBuilders.size());
+            for (int i = 0; i < nonPrefixedPropertyMaskBuilders.size(); ++i) {
+                String nonPrefixedType = nonPrefixedPropertyMaskBuilders.get(i).getSchemaType();
                 String prefixedType = nonPrefixedType.equals(
                         GetByDocumentIdRequest.PROJECTION_SCHEMA_TYPE_WILDCARD)
                         ? nonPrefixedType : prefix + nonPrefixedType;
                 prefixedPropertyMasks.add(
-                        typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
+                        nonPrefixedPropertyMaskBuilders.get(i).setSchemaType(prefixedType).build());
             }
             GetResultSpecProto getResultSpec =
-                    GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks
-                    ).build();
+                    GetResultSpecProto.newBuilder().addAllTypePropertyMasks(prefixedPropertyMasks)
+                            .build();
 
             String finalNamespace = createPrefix(packageName, databaseName) + namespace;
             if (mLogUtil.isPiiTraceEnabled()) {
@@ -867,13 +865,19 @@
             }
 
             String prefix = createPrefix(packageName, databaseName);
-            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
+            SearchSpecToProtoConverter searchSpecToProtoConverter =
+                    new SearchSpecToProtoConverter(searchSpec, Collections.singleton(prefix),
+                            mNamespaceMapLocked, mSchemaMapLocked);
+            if (searchSpecToProtoConverter.isNothingToSearch()) {
+                // there is nothing to search over given their search filters, so we can return an
+                // empty SearchResult and skip sending request to Icing.
+                return new SearchResultPage(Bundle.EMPTY);
+            }
 
             SearchResultPage searchResultPage =
-                    doQueryLocked(Collections.singleton(createPrefix(packageName, databaseName)),
-                            allowedPrefixedSchemas,
+                    doQueryLocked(
                             queryExpression,
-                            searchSpec,
+                            searchSpecToProtoConverter,
                             sStatsBuilder);
             addNextPageToken(packageName, searchResultPage.getNextPageToken());
             return searchResultPage;
@@ -946,56 +950,22 @@
                     }
                 }
             }
-
-            // Convert schema filters to prefixed schema filters
-            ArraySet<String> prefixedSchemaFilters = new ArraySet<>();
-            for (String prefix : prefixFilters) {
-                List<String> schemaFilters = searchSpec.getFilterSchemas();
-                if (schemaFilters.isEmpty()) {
-                    // Client didn't specify certain schemas to search over, check all schemas
-                    prefixedSchemaFilters.addAll(mSchemaMapLocked.get(prefix).keySet());
-                } else {
-                    // Client specified some schemas to search over, check each one
-                    for (int i = 0; i < schemaFilters.size(); i++) {
-                        prefixedSchemaFilters.add(prefix + schemaFilters.get(i));
-                    }
-                }
+            SearchSpecToProtoConverter searchSpecToProtoConverter =
+                    new SearchSpecToProtoConverter(searchSpec, prefixFilters, mNamespaceMapLocked,
+                    mSchemaMapLocked);
+            // Remove those inaccessible schemas.
+            searchSpecToProtoConverter.removeInaccessibleSchemaFilter(callerPackageName,
+                    visibilityStore, callerUid, callerHasSystemAccess);
+            if (searchSpecToProtoConverter.isNothingToSearch()) {
+                // there is nothing to search over given their search filters, so we can return an
+                // empty SearchResult and skip sending request to Icing.
+                return new SearchResultPage(Bundle.EMPTY);
             }
-
-            // Remove the schemas the client is not allowed to search over
-            Iterator<String> prefixedSchemaIt = prefixedSchemaFilters.iterator();
-            while (prefixedSchemaIt.hasNext()) {
-                String prefixedSchema = prefixedSchemaIt.next();
-                String packageName = getPackageName(prefixedSchema);
-
-                boolean allow;
-                if (packageName.equals(callerPackageName)) {
-                    // Callers can always retrieve their own data
-                    allow = true;
-                } else if (visibilityStore == null) {
-                    // If there's no visibility store, there's no extra access
-                    allow = false;
-                } else {
-                    String databaseName = getDatabaseName(prefixedSchema);
-                    allow = visibilityStore.isSchemaSearchableByCaller(
-                            packageName,
-                            databaseName,
-                            prefixedSchema,
-                            callerUid,
-                            callerHasSystemAccess);
-                }
-
-                if (!allow) {
-                    prefixedSchemaIt.remove();
-                }
-            }
-
-            SearchResultPage searchResultPage = doQueryLocked(
-                    prefixFilters,
-                    prefixedSchemaFilters,
-                    queryExpression,
-                    searchSpec,
-                    sStatsBuilder);
+            SearchResultPage searchResultPage =
+                    doQueryLocked(
+                            queryExpression,
+                            searchSpecToProtoConverter,
+                            sStatsBuilder);
             addNextPageToken(callerPackageName, searchResultPage.getNextPageToken());
             return searchResultPage;
         } finally {
@@ -1009,6 +979,65 @@
         }
     }
 
+    @GuardedBy("mReadWriteLock")
+    private SearchResultPage doQueryLocked(
+            @NonNull String queryExpression,
+            @NonNull SearchSpecToProtoConverter searchSpecToProtoConverter,
+            @Nullable SearchStats.Builder sStatsBuilder)
+            throws AppSearchException {
+        // Rewrite the given SearchSpec into SearchSpecProto, ResultSpecProto and ScoringSpecProto.
+        // All processes are counted in rewriteSearchSpecLatencyMillis
+        long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
+        SearchSpecProto finalSearchSpec =
+                searchSpecToProtoConverter.toSearchSpecProto(queryExpression);
+        ResultSpecProto finalResultSpec = searchSpecToProtoConverter.toResultSpecProto(
+                mNamespaceMapLocked);
+        ScoringSpecProto scoringSpec = searchSpecToProtoConverter.toScoringSpecProto();
+        if (sStatsBuilder != null) {
+            sStatsBuilder.setRewriteSearchSpecLatencyMillis((int)
+                    (SystemClock.elapsedRealtime() - rewriteSearchSpecLatencyStartMillis));
+        }
+
+        // Send request to Icing.
+        SearchResultProto searchResultProto = searchInIcingLocked(
+                finalSearchSpec, finalResultSpec, scoringSpec, sStatsBuilder);
+
+        long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
+        // Rewrite search result before we return.
+        SearchResultPage searchResultPage = SearchResultToProtoConverter
+                .toSearchResultPage(searchResultProto, mSchemaMapLocked);
+        if (sStatsBuilder != null) {
+            sStatsBuilder.setRewriteSearchResultLatencyMillis(
+                    (int) (SystemClock.elapsedRealtime()
+                            - rewriteSearchResultLatencyStartMillis));
+        }
+        return searchResultPage;
+    }
+
+    @GuardedBy("mReadWriteLock")
+    private SearchResultProto searchInIcingLocked(
+            @NonNull SearchSpecProto searchSpec,
+            @NonNull ResultSpecProto resultSpec,
+            @NonNull ScoringSpecProto scoringSpec,
+            @Nullable SearchStats.Builder sStatsBuilder) throws AppSearchException {
+        if (mLogUtil.isPiiTraceEnabled()) {
+            mLogUtil.piiTrace(
+                    "search, request",
+                    searchSpec.getQuery(),
+                    searchSpec + ", " + scoringSpec + ", " + resultSpec);
+        }
+        SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
+                searchSpec, scoringSpec, resultSpec);
+        mLogUtil.piiTrace(
+                "search, response", searchResultProto.getResultsCount(), searchResultProto);
+        if (sStatsBuilder != null) {
+            sStatsBuilder.setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()));
+            AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
+        }
+        checkSuccess(searchResultProto.getStatus());
+        return searchResultProto;
+    }
+
     /**
      * Returns a mapping of package names to all the databases owned by that package.
      *
@@ -1038,89 +1067,6 @@
         }
     }
 
-    @GuardedBy("mReadWriteLock")
-    private SearchResultPage doQueryLocked(
-            @NonNull Set<String> prefixes,
-            @NonNull Set<String> allowedPrefixedSchemas,
-            @NonNull String queryExpression,
-            @NonNull SearchSpec searchSpec,
-            @Nullable SearchStats.Builder sStatsBuilder)
-            throws AppSearchException {
-        long rewriteSearchSpecLatencyStartMillis = SystemClock.elapsedRealtime();
-
-        SearchSpecProto.Builder searchSpecBuilder =
-                SearchSpecToProtoConverter.toSearchSpecProto(searchSpec).toBuilder().setQuery(
-                        queryExpression);
-        // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
-        // over given their search filters, so we can return an empty SearchResult and skip
-        // sending request to Icing.
-        if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder, prefixes,
-                allowedPrefixedSchemas)) {
-            if (sStatsBuilder != null) {
-                sStatsBuilder.setRewriteSearchSpecLatencyMillis(
-                        (int) (SystemClock.elapsedRealtime()
-                                - rewriteSearchSpecLatencyStartMillis));
-            }
-            return new SearchResultPage(Bundle.EMPTY);
-        }
-
-        // rewriteSearchSpec, rewriteResultSpec and convertScoringSpec are all counted in
-        // rewriteSearchSpecLatencyMillis
-        ResultSpecProto.Builder resultSpecBuilder =
-                SearchSpecToProtoConverter.toResultSpecProto(searchSpec).toBuilder();
-
-        int groupingType = searchSpec.getResultGroupingTypeFlags();
-        if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
-                && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
-            addPerPackagePerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
-                    searchSpec.getResultGroupingLimit());
-        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
-            addPerPackageResultGroupingsLocked(resultSpecBuilder, prefixes,
-                    searchSpec.getResultGroupingLimit());
-        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
-            addPerNamespaceResultGroupingsLocked(resultSpecBuilder, prefixes,
-                    searchSpec.getResultGroupingLimit());
-        }
-
-        rewriteResultSpecForPrefixesLocked(resultSpecBuilder, prefixes, allowedPrefixedSchemas);
-        ScoringSpecProto scoringSpec = SearchSpecToProtoConverter.toScoringSpecProto(searchSpec);
-        SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
-        ResultSpecProto finalResultSpec = resultSpecBuilder.build();
-
-        long rewriteSearchSpecLatencyEndMillis = SystemClock.elapsedRealtime();
-
-        if (mLogUtil.isPiiTraceEnabled()) {
-            mLogUtil.piiTrace(
-                    "search, request",
-                    finalSearchSpec.getQuery(),
-                    finalSearchSpec + ", " + scoringSpec + ", " + finalResultSpec);
-        }
-        SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
-                finalSearchSpec, scoringSpec, finalResultSpec);
-        mLogUtil.piiTrace(
-                "search, response", searchResultProto.getResultsCount(), searchResultProto);
-
-        if (sStatsBuilder != null) {
-            sStatsBuilder
-                    .setStatusCode(statusProtoToResultCode(searchResultProto.getStatus()))
-                    .setRewriteSearchSpecLatencyMillis((int) (rewriteSearchSpecLatencyEndMillis
-                            - rewriteSearchSpecLatencyStartMillis));
-            AppSearchLoggerHelper.copyNativeStats(searchResultProto.getQueryStats(), sStatsBuilder);
-        }
-
-        checkSuccess(searchResultProto.getStatus());
-
-        long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
-        SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto, mSchemaMapLocked);
-        if (sStatsBuilder != null) {
-            sStatsBuilder.setRewriteSearchResultLatencyMillis(
-                    (int) (SystemClock.elapsedRealtime()
-                            - rewriteSearchResultLatencyStartMillis));
-        }
-
-        return resultPage;
-    }
-
     /**
      * Fetches the next page of results of a previously executed query. Results can be empty if
      * next-page token is invalid or all pages have been returned.
@@ -1169,14 +1115,15 @@
                 }
             }
             long rewriteSearchResultLatencyStartMillis = SystemClock.elapsedRealtime();
-            SearchResultPage resultPage = rewriteSearchResultProto(searchResultProto,
-                    mSchemaMapLocked);
+            // Rewrite search result before we return.
+            SearchResultPage searchResultPage = SearchResultToProtoConverter
+                    .toSearchResultPage(searchResultProto, mSchemaMapLocked);
             if (statsBuilder != null) {
                 statsBuilder.setRewriteSearchResultLatencyMillis(
                         (int) (SystemClock.elapsedRealtime()
                                 - rewriteSearchResultLatencyStartMillis));
             }
-            return resultPage;
+            return searchResultPage;
         } finally {
             mReadWriteLock.readLock().unlock();
             if (statsBuilder != null) {
@@ -1350,27 +1297,32 @@
                 return;
             }
 
-            SearchSpecProto searchSpecProto =
-                    SearchSpecToProtoConverter.toSearchSpecProto(searchSpec);
-            SearchSpecProto.Builder searchSpecBuilder = searchSpecProto.toBuilder()
-                    .setQuery(queryExpression);
-
             String prefix = createPrefix(packageName, databaseName);
-            Set<String> allowedPrefixedSchemas = getAllowedPrefixSchemasLocked(prefix, searchSpec);
-
-            // rewriteSearchSpecForPrefixesLocked will return false if there is nothing to search
-            // over given their search filters, so we can return early and skip sending request
-            // to Icing.
-            if (!rewriteSearchSpecForPrefixesLocked(searchSpecBuilder,
-                    Collections.singleton(prefix), allowedPrefixedSchemas)) {
+            if (!mNamespaceMapLocked.containsKey(prefix)) {
+                // The target database is empty so we can return early and skip sending request to
+                // Icing.
                 return;
             }
-            SearchSpecProto finalSearchSpec = searchSpecBuilder.build();
+
+            SearchSpecToProtoConverter searchSpecToProtoConverter =
+                    new SearchSpecToProtoConverter(searchSpec, Collections.singleton(prefix),
+                            mNamespaceMapLocked, mSchemaMapLocked);
+            if (searchSpecToProtoConverter.isNothingToSearch()) {
+                // there is nothing to search over given their search filters, so we can return
+                // early and skip sending request to Icing.
+                return;
+            }
+
+            SearchSpecProto finalSearchSpec =
+                    searchSpecToProtoConverter.toSearchSpecProto(queryExpression);
 
             Set<String> prefixedObservedSchemas = null;
             if (mObserverManager.isPackageObserved(packageName)) {
                 prefixedObservedSchemas = new ArraySet<>();
-                for (String prefixedType : allowedPrefixedSchemas) {
+                List<String> prefixedTargetSchemaTypes =
+                        finalSearchSpec.getSchemaTypeFiltersList();
+                for (int i = 0; i < prefixedTargetSchemaTypes.size(); i++) {
+                    String prefixedType = prefixedTargetSchemaTypes.get(i);
                     String shortTypeName = PrefixUtil.removePrefix(prefixedType);
                     if (mObserverManager.isSchemaTypeObserved(packageName, shortTypeName)) {
                         prefixedObservedSchemas.add(prefixedType);
@@ -1399,8 +1351,8 @@
      * Executes removeByQuery, creating change notifications for removal.
      *
      * @param packageName         The package name that owns the documents.
-     * @param rewrittenSearchSpec A search spec that has been run through
-     *                            {@link #rewriteSearchSpecForPrefixesLocked}.
+     * @param finalSearchSpec     The final search spec that has been written through
+     *                            {@link SearchSpecToProtoConverter}.
      * @param prefixedObservedSchemas The set of prefixed schemas that have valid registered
      *                                observers. Only changes to schemas in this set will be queued.
      */
@@ -1409,13 +1361,13 @@
     @GuardedBy("mReadWriteLock")
     private void doRemoveByQueryWithChangeNotificationLocked(
             @NonNull String packageName,
-            @NonNull SearchSpecProto rewrittenSearchSpec,
+            @NonNull SearchSpecProto finalSearchSpec,
             @NonNull Set<String> prefixedObservedSchemas,
             @Nullable RemoveStats.Builder removeStatsBuilder) throws AppSearchException {
         mLogUtil.piiTrace(
-                "removeByQuery.withChangeNotification, query request", rewrittenSearchSpec);
+                "removeByQuery.withChangeNotification, query request", finalSearchSpec);
         SearchResultProto searchResultProto = mIcingSearchEngineLocked.search(
-                rewrittenSearchSpec,
+                finalSearchSpec,
                 ScoringSpecProto.getDefaultInstance(),
                 RESULT_SPEC_NO_PROPERTIES);
         mLogUtil.piiTrace(
@@ -1952,302 +1904,6 @@
         return rewrittenSchemaResults;
     }
 
-    /**
-     * Rewrites the search spec filters with {@code prefixes}.
-     *
-     * <p>This method should be only called in query methods and get the READ lock to keep thread
-     * safety.
-     *
-     * @param searchSpecBuilder      Client-provided SearchSpec
-     * @param prefixes               Prefixes that we should prepend to all our filters
-     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over. This
-     *                               supersedes the schema filters that may exist on the {@code
-     *                               searchSpecBuilder}.
-     * @return false if none there would be nothing to search over.
-     */
-    @VisibleForTesting
-    @GuardedBy("mReadWriteLock")
-    boolean rewriteSearchSpecForPrefixesLocked(
-            @NonNull SearchSpecProto.Builder searchSpecBuilder,
-            @NonNull Set<String> prefixes,
-            @NonNull Set<String> allowedPrefixedSchemas) {
-        // Create a copy since retainAll() modifies the original set.
-        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
-        existingPrefixes.retainAll(prefixes);
-
-        if (existingPrefixes.isEmpty()) {
-            // None of the prefixes exist, empty query.
-            return false;
-        }
-
-        if (allowedPrefixedSchemas.isEmpty()) {
-            // Not allowed to search over any schemas, empty query.
-            return false;
-        }
-
-        // Clear the schema type filters since we'll be rewriting them with the
-        // allowedPrefixedSchemas.
-        searchSpecBuilder.clearSchemaTypeFilters();
-        searchSpecBuilder.addAllSchemaTypeFilters(allowedPrefixedSchemas);
-
-        // Cache the namespaces before clearing everything.
-        List<String> namespaceFilters = searchSpecBuilder.getNamespaceFiltersList();
-        searchSpecBuilder.clearNamespaceFilters();
-
-        // Rewrite non-schema filters to include a prefix.
-        for (String prefix : existingPrefixes) {
-            // TODO(b/169883602): We currently grab every namespace for every prefix. We can
-            //  optimize this by checking if a prefix has any allowedSchemaTypes. If not, that
-            //  means we don't want to query over anything in that prefix anyways, so we don't
-            //  need to grab its namespaces either.
-
-            // Empty namespaces on the search spec means to query over all namespaces.
-            Set<String> existingNamespaces = mNamespaceMapLocked.get(prefix);
-            if (existingNamespaces != null) {
-                if (namespaceFilters.isEmpty()) {
-                    // Include all namespaces
-                    searchSpecBuilder.addAllNamespaceFilters(existingNamespaces);
-                } else {
-                    // Prefix the given namespaces.
-                    for (int i = 0; i < namespaceFilters.size(); i++) {
-                        String prefixedNamespace = prefix + namespaceFilters.get(i);
-                        if (existingNamespaces.contains(prefixedNamespace)) {
-                            searchSpecBuilder.addNamespaceFilters(prefixedNamespace);
-                        }
-                    }
-                }
-            }
-        }
-        if (searchSpecBuilder.getNamespaceFiltersCount() == 0) {
-            // None of the user wanted namespace exist, empty query.
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Returns the set of allowed prefixed schemas that the {@code prefix} can query while taking
-     * into account the {@code searchSpec} schema filters.
-     *
-     * <p>This only checks intersection of schema filters on the search spec with those that the
-     * prefix owns itself. This does not check global query permissions.
-     */
-    @GuardedBy("mReadWriteLock")
-    private Set<String> getAllowedPrefixSchemasLocked(@NonNull String prefix,
-            @NonNull SearchSpec searchSpec) {
-        Set<String> allowedPrefixedSchemas = new ArraySet<>();
-
-        List<String> schemaFilters = searchSpec.getFilterSchemas();
-        Map<String, SchemaTypeConfigProto> prefixedSchemaMap = mSchemaMapLocked.get(prefix);
-        if (prefixedSchemaMap == null) {
-            // The db is empty, return early;
-            return allowedPrefixedSchemas;
-        }
-        if (schemaFilters.isEmpty()) {
-            // If the client didn't specify any schema filters, search over all of their schemas
-            allowedPrefixedSchemas.addAll(prefixedSchemaMap.keySet());
-        } else {
-            // Check all client specified schemas, add them if they exist in AppSearch.
-            for (int i = 0; i < schemaFilters.size(); i++) {
-                String prefixedSchemaType = prefix + schemaFilters.get(i);
-                if (prefixedSchemaMap.containsKey(prefixedSchemaType)) {
-                    allowedPrefixedSchemas.add(prefixedSchemaType);
-                }
-            }
-        }
-        return allowedPrefixedSchemas;
-    }
-
-    /**
-     * Rewrites the typePropertyMasks that exist in {@code prefixes}.
-     *
-     * <p>This method should be only called in query methods and get the READ lock to keep thread
-     * safety.
-     *
-     * @param resultSpecBuilder      ResultSpecs as specified by client
-     * @param prefixes               Prefixes that we should prepend to all our filters
-     * @param allowedPrefixedSchemas Prefixed schemas that the client is allowed to query over.
-     */
-    @VisibleForTesting
-    @GuardedBy("mReadWriteLock")
-    void rewriteResultSpecForPrefixesLocked(
-            @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes, @NonNull Set<String> allowedPrefixedSchemas) {
-        // Create a copy since retainAll() modifies the original set.
-        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
-        existingPrefixes.retainAll(prefixes);
-
-        List<TypePropertyMask> prefixedTypePropertyMasks = new ArrayList<>();
-        // Rewrite filters to include a database prefix.
-        for (String prefix : existingPrefixes) {
-            // Qualify the given schema types
-            for (TypePropertyMask typePropertyMask :
-                    resultSpecBuilder.getTypePropertyMasksList()) {
-                String unprefixedType = typePropertyMask.getSchemaType();
-                boolean isWildcard =
-                        unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
-                String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
-                if (isWildcard || allowedPrefixedSchemas.contains(prefixedType)) {
-                    prefixedTypePropertyMasks.add(
-                            typePropertyMask.toBuilder().setSchemaType(prefixedType).build());
-                }
-            }
-        }
-        resultSpecBuilder.clearTypePropertyMasks().addAllTypePropertyMasks(
-                prefixedTypePropertyMasks);
-    }
-
-    /**
-     * Adds result groupings for each namespace in each package being queried for.
-     *
-     * <p>This method should be only called in query methods and get the READ lock to keep thread
-     * safety.
-     *
-     * @param resultSpecBuilder ResultSpecs as specified by client
-     * @param prefixes          Prefixes that we should prepend to all our filters
-     * @param maxNumResults     The maximum number of results for each grouping to support.
-     */
-    @GuardedBy("mReadWriteLock")
-    private void addPerPackagePerNamespaceResultGroupingsLocked(
-            @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes, int maxNumResults) {
-        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
-        existingPrefixes.retainAll(prefixes);
-
-        // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
-        // same as the list of namespaces. If one package has multiple databases, each with the same
-        // namespace, then those should be grouped together.
-        Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
-        for (String prefix : existingPrefixes) {
-            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
-            if (prefixedNamespaces == null) {
-                continue;
-            }
-            String packageName = getPackageName(prefix);
-            // Create a new prefix without the database name. This will allow us to group namespaces
-            // that have the same name and package but a different database name together.
-            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
-            for (String prefixedNamespace : prefixedNamespaces) {
-                String namespace;
-                try {
-                    namespace = removePrefix(prefixedNamespace);
-                } catch (AppSearchException e) {
-                    // This should never happen. Skip this namespace if it does.
-                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
-                    continue;
-                }
-                String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
-                List<String> namespaceList =
-                        packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
-                if (namespaceList == null) {
-                    namespaceList = new ArrayList<>();
-                    packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
-                            namespaceList);
-                }
-                namespaceList.add(prefixedNamespace);
-            }
-        }
-
-        for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
-            resultSpecBuilder.addResultGroupings(
-                    ResultSpecProto.ResultGrouping.newBuilder()
-                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
-        }
-    }
-
-    /**
-     * Adds result groupings for each package being queried for.
-     *
-     * <p>This method should be only called in query methods and get the READ lock to keep thread
-     * safety.
-     *
-     * @param resultSpecBuilder ResultSpecs as specified by client
-     * @param prefixes          Prefixes that we should prepend to all our filters
-     * @param maxNumResults     The maximum number of results for each grouping to support.
-     */
-    @GuardedBy("mReadWriteLock")
-    private void addPerPackageResultGroupingsLocked(
-            @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes, int maxNumResults) {
-        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
-        existingPrefixes.retainAll(prefixes);
-
-        // Build up a map of package to namespaces.
-        Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
-        for (String prefix : existingPrefixes) {
-            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
-            if (prefixedNamespaces == null) {
-                continue;
-            }
-            String packageName = getPackageName(prefix);
-            List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
-            if (packageNamespaceList == null) {
-                packageNamespaceList = new ArrayList<>();
-                packageToNamespacesMap.put(packageName, packageNamespaceList);
-            }
-            packageNamespaceList.addAll(prefixedNamespaces);
-        }
-
-        for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
-            resultSpecBuilder.addResultGroupings(
-                    ResultSpecProto.ResultGrouping.newBuilder()
-                            .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
-        }
-    }
-
-    /**
-     * Adds result groupings for each namespace being queried for.
-     *
-     * <p>This method should be only called in query methods and get the READ lock to keep thread
-     * safety.
-     *
-     * @param resultSpecBuilder ResultSpecs as specified by client
-     * @param prefixes          Prefixes that we should prepend to all our filters
-     * @param maxNumResults     The maximum number of results for each grouping to support.
-     */
-    @GuardedBy("mReadWriteLock")
-    private void addPerNamespaceResultGroupingsLocked(
-            @NonNull ResultSpecProto.Builder resultSpecBuilder,
-            @NonNull Set<String> prefixes, int maxNumResults) {
-        Set<String> existingPrefixes = new ArraySet<>(mNamespaceMapLocked.keySet());
-        existingPrefixes.retainAll(prefixes);
-
-        // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
-        // same as the list of namespaces. If a namespace exists under different packages and/or
-        // different databases, they should still be grouped together.
-        Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
-        for (String prefix : existingPrefixes) {
-            Set<String> prefixedNamespaces = mNamespaceMapLocked.get(prefix);
-            if (prefixedNamespaces == null) {
-                continue;
-            }
-            for (String prefixedNamespace : prefixedNamespaces) {
-                String namespace;
-                try {
-                    namespace = removePrefix(prefixedNamespace);
-                } catch (AppSearchException e) {
-                    // This should never happen. Skip this namespace if it does.
-                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
-                    continue;
-                }
-                List<String> groupedPrefixedNamespaces =
-                        namespaceToPrefixedNamespaces.get(namespace);
-                if (groupedPrefixedNamespaces == null) {
-                    groupedPrefixedNamespaces = new ArrayList<>();
-                    namespaceToPrefixedNamespaces.put(namespace,
-                            groupedPrefixedNamespaces);
-                }
-                groupedPrefixedNamespaces.add(prefixedNamespace);
-            }
-        }
-
-        for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
-            resultSpecBuilder.addResultGroupings(
-                    ResultSpecProto.ResultGrouping.newBuilder()
-                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
-        }
-    }
-
     @VisibleForTesting
     @GuardedBy("mReadWriteLock")
     SchemaProto getSchemaProtoLocked() throws AppSearchException {
@@ -2474,34 +2130,6 @@
         }
     }
 
-    /** Remove the rewritten schema types from any result documents. */
-    @NonNull
-    @VisibleForTesting
-    static SearchResultPage rewriteSearchResultProto(
-            @NonNull SearchResultProto searchResultProto,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
-            throws AppSearchException {
-        // Parallel array of package names for each document search result.
-        List<String> packageNames = new ArrayList<>(searchResultProto.getResultsCount());
-
-        // Parallel array of database names for each document search result.
-        List<String> databaseNames = new ArrayList<>(searchResultProto.getResultsCount());
-
-        SearchResultProto.Builder resultsBuilder = searchResultProto.toBuilder();
-        for (int i = 0; i < searchResultProto.getResultsCount(); i++) {
-            SearchResultProto.ResultProto.Builder resultBuilder =
-                    searchResultProto.getResults(i).toBuilder();
-            DocumentProto.Builder documentBuilder = resultBuilder.getDocument().toBuilder();
-            String prefix = removePrefixesFromDocument(documentBuilder);
-            packageNames.add(getPackageName(prefix));
-            databaseNames.add(getDatabaseName(prefix));
-            resultBuilder.setDocument(documentBuilder);
-            resultsBuilder.setResults(i, resultBuilder);
-        }
-        return SearchResultToProtoConverter.toSearchResultPage(resultsBuilder, packageNames,
-                databaseNames, schemaMap);
-    }
-
     @GuardedBy("mReadWriteLock")
     @VisibleForTesting
     GetOptimizeInfoResultProto getOptimizeInfoResultLocked() {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
index b51959c..a139d3b 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/GenericDocumentToProtoConverter.java
@@ -23,6 +23,7 @@
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.DocumentProto;
+import com.google.android.icing.proto.DocumentProtoOrBuilder;
 import com.google.android.icing.proto.PropertyProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.protobuf.ByteString;
@@ -129,7 +130,7 @@
      *                      that has all empty values.
      */
     @NonNull
-    public static GenericDocument toGenericDocument(@NonNull DocumentProto proto,
+    public static GenericDocument toGenericDocument(@NonNull DocumentProtoOrBuilder proto,
             @NonNull String prefix,
             @NonNull Map<String, SchemaTypeConfigProto> schemaTypeMap) {
         Preconditions.checkNotNull(proto);
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
index a1842e9..de42623 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchResultToProtoConverter.java
@@ -16,7 +16,9 @@
 
 package androidx.appsearch.localstorage.converter;
 
-import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefixesFromDocument;
 
 import android.os.Bundle;
 
@@ -25,16 +27,15 @@
 import androidx.appsearch.app.GenericDocument;
 import androidx.appsearch.app.SearchResult;
 import androidx.appsearch.app.SearchResultPage;
-import androidx.core.util.Preconditions;
+import androidx.appsearch.exceptions.AppSearchException;
 
+import com.google.android.icing.proto.DocumentProto;
 import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.SearchResultProto;
-import com.google.android.icing.proto.SearchResultProtoOrBuilder;
 import com.google.android.icing.proto.SnippetMatchProto;
 import com.google.android.icing.proto.SnippetProto;
 
 import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -51,32 +52,19 @@
      * Translate a {@link SearchResultProto} into {@link SearchResultPage}.
      *
      * @param proto         The {@link SearchResultProto} containing results.
-     * @param packageNames  A parallel array of package names. The package name at index 'i' of
-     *                      this list should be the package that indexed the document at index 'i'
-     *                      of proto.getResults(i).
-     * @param databaseNames A parallel array of database names. The database name at index 'i' of
-     *                      this list shold be the database that indexed the document at index 'i'
-     *                      of proto.getResults(i).
-     * @param schemaMap     A map of prefixes to an inner-map of prefixed schema type to
-     *                      SchemaTypeConfigProtos, used for setting a default value for results
-     *                      with DocumentProtos that have empty values.
+     * @param schemaMap     The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
+     *                      stores all existing prefixed schema type.
      * @return {@link SearchResultPage} of results.
      */
     @NonNull
-    public static SearchResultPage toSearchResultPage(@NonNull SearchResultProtoOrBuilder proto,
-            @NonNull List<String> packageNames, @NonNull List<String> databaseNames,
-            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
-        Preconditions.checkArgument(
-                proto.getResultsCount() == packageNames.size(),
-                "Size of results does not match the number of package names.");
+    public static SearchResultPage toSearchResultPage(@NonNull SearchResultProto proto,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            throws AppSearchException {
         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++) {
-            String prefix = createPrefix(packageNames.get(i), databaseNames.get(i));
-            Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
-            SearchResult result = toSearchResult(
-                    proto.getResults(i), packageNames.get(i), databaseNames.get(i), schemaTypeMap);
+            SearchResult result = toUnprefixedSearchResult(proto.getResults(i), schemaMap);
             resultBundles.add(result.getBundle());
         }
         bundle.putParcelableArrayList(SearchResultPage.RESULTS_FIELD, resultBundles);
@@ -84,28 +72,28 @@
     }
 
     /**
-     * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}.
+     * Translate a {@link SearchResultProto.ResultProto} into {@link SearchResult}. The package and
+     * database prefix will be removed from {@link GenericDocument}.
      *
-     * @param proto                The proto to be converted.
-     * @param packageName          The package name associated with the document in {@code proto}.
-     * @param databaseName         The database name associated with the document in {@code proto}.
-     * @param schemaTypeToProtoMap A map of prefixed schema types to their corresponding
-     *                             SchemaTypeConfigProto, used for setting a default value for
-     *                             results with DocumentProtos that have empty values.
-     * @return A {@link SearchResult} bundle.
+     * @param proto          The proto to be converted.
+     * @param schemaMap      The cached Map of <Prefix, Map<PrefixedSchemaType, schemaProto>>
+     *                       stores all existing prefixed schema type.
+     * @return A {@link SearchResult}.
      */
     @NonNull
-    private static SearchResult toSearchResult(
-            @NonNull SearchResultProto.ResultProtoOrBuilder proto,
-            @NonNull String packageName,
-            @NonNull String databaseName,
-            @NonNull Map<String, SchemaTypeConfigProto> schemaTypeToProtoMap) {
-        String prefix = createPrefix(packageName, databaseName);
+    private static SearchResult toUnprefixedSearchResult(
+            @NonNull SearchResultProto.ResultProto proto,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap)
+            throws AppSearchException {
+
+        DocumentProto.Builder documentBuilder = proto.getDocument().toBuilder();
+        String prefix = removePrefixesFromDocument(documentBuilder);
+        Map<String, SchemaTypeConfigProto> schemaTypeMap = schemaMap.get(prefix);
         GenericDocument document =
-                GenericDocumentToProtoConverter.toGenericDocument(proto.getDocument(), prefix,
-                        schemaTypeToProtoMap);
+                GenericDocumentToProtoConverter.toGenericDocument(documentBuilder, prefix,
+                        schemaTypeMap);
         SearchResult.Builder builder =
-                new SearchResult.Builder(packageName, databaseName)
+                new SearchResult.Builder(getPackageName(prefix), getDatabaseName(prefix))
                         .setGenericDocument(document).setRankingSignal(proto.getScore());
         if (proto.hasSnippet()) {
             for (int i = 0; i < proto.getSnippet().getEntriesCount(); i++) {
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
index abc97f3..af24c98 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/SearchSpecToProtoConverter.java
@@ -16,15 +16,35 @@
 
 package androidx.appsearch.localstorage.converter;
 
+import static androidx.appsearch.localstorage.util.PrefixUtil.createPrefix;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getDatabaseName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.getPackageName;
+import static androidx.appsearch.localstorage.util.PrefixUtil.removePrefix;
+
+import android.util.Log;
+
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.appsearch.app.SearchSpec;
+import androidx.appsearch.exceptions.AppSearchException;
+import androidx.appsearch.localstorage.visibilitystore.VisibilityStore;
+import androidx.collection.ArrayMap;
+import androidx.collection.ArraySet;
 import androidx.core.util.Preconditions;
 
 import com.google.android.icing.proto.ResultSpecProto;
+import com.google.android.icing.proto.SchemaTypeConfigProto;
 import com.google.android.icing.proto.ScoringSpecProto;
 import com.google.android.icing.proto.SearchSpecProto;
 import com.google.android.icing.proto.TermMatchType;
+import com.google.android.icing.proto.TypePropertyMask;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * Translates a {@link SearchSpec} into icing search protos.
@@ -33,18 +53,158 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class SearchSpecToProtoConverter {
-    private SearchSpecToProtoConverter() {
+    private static final String TAG = "AppSearchSearchSpecConv";
+    private final SearchSpec mSearchSpec;
+    private final Set<String> mPrefixes;
+    /** Prefixed namespaces that the client is allowed to query over */
+    private final Set<String> mTargetPrefixedNamespaceFilters = new ArraySet<>();
+    /** Prefixed schemas that the client is allowed to query over */
+    private final Set<String> mTargetPrefixedSchemaFilters = new ArraySet<>();
+
+    /**
+     * Creates a {@link SearchSpecToProtoConverter} for given {@link SearchSpec}.
+     *
+     * @param searchSpec    The spec we need to convert from.
+     * @param prefixes      Set of database prefix which the caller want to access.
+     * @param namespaceMap  The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+     *                      all prefixed namespace filters which are stored in AppSearch.
+     * @param schemaMap     The cached Map of {@code <Prefix, Map<PrefixedSchemaType, schemaProto>>}
+     *                      stores all prefixed schema filters which are stored inAppSearch.
+     */
+    public SearchSpecToProtoConverter(@NonNull SearchSpec searchSpec,
+            @NonNull Set<String> prefixes,
+            @NonNull Map<String, Set<String>> namespaceMap,
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        mSearchSpec = Preconditions.checkNotNull(searchSpec);
+        mPrefixes = Preconditions.checkNotNull(prefixes);
+        Preconditions.checkNotNull(namespaceMap);
+        Preconditions.checkNotNull(schemaMap);
+        generateTargetNamespaceFilters(namespaceMap);
+        if (!mTargetPrefixedNamespaceFilters.isEmpty()) {
+            // Skip generate the target schema filter if the target namespace filter is empty. We
+            // have nothing to search anyway.
+            generateTargetSchemaFilters(schemaMap);
+        }
     }
 
-    /** Extracts {@link SearchSpecProto} information from a {@link SearchSpec}. */
-    @NonNull
-    public static SearchSpecProto toSearchSpecProto(@NonNull SearchSpec spec) {
-        Preconditions.checkNotNull(spec);
-        SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
-                .addAllSchemaTypeFilters(spec.getFilterSchemas())
-                .addAllNamespaceFilters(spec.getFilterNamespaces());
+    /**
+     * Add prefix to the given namespace filters that user want to search over and find the
+     * intersection set with those prefixed namespace candidates that are stored in AppSearch.
+     *
+     * @param namespaceMap   The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+     *                       all prefixed namespace filters which are stored in AppSearch.
+     */
+    private void generateTargetNamespaceFilters(
+            @NonNull Map<String, Set<String>> namespaceMap) {
+        // Convert namespace filters to prefixed namespace filters
+        for (String prefix : mPrefixes) {
+            // Step1: find all prefixed namespace candidates that are stored in AppSearch.
+            Set<String> prefixedNamespaceCandidates = namespaceMap.get(prefix);
+            if (prefixedNamespaceCandidates == null) {
+                // This is should never happen. All prefixes should be verified before reach
+                // here.
+                continue;
+            }
+            // Step2: get the intersection of user searching filters and those candidates which are
+            // stored in AppSearch.
+            getIntersectedFilters(prefix, prefixedNamespaceCandidates,
+                    mSearchSpec.getFilterNamespaces(), mTargetPrefixedNamespaceFilters);
+        }
+    }
 
-        @SearchSpec.TermMatch int termMatchCode = spec.getTermMatch();
+    /**
+     * Add prefix to the given schema filters that user want to search over and find the
+     * intersection set with those prefixed schema candidates that are stored in AppSearch.
+     *
+     * @param schemaMap              The cached Map of
+     *                               <Prefix, Map<PrefixedSchemaType, schemaProto>>
+     *                               stores all prefixed schema filters which are stored in
+     *                               AppSearch.
+     */
+    private void generateTargetSchemaFilters(
+            @NonNull Map<String, Map<String, SchemaTypeConfigProto>> schemaMap) {
+        // Append prefix to input schema filters and get the intersection of existing schema filter.
+        for (String prefix : mPrefixes) {
+            // Step1: find all prefixed schema candidates that are stored in AppSearch.
+            Map<String, SchemaTypeConfigProto> prefixedSchemaMap = schemaMap.get(prefix);
+            if (prefixedSchemaMap == null) {
+                // This is should never happen. All prefixes should be verified before reach
+                // here.
+                continue;
+            }
+            Set<String> prefixedSchemaCandidates = prefixedSchemaMap.keySet();
+            // Step2: get the intersection of user searching filters and those candidates which are
+            // stored in AppSearch.
+            getIntersectedFilters(prefix, prefixedSchemaCandidates, mSearchSpec.getFilterSchemas(),
+                    mTargetPrefixedSchemaFilters);
+        }
+    }
+
+    /**
+     * @return whether this search's target filters are empty. If any target filter is empty, we
+     * should skip send request to Icing.
+     */
+    public boolean isNothingToSearch() {
+        return mTargetPrefixedNamespaceFilters.isEmpty() || mTargetPrefixedSchemaFilters.isEmpty();
+    }
+
+    /**
+     * For each target schema, we will check visibility store is that accessible to the caller. And
+     * remove this schemas if it is not allowed for caller to query.
+     *
+     * @param callerPackageName            The package name of caller
+     * @param visibilityStore              The visibility store which holds all visibility settings.
+     *                                     If you pass null, all schemas that don't belong to the
+     *                                     caller package will be removed.
+     * @param callerUid                    The uid of the caller.
+     * @param callerHasSystemAccess        Whether the caller has system access.
+     */
+    public void removeInaccessibleSchemaFilter(@NonNull String callerPackageName,
+            @Nullable VisibilityStore visibilityStore,
+            int callerUid,
+            boolean callerHasSystemAccess) {
+        Iterator<String> targetPrefixedSchemaFilterIterator =
+                mTargetPrefixedSchemaFilters.iterator();
+        while (targetPrefixedSchemaFilterIterator.hasNext()) {
+            String targetPrefixedSchemaFilter = targetPrefixedSchemaFilterIterator.next();
+            String packageName = getPackageName(targetPrefixedSchemaFilter);
+
+            boolean allow;
+            if (packageName.equals(callerPackageName)) {
+                // Callers can always retrieve their own data
+                allow = true;
+            } else if (visibilityStore == null) {
+                // If there's no visibility store, there's no extra access
+                allow = false;
+            } else {
+                String databaseName = getDatabaseName(targetPrefixedSchemaFilter);
+                allow = visibilityStore.isSchemaSearchableByCaller(packageName, databaseName,
+                        targetPrefixedSchemaFilter, callerUid, callerHasSystemAccess);
+            }
+            if (!allow) {
+                targetPrefixedSchemaFilterIterator.remove();
+            }
+        }
+    }
+
+    /**
+     * Extracts {@link SearchSpecProto} information from a {@link SearchSpec}.
+     *
+     * @param queryExpression                Query String to search.
+     */
+    @NonNull
+    public SearchSpecProto toSearchSpecProto(
+            @NonNull String queryExpression) {
+        Preconditions.checkNotNull(queryExpression);
+
+        // set query to SearchSpecProto and override schema and namespace filter by
+        // targetPrefixedFilters which is
+        SearchSpecProto.Builder protoBuilder = SearchSpecProto.newBuilder()
+                .setQuery(queryExpression)
+                .addAllNamespaceFilters(mTargetPrefixedNamespaceFilters)
+                .addAllSchemaTypeFilters(mTargetPrefixedSchemaFilters);
+
+        @SearchSpec.TermMatch int termMatchCode = mSearchSpec.getTermMatch();
         TermMatchType.Code termMatchCodeProto = TermMatchType.Code.forNumber(termMatchCode);
         if (termMatchCodeProto == null || termMatchCodeProto.equals(TermMatchType.Code.UNKNOWN)) {
             throw new IllegalArgumentException("Invalid term match type: " + termMatchCode);
@@ -54,35 +214,73 @@
         return protoBuilder.build();
     }
 
-    /** Extracts {@link ResultSpecProto} information from a {@link SearchSpec}. */
+    /**
+     * Extracts {@link ResultSpecProto} information from a {@link SearchSpec}.
+     *
+     * @param namespaceMap    The cached Map of {@code <Prefix, Set<PrefixedNamespace>>} stores
+     *                        all existing prefixed namespace.
+     */
     @NonNull
-    public static ResultSpecProto toResultSpecProto(@NonNull SearchSpec spec) {
-        Preconditions.checkNotNull(spec);
-        return ResultSpecProto.newBuilder()
-                .setNumPerPage(spec.getResultCountPerPage())
+    public ResultSpecProto toResultSpecProto(
+            @NonNull Map<String, Set<String>> namespaceMap) {
+        ResultSpecProto.Builder resultSpecBuilder = ResultSpecProto.newBuilder()
+                .setNumPerPage(mSearchSpec.getResultCountPerPage())
                 .setSnippetSpec(
                         ResultSpecProto.SnippetSpecProto.newBuilder()
-                                .setNumToSnippet(spec.getSnippetCount())
-                                .setNumMatchesPerProperty(spec.getSnippetCountPerProperty())
-                                .setMaxWindowBytes(spec.getMaxSnippetSize()))
-                .addAllTypePropertyMasks(TypePropertyPathToProtoConverter.toTypePropertyMaskList(
-                        spec.getProjections())).build();
+                                .setNumToSnippet(mSearchSpec.getSnippetCount())
+                                .setNumMatchesPerProperty(mSearchSpec.getSnippetCountPerProperty())
+                                .setMaxWindowBytes(mSearchSpec.getMaxSnippetSize()));
+
+        // Rewrites the typePropertyMasks that exist in {@code prefixes}.
+        int groupingType = mSearchSpec.getResultGroupingTypeFlags();
+        if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0
+                && (groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerPackagePerNamespaceResultGroupings(mPrefixes,
+                    mSearchSpec.getResultGroupingLimit(),
+                    namespaceMap, resultSpecBuilder);
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_PACKAGE) != 0) {
+            addPerPackageResultGroupings(mPrefixes, mSearchSpec.getResultGroupingLimit(),
+                    namespaceMap, resultSpecBuilder);
+        } else if ((groupingType & SearchSpec.GROUPING_TYPE_PER_NAMESPACE) != 0) {
+            addPerNamespaceResultGroupings(mPrefixes, mSearchSpec.getResultGroupingLimit(),
+                    namespaceMap, resultSpecBuilder);
+        }
+
+        List<TypePropertyMask.Builder> typePropertyMaskBuilders =
+                TypePropertyPathToProtoConverter
+                        .toTypePropertyMaskBuilderList(mSearchSpec.getProjections());
+        // Rewrite filters to include a database prefix.
+        resultSpecBuilder.clearTypePropertyMasks();
+        for (int i = 0; i < typePropertyMaskBuilders.size(); i++) {
+            String unprefixedType = typePropertyMaskBuilders.get(i).getSchemaType();
+            boolean isWildcard =
+                    unprefixedType.equals(SearchSpec.PROJECTION_SCHEMA_TYPE_WILDCARD);
+            // Qualify the given schema types
+            for (String prefix : mPrefixes) {
+                String prefixedType = isWildcard ? unprefixedType : prefix + unprefixedType;
+                if (isWildcard || mTargetPrefixedSchemaFilters.contains(prefixedType)) {
+                    resultSpecBuilder.addTypePropertyMasks(typePropertyMaskBuilders.get(i)
+                            .setSchemaType(prefixedType).build());
+                }
+            }
+        }
+
+        return resultSpecBuilder.build();
     }
 
     /** Extracts {@link ScoringSpecProto} information from a {@link SearchSpec}. */
     @NonNull
-    public static ScoringSpecProto toScoringSpecProto(@NonNull SearchSpec spec) {
-        Preconditions.checkNotNull(spec);
+    public ScoringSpecProto toScoringSpecProto() {
         ScoringSpecProto.Builder protoBuilder = ScoringSpecProto.newBuilder();
 
-        @SearchSpec.Order int orderCode = spec.getOrder();
+        @SearchSpec.Order int orderCode = mSearchSpec.getOrder();
         ScoringSpecProto.Order.Code orderCodeProto =
                 ScoringSpecProto.Order.Code.forNumber(orderCode);
         if (orderCodeProto == null) {
             throw new IllegalArgumentException("Invalid result ranking order: " + orderCode);
         }
         protoBuilder.setOrderBy(orderCodeProto).setRankBy(
-                toProtoRankingStrategy(spec.getRankingStrategy()));
+                toProtoRankingStrategy(mSearchSpec.getRankingStrategy()));
 
         return protoBuilder.build();
     }
@@ -111,4 +309,174 @@
                         + rankingStrategyCode);
         }
     }
+
+
+    /**
+     * Adds result groupings for each namespace in each package being queried for.
+     *
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     * @param namespaceMap      The namespace map contains all prefixed existing namespaces.
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     */
+    private static void addPerPackagePerNamespaceResultGroupings(
+            @NonNull Set<String> prefixes,
+            int maxNumResults,
+            @NonNull Map<String, Set<String>> namespaceMap,
+            @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+        // Create a map for package+namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If one package has multiple databases, each with the same
+        // namespace, then those should be grouped together.
+        Map<String, List<String>> packageAndNamespaceToNamespaces = new ArrayMap<>();
+        for (String prefix : prefixes) {
+            Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            String packageName = getPackageName(prefix);
+            // Create a new prefix without the database name. This will allow us to group namespaces
+            // that have the same name and package but a different database name together.
+            String emptyDatabasePrefix = createPrefix(packageName, /*databaseName*/"");
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                String emptyDatabasePrefixedNamespace = emptyDatabasePrefix + namespace;
+                List<String> namespaceList =
+                        packageAndNamespaceToNamespaces.get(emptyDatabasePrefixedNamespace);
+                if (namespaceList == null) {
+                    namespaceList = new ArrayList<>();
+                    packageAndNamespaceToNamespaces.put(emptyDatabasePrefixedNamespace,
+                            namespaceList);
+                }
+                namespaceList.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : packageAndNamespaceToNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each package being queried for.
+     *
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     * @param namespaceMap      The namespace map contains all prefixed existing namespaces.
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     */
+    private static void addPerPackageResultGroupings(
+            @NonNull Set<String> prefixes,
+            int maxNumResults,
+            @NonNull Map<String, Set<String>> namespaceMap,
+            @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+        // Build up a map of package to namespaces.
+        Map<String, List<String>> packageToNamespacesMap = new ArrayMap<>();
+        for (String prefix : prefixes) {
+            Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            String packageName = getPackageName(prefix);
+            List<String> packageNamespaceList = packageToNamespacesMap.get(packageName);
+            if (packageNamespaceList == null) {
+                packageNamespaceList = new ArrayList<>();
+                packageToNamespacesMap.put(packageName, packageNamespaceList);
+            }
+            packageNamespaceList.addAll(prefixedNamespaces);
+        }
+
+        for (List<String> prefixedNamespaces : packageToNamespacesMap.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(prefixedNamespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Adds result groupings for each namespace being queried for.
+     *
+     * @param prefixes          Prefixes that we should prepend to all our filters
+     * @param maxNumResults     The maximum number of results for each grouping to support.
+     * @param namespaceMap      The namespace map contains all prefixed existing namespaces.
+     * @param resultSpecBuilder ResultSpecs as specified by client
+     */
+    private static void addPerNamespaceResultGroupings(
+            @NonNull Set<String> prefixes,
+            int maxNumResults,
+            @NonNull Map<String, Set<String>> namespaceMap,
+            @NonNull ResultSpecProto.Builder resultSpecBuilder) {
+        // Create a map of namespace to prefixedNamespaces. This is NOT necessarily the
+        // same as the list of namespaces. If a namespace exists under different packages and/or
+        // different databases, they should still be grouped together.
+        Map<String, List<String>> namespaceToPrefixedNamespaces = new ArrayMap<>();
+        for (String prefix : prefixes) {
+            Set<String> prefixedNamespaces = namespaceMap.get(prefix);
+            if (prefixedNamespaces == null) {
+                continue;
+            }
+            for (String prefixedNamespace : prefixedNamespaces) {
+                String namespace;
+                try {
+                    namespace = removePrefix(prefixedNamespace);
+                } catch (AppSearchException e) {
+                    // This should never happen. Skip this namespace if it does.
+                    Log.e(TAG, "Prefixed namespace " + prefixedNamespace + " is malformed.");
+                    continue;
+                }
+                List<String> groupedPrefixedNamespaces =
+                        namespaceToPrefixedNamespaces.get(namespace);
+                if (groupedPrefixedNamespaces == null) {
+                    groupedPrefixedNamespaces = new ArrayList<>();
+                    namespaceToPrefixedNamespaces.put(namespace,
+                            groupedPrefixedNamespaces);
+                }
+                groupedPrefixedNamespaces.add(prefixedNamespace);
+            }
+        }
+
+        for (List<String> namespaces : namespaceToPrefixedNamespaces.values()) {
+            resultSpecBuilder.addResultGroupings(
+                    ResultSpecProto.ResultGrouping.newBuilder()
+                            .addAllNamespaces(namespaces).setMaxResults(maxNumResults));
+        }
+    }
+
+    /**
+     * Find the intersection set of candidates existing in AppSearch and user specified filters.
+     *
+     * @param prefix                   The package and database's identifier.
+     * @param prefixedCandidates       The set contains all prefixed candidates which are existing
+     *                                 in a database.
+     * @param inputFilters             The set contains all desired but un-prefixed filters of user.
+     * @param prefixedTargetFilters    The output set contains all desired prefixed filters which
+     *                                 are existing in the database.
+     */
+    private static void getIntersectedFilters(
+            @NonNull String prefix,
+            @NonNull Set<String> prefixedCandidates,
+            @NonNull List<String> inputFilters,
+            @NonNull Set<String> prefixedTargetFilters) {
+        if (inputFilters.isEmpty()) {
+            // Client didn't specify certain schemas to search over, add all candidates.
+            prefixedTargetFilters.addAll(prefixedCandidates);
+        } else {
+            // Client specified some filters to search over, check and only add those are
+            // existing in the database.
+            for (int i = 0; i < inputFilters.size(); i++) {
+                String prefixedTargetFilter = prefix + inputFilters.get(i);
+                if (prefixedCandidates.contains(prefixedTargetFilter)) {
+                    prefixedTargetFilters.add(prefixedTargetFilter);
+                }
+            }
+        }
+    }
 }
diff --git a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
index 98f5642..1e81145 100644
--- a/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
+++ b/appsearch/appsearch-local-storage/src/main/java/androidx/appsearch/localstorage/converter/TypePropertyPathToProtoConverter.java
@@ -37,15 +37,16 @@
 
     /** Extracts {@link TypePropertyMask} information from a {@link Map}. */
     @NonNull
-    public static List<TypePropertyMask> toTypePropertyMaskList(@NonNull Map<String,
-            List<String>> typePropertyPaths) {
+    public static List<TypePropertyMask.Builder> toTypePropertyMaskBuilderList(
+            @NonNull Map<String, List<String>> typePropertyPaths) {
         Preconditions.checkNotNull(typePropertyPaths);
-        List<TypePropertyMask> typePropertyMasks = new ArrayList<>(typePropertyPaths.size());
+        List<TypePropertyMask.Builder> typePropertyMaskBuilders =
+                new ArrayList<>(typePropertyPaths.size());
         for (Map.Entry<String, List<String>> e : typePropertyPaths.entrySet()) {
-            typePropertyMasks.add(
+            typePropertyMaskBuilders.add(
                     TypePropertyMask.newBuilder().setSchemaType(
-                            e.getKey()).addAllPaths(e.getValue()).build());
+                            e.getKey()).addAllPaths(e.getValue()));
         }
-        return typePropertyMasks;
+        return typePropertyMaskBuilders;
     }
 }
diff --git a/benchmark/benchmark-macro-junit4/build.gradle b/benchmark/benchmark-macro-junit4/build.gradle
index 75fe717..75bc6d4 100644
--- a/benchmark/benchmark-macro-junit4/build.gradle
+++ b/benchmark/benchmark-macro-junit4/build.gradle
@@ -42,7 +42,6 @@
     implementation("androidx.test.uiautomator:uiautomator:2.2.0")
 
     androidTestImplementation(project(":internal-testutils-ktx"))
-    androidTestImplementation(project(":tracing:tracing-ktx"))
     androidTestImplementation("androidx.test:rules:1.3.0")
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
index 58ced3f..1bec5eb 100644
--- a/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
+++ b/benchmark/benchmark-macro-junit4/src/main/java/androidx/benchmark/macro/junit4/MacrobenchmarkRule.kt
@@ -53,7 +53,7 @@
      * @param packageName Package name of the app being measured.
      * @param metrics List of metrics to measure.
      * @param compilationMode Mode of compilation used before capturing measurement, such as
-     * [CompilationMode.SpeedProfile].
+     * [CompilationMode.Partial], defaults to [CompilationMode.DEFAULT].
      * @param startupMode Optional mode to force app launches performed with
      * [MacrobenchmarkScope.startActivityAndWait] (and similar variants) to be of the assigned
      * type. For example, `COLD` launches kill the process before the measureBlock, to ensure
@@ -70,7 +70,7 @@
     public fun measureRepeated(
         packageName: String,
         metrics: List<Metric>,
-        compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+        compilationMode: CompilationMode = CompilationMode.DEFAULT,
         startupMode: StartupMode? = null,
         @IntRange(from = 1)
         iterations: Int,
diff --git a/benchmark/benchmark-macro/api/current.txt b/benchmark/benchmark-macro/api/current.txt
index 6327890..0d971ac 100644
--- a/benchmark/benchmark-macro/api/current.txt
+++ b/benchmark/benchmark-macro/api/current.txt
@@ -4,27 +4,52 @@
   @RequiresApi(29) public final class Api29Kt {
   }
 
+  public enum BaselineProfileMode {
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+  }
+
   public final class BaselineProfilesKt {
   }
 
   public abstract sealed class CompilationMode {
+    field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+    field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
   }
 
-  public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+  @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
   }
 
-  public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+  public static final class CompilationMode.Companion {
   }
 
-  public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+  public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Full();
   }
 
-  public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+  @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.None();
+  }
+
+  @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+    ctor public CompilationMode.Partial();
+    method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
     method public int getWarmupIterations();
+    property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+    property public final int warmupIterations;
+  }
+
+  @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+  }
+
+  @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+    ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+    method @Deprecated public int getWarmupIterations();
     property public final int warmupIterations;
   }
 
diff --git a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
index 35e822e..4e4fde3 100644
--- a/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
+++ b/benchmark/benchmark-macro/api/public_plus_experimental_current.txt
@@ -4,27 +4,52 @@
   @RequiresApi(29) public final class Api29Kt {
   }
 
+  public enum BaselineProfileMode {
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+  }
+
   public final class BaselineProfilesKt {
   }
 
   public abstract sealed class CompilationMode {
+    field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+    field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
   }
 
-  public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+  @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
   }
 
-  public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+  public static final class CompilationMode.Companion {
   }
 
-  public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+  public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Full();
   }
 
-  public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+  @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.None();
+  }
+
+  @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+    ctor public CompilationMode.Partial();
+    method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
     method public int getWarmupIterations();
+    property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+    property public final int warmupIterations;
+  }
+
+  @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+  }
+
+  @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+    ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+    method @Deprecated public int getWarmupIterations();
     property public final int warmupIterations;
   }
 
diff --git a/benchmark/benchmark-macro/api/restricted_current.txt b/benchmark/benchmark-macro/api/restricted_current.txt
index ee02155..3292f02 100644
--- a/benchmark/benchmark-macro/api/restricted_current.txt
+++ b/benchmark/benchmark-macro/api/restricted_current.txt
@@ -4,31 +4,56 @@
   @RequiresApi(29) public final class Api29Kt {
   }
 
+  public enum BaselineProfileMode {
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Disable;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode Require;
+    enum_constant public static final androidx.benchmark.macro.BaselineProfileMode UseIfAvailable;
+  }
+
   public final class BaselineProfilesKt {
-    method @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
+    method @RequiresApi(28) @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static void collectBaselineProfile(String uniqueName, String packageName, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> setupBlock, kotlin.jvm.functions.Function1<? super androidx.benchmark.macro.MacrobenchmarkScope,kotlin.Unit> profileBlock);
   }
 
   public abstract sealed class CompilationMode {
+    field public static final androidx.benchmark.macro.CompilationMode.Companion Companion;
+    field public static final androidx.benchmark.macro.CompilationMode DEFAULT;
   }
 
-  public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+  @Deprecated public static final class CompilationMode.BaselineProfile extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.BaselineProfile INSTANCE;
+  }
+
+  public static final class CompilationMode.Companion {
+  }
+
+  public static final class CompilationMode.Full extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Full();
   }
 
   @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public static final class CompilationMode.Interpreted extends androidx.benchmark.macro.CompilationMode {
   }
 
-  public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.None INSTANCE;
+  @RequiresApi(24) public static final class CompilationMode.None extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.None();
   }
 
-  public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
-    field public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
-  }
-
-  public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
-    ctor public CompilationMode.SpeedProfile(optional int warmupIterations);
+  @RequiresApi(24) public static final class CompilationMode.Partial extends androidx.benchmark.macro.CompilationMode {
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode, optional @IntRange(from=0) int warmupIterations);
+    ctor public CompilationMode.Partial(optional androidx.benchmark.macro.BaselineProfileMode baselineProfileMode);
+    ctor public CompilationMode.Partial();
+    method public androidx.benchmark.macro.BaselineProfileMode getBaselineProfileMode();
     method public int getWarmupIterations();
+    property public final androidx.benchmark.macro.BaselineProfileMode baselineProfileMode;
+    property public final int warmupIterations;
+  }
+
+  @Deprecated public static final class CompilationMode.Speed extends androidx.benchmark.macro.CompilationMode {
+    field @Deprecated public static final androidx.benchmark.macro.CompilationMode.Speed INSTANCE;
+  }
+
+  @Deprecated public static final class CompilationMode.SpeedProfile extends androidx.benchmark.macro.CompilationMode {
+    ctor @Deprecated public CompilationMode.SpeedProfile(optional int warmupIterations);
+    method @Deprecated public int getWarmupIterations();
     property public final int warmupIterations;
   }
 
diff --git a/benchmark/benchmark-macro/build.gradle b/benchmark/benchmark-macro/build.gradle
index 13147d6..d16e515 100644
--- a/benchmark/benchmark-macro/build.gradle
+++ b/benchmark/benchmark-macro/build.gradle
@@ -48,7 +48,7 @@
 
     implementation(project(":benchmark:benchmark-common"))
     implementation("androidx.profileinstaller:profileinstaller:1.0.3")
-    implementation(project(":tracing:tracing-ktx"))
+    implementation("androidx.tracing:tracing-ktx:1.1.0-beta01")
     implementation(libs.testCore)
     implementation(libs.testUiautomator)
 
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
index a09cfeb..4cafd89 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/CompilationModeTest.kt
@@ -16,8 +16,10 @@
 
 package androidx.benchmark.macro
 
+import android.os.Build
 import androidx.benchmark.Shell
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertFalse
@@ -26,10 +28,12 @@
 import org.junit.Assume.assumeTrue
 import org.junit.Test
 import org.junit.runner.RunWith
+import kotlin.test.assertFailsWith
 
+@Suppress("DEPRECATION")
 @RunWith(AndroidJUnit4::class)
 @SmallTest
-public class CompilationModeTest {
+class CompilationModeTest {
     private val vmRunningInterpretedOnly: Boolean
 
     init {
@@ -37,33 +41,76 @@
         vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
     }
 
+    @SdkSuppress(minSdkVersion = 24)
     @Test
-    public fun names() {
+    fun partial() {
+        assertFailsWith<IllegalArgumentException> { // can't ignore with 0 iters
+            CompilationMode.Partial(BaselineProfileMode.Disable, warmupIterations = 0)
+        }
+        assertFailsWith<java.lang.IllegalArgumentException> { // can't set negative iters
+            CompilationMode.Partial(BaselineProfileMode.Require, warmupIterations = -1)
+        }
+    }
+
+    @Test
+    fun names() {
         // We test these names, as they're likely built into parameterized
         // test strings, so stability/brevity are important
-        assertEquals("None", CompilationMode.None.toString())
-        assertEquals("SpeedProfile(iterations=123)", CompilationMode.SpeedProfile(123).toString())
-        assertEquals("Speed", CompilationMode.Speed.toString())
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            assertEquals("None", CompilationMode.None().toString())
+            assertEquals("BaselineProfile", CompilationMode.Partial().toString())
+            assertEquals(
+                "WarmupProfile(iterations=3)",
+                CompilationMode.Partial(
+                    BaselineProfileMode.Disable,
+                    warmupIterations = 3
+                ).toString()
+            )
+            assertEquals(
+                "Partial(baselineProfile=Require,iterations=3)",
+                CompilationMode.Partial(warmupIterations = 3).toString()
+            )
+            assertEquals("Full", CompilationMode.Full().toString())
+        }
         assertEquals("Interpreted", CompilationMode.Interpreted.toString())
+
+        // deprecated
+        assertEquals(
+            "SpeedProfile(iterations=123)",
+            CompilationMode.SpeedProfile(123).toString()
+        )
+        assertEquals("Speed", CompilationMode.Speed.toString())
     }
 
     @Test
-    public fun isSupportedWithVmSettings_jitEnabled() {
+    fun isSupportedWithVmSettings_jitEnabled() {
         assumeFalse(vmRunningInterpretedOnly)
 
-        assertTrue(CompilationMode.None.isSupportedWithVmSettings())
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            assertTrue(CompilationMode.None().isSupportedWithVmSettings())
+            assertTrue(CompilationMode.Partial().isSupportedWithVmSettings())
+            assertTrue(CompilationMode.Full().isSupportedWithVmSettings())
+        }
+        assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
+
+        // deprecated
         assertTrue(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
         assertTrue(CompilationMode.Speed.isSupportedWithVmSettings())
-        assertFalse(CompilationMode.Interpreted.isSupportedWithVmSettings())
     }
 
     @Test
-    public fun isSupportedWithVmSettings_jitDisabled() {
+    fun isSupportedWithVmSettings_jitDisabled() {
         assumeTrue(vmRunningInterpretedOnly)
 
-        assertFalse(CompilationMode.None.isSupportedWithVmSettings())
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            assertFalse(CompilationMode.None().isSupportedWithVmSettings())
+            assertFalse(CompilationMode.Partial().isSupportedWithVmSettings())
+            assertFalse(CompilationMode.Full().isSupportedWithVmSettings())
+        }
+        assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
+
+        // deprecated
         assertFalse(CompilationMode.SpeedProfile().isSupportedWithVmSettings())
         assertFalse(CompilationMode.Speed.isSupportedWithVmSettings())
-        assertTrue(CompilationMode.Interpreted.isSupportedWithVmSettings())
     }
 }
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
index d60f101..ffac1fa 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkScopeTest.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.os.Build
+import androidx.annotation.RequiresApi
 import androidx.benchmark.Shell
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
@@ -40,7 +41,6 @@
 @LargeTest
 class MacrobenchmarkScopeTest {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
-    private val device = UiDevice.getInstance(instrumentation)
 
     @Before
     fun setup() {
@@ -66,14 +66,17 @@
         assertFalse(Shell.isPackageAlive(Packages.TARGET))
     }
 
-    @SdkSuppress(minSdkVersion = 24) // TODO: define behavior for older platforms
+    @SdkSuppress(minSdkVersion = 24)
     @Test
     fun compile_speedProfile() {
         val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
         val iterations = 1
         var executions = 0
-        val compilation = CompilationMode.SpeedProfile(warmupIterations = iterations)
-        compilation.compile(Packages.TARGET) {
+        val compilation = CompilationMode.Partial(
+            baselineProfileMode = BaselineProfileMode.Disable,
+            warmupIterations = iterations
+        )
+        compilation.resetAndCompile(Packages.TARGET) {
             executions += 1
             scope.pressHome()
             scope.startActivityAndWait()
@@ -81,11 +84,10 @@
         assertEquals(iterations, executions)
     }
 
-    @SdkSuppress(minSdkVersion = 24) // TODO: define behavior for older platforms
     @Test
-    fun compile_speed() {
-        val compilation = CompilationMode.Speed
-        compilation.compile(Packages.TARGET) {
+    fun compile_full() {
+        val compilation = CompilationMode.Full()
+        compilation.resetAndCompile(Packages.TARGET) {
             fail("Should never be called for $compilation")
         }
     }
@@ -119,6 +121,7 @@
         }
     }
 
+    @RequiresApi(24) // Test flakes locally on API 23, appears to be UiAutomation issue
     @Test
     fun startActivityAndWait_invalidActivity() {
         val scope = MacrobenchmarkScope(Packages.TARGET, launchWithClearTask = true)
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
index c7b5398..de2131e 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/MacrobenchmarkTest.kt
@@ -42,7 +42,7 @@
                 testName = "testName",
                 packageName = "com.ignored",
                 metrics = emptyList(), // invalid
-                compilationMode = CompilationMode.None,
+                compilationMode = CompilationMode.noop,
                 iterations = 1,
                 startupMode = null,
                 setupBlock = {},
@@ -61,7 +61,7 @@
                 testName = "testName",
                 packageName = "com.ignored",
                 metrics = listOf(FrameTimingMetric()),
-                compilationMode = CompilationMode.None,
+                compilationMode = CompilationMode.noop,
                 iterations = 0, // invalid
                 startupMode = null,
                 setupBlock = {},
@@ -87,7 +87,7 @@
             testName = "validateCallbackBehavior",
             packageName = Packages.TARGET,
             metrics = listOf(TraceSectionMetric(TRACE_LABEL)),
-            compilationMode = CompilationMode.None,
+            compilationMode = CompilationMode.DEFAULT,
             iterations = 2,
             startupMode = startupMode,
             setupBlock = {
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt
new file mode 100644
index 0000000..66626cc
--- /dev/null
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfileMode.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.benchmark.macro
+
+/**
+ * Choice of how the Baseline Profile in a target application should be included or ignored during pre-compilation.
+ */
+enum class BaselineProfileMode {
+    /**
+     * Require the BaselineProfile methods/classes from the target app to be pre-compiled.
+     *
+     * If the ProfileInstaller library or Baseline Profile isn't present in the target app, an
+     * exception will be thrown at compilation time.
+     */
+    Require,
+
+    /**
+     * Include the BaselineProfile methods/classes from the target app into the compilation step if
+     * a Baseline Profile and the ProfileInstaller library are both present in the target.
+     *
+     * This is the same as [Require], except it logs instead of throwing when the
+     * Baseline Profile or ProfileInstaller library aren't present in the target application.
+     */
+    UseIfAvailable,
+
+    /**
+     * Do not include the Baseline Profile, if present, in the compilation of the target app.
+     */
+    Disable
+}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
index 20694f0..1712f72 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/BaselineProfiles.kt
@@ -18,6 +18,7 @@
 
 import android.os.Build
 import android.util.Log
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.benchmark.InstrumentationResults
 import androidx.benchmark.Outputs
@@ -30,6 +31,7 @@
  * @suppress
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+@RequiresApi(28)
 fun collectBaselineProfile(
     uniqueName: String,
     packageName: String,
@@ -46,14 +48,21 @@
     }
 
     val startTime = System.nanoTime()
-    val scope = MacrobenchmarkScope(packageName, /* launchWithClearTask */ true)
-    val speedProfile = CompilationMode.SpeedProfile(warmupIterations = 3)
+    val scope = MacrobenchmarkScope(packageName, launchWithClearTask = true)
+
+    // Ignore because we're *creating* a baseline profile, not using it yet
+    val compilationMode = CompilationMode.Partial(
+        baselineProfileMode = BaselineProfileMode.Disable,
+        warmupIterations = 3
+    )
 
     // always kill the process at beginning of a collection.
     scope.killProcess()
     try {
         userspaceTrace("compile $packageName") {
-            speedProfile.compile(packageName) {
+            compilationMode.resetAndCompile(
+                packageName = packageName
+            ) {
                 setupBlock(scope)
                 profileBlock(scope)
             }
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
index 19cc93a..135615c 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/CompilationMode.kt
@@ -18,11 +18,11 @@
 
 import android.os.Build
 import android.util.Log
+import androidx.annotation.IntRange
+import androidx.annotation.RequiresApi
 import androidx.annotation.RestrictTo
 import androidx.benchmark.DeviceInfo
 import androidx.benchmark.Shell
-import androidx.benchmark.macro.CompilationMode.BaselineProfile
-import androidx.benchmark.macro.CompilationMode.SpeedProfile
 import androidx.profileinstaller.ProfileInstallReceiver
 import androidx.profileinstaller.ProfileInstaller
 import org.junit.AssumptionViolatedException
@@ -34,42 +34,225 @@
  * with the next. This compilation mode dictates any pre-compilation that occurs before repeatedly
  * running the setup / measure blocks of the benchmark.
  *
- * If [SpeedProfile] is used, the following occur before measurement:
- * 1. Compilation is reset
- * 2. The setup / measure loop will be run a configurable number of profiling iterations to capture
- * a profile
- * 3. The app is compiled with `cmd package compile -f -m speed-profile <package>`
+ * On Android N+ (API 24+), there are different levels of compilation supported:
  *
- * * [None] skips steps 2 and 3 above.
- * * [BaselineProfile] has an alternate implementation of 2, where it installs profile information
- * bundled within the APK.
- * * [Speed] skips 2 (since it AOT compiles the full app), and uses `speed` instead of
- * `speed-profile` for 3.
+ * * [Partial] - the default configuration of [Partial] will partially pre-compile your application,
+ * if a Baseline Profile is included in your app. This represents the most realistic fresh-install
+ * experience on an end-user's device. You can additionally or instead use
+ * [Partial.warmupIterations] to use Profile Guided Optimization, using the benchmark content to
+ * guide pre-compilation to mimic an application's performance after some, and JIT-ing has occurred.
  *
- * While some of these modes directly map to
+ * * [Full] - the app is fully pre-compiled. This is generally not representative of real user
+ * experience, as apps are not fully pre-compiled on user devices, but this can be used to either
+ * illustrate ideal performance, or to reduce noise/inconsistency from just-in-time compilation
+ * while the benchmark runs.
+ *
+ * * [None] - the app isn't pre-compiled at all, bypassing the default compilation that should
+ * generally be done at install time, e.g. by the Play Store. This will illustrate worst case
+ * performance, and will show you performance of your app if you do not enable baseline profiles,
+ * useful for judging the performance impact of the baseline profiles included in your application.
+ *
+ * On Android M (API 23), only [Full] is supported, as all apps are always fully compiled.
+ *
+ * To understand more how these modes work, you can see comments for each class, and also see the
  * [Android Runtime compilation modes](https://source.android.com/devices/tech/dalvik/configure#compilation_options)
- * (which can be passed to
- * [`cmd compile`](https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package)),
- * there isn't a direct mapping.
+ * (which are passed by benchmark into
+ * [`cmd compile`](https://source.android.com/devices/tech/dalvik/jit-compiler#force-compilation-of-a-specific-package)
+ * to compile the target app).
  */
-public sealed class CompilationMode(
-    // for modes other than [None], is argument passed `cmd package compile`
-    private val compileArgument: String?
-) {
-    internal fun compileArgument(): String {
-        if (compileArgument == null) {
-            throw UnsupportedOperationException("No compileArgument for mode $this")
-        }
-        return compileArgument
+sealed class CompilationMode {
+    internal fun resetAndCompile(packageName: String, warmupBlock: () -> Unit) {
+        Log.d(TAG, "Clearing profiles for $packageName")
+        Shell.executeCommand("cmd package compile --reset $packageName")
+        compileImpl(packageName, warmupBlock)
     }
 
+    internal fun cmdPackageCompile(packageName: String, compileArgument: String) {
+        Shell.executeCommand("cmd package compile -f -m $compileArgument $packageName")
+    }
+
+    internal abstract fun compileImpl(packageName: String, warmupBlock: () -> Unit)
+
     /**
      * No pre-compilation - entire app will be allowed to Just-In-Time compile as it runs.
      *
      * Note that later iterations may perform differently, as app code is jitted.
      */
-    public object None : CompilationMode(null) {
-        public override fun toString(): String = "None"
+    // Leaving possibility for future configuration (such as interpreted = true)
+    @Suppress("CanSealedSubClassBeObject")
+    @RequiresApi(24)
+    class None : CompilationMode() {
+        override fun toString(): String = "None"
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            // nothing to do!
+        }
+    }
+
+    /**
+     * Partial ahead-of-time app compilation.
+     *
+     * The default parameters for this mimic the default state of an app partially pre-compiled by
+     * the installer - such as via Google Play.
+     *
+     * Either [baselineProfileMode] must be set to non-[BaselineProfileMode.Disable], or
+     * [warmupIterations] must be set to a non-`0` value.
+     *
+     * Note: `[baselineProfileMode] = [BaselineProfileMode.Require]` is only supported for APKs that
+     * have the ProfileInstaller library included, and have been built by AGP 7.0+ to package the
+     * baseline profile in the APK.
+     */
+    @RequiresApi(24)
+    class Partial @JvmOverloads constructor(
+        /**
+         * Controls whether a Baseline Profile should be used to partially pre compile the app.
+         *
+         * Defaults to [BaselineProfileMode.Require]
+         *
+         * @see BaselineProfileMode
+         */
+        val baselineProfileMode: BaselineProfileMode = BaselineProfileMode.Require,
+
+        /**
+         * If greater than 0, your macrobenchmark will run an extra [warmupIterations] times before
+         * compilation, to prepare
+         */
+        @IntRange(from = 0)
+        val warmupIterations: Int = 0
+    ) : CompilationMode() {
+        init {
+            require(warmupIterations >= 0) {
+                "warmupIterations must be non-negative, was $warmupIterations"
+            }
+            require(
+                baselineProfileMode != BaselineProfileMode.Disable || warmupIterations > 0
+            ) {
+                "Must set baselineProfileMode != Ignore, or warmup iterations > 0 to define" +
+                    " which portion of the app to pre-compile."
+            }
+        }
+
+        override fun toString(): String {
+            return if (
+                baselineProfileMode == BaselineProfileMode.Require && warmupIterations == 0
+            ) {
+                "BaselineProfile"
+            } else if (baselineProfileMode == BaselineProfileMode.Disable && warmupIterations > 0) {
+                "WarmupProfile(iterations=$warmupIterations)"
+            } else {
+                "Partial(baselineProfile=$baselineProfileMode,iterations=$warmupIterations)"
+            }
+        }
+
+        /**
+         * Returns null on success, or an error string otherwise.
+         *
+         * Returned error strings aren't thrown, to let the calling function decide strictness.
+         */
+        private fun broadcastBaselineProfileInstall(packageName: String): String? {
+            // For baseline profiles, we trigger this broadcast to force the baseline profile to be
+            // installed synchronously
+            val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
+            // Use an explicit broadcast given the app was force-stopped.
+            val name = ProfileInstallReceiver::class.java.name
+            val result = Shell.executeCommand("am broadcast -a $action $packageName/$name")
+                .substringAfter("Broadcast completed: result=")
+                .trim()
+                .toIntOrNull()
+            when (result) {
+                null,
+                    // 0 is returned by the platform by default, and also if no broadcast receiver
+                    // receives the broadcast.
+                0 -> {
+                    return "The baseline profile install broadcast was not received. " +
+                        "This most likely means that the profileinstaller library is missing " +
+                        "from the target apk."
+                }
+                ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
+                    return null // success!
+                }
+                ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
+                    throw RuntimeException(
+                        "Unable to install baseline profile. This most likely means that the " +
+                            "latest version of the profileinstaller library is not being used. " +
+                            "Please use the latest profileinstaller library version " +
+                            "in the target app."
+                    )
+                }
+                ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
+                    throw RuntimeException(
+                        "Baseline profiles aren't supported on this device version"
+                    )
+                }
+                ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
+                    return "No baseline profile was found in the target apk."
+                }
+                ProfileInstaller.RESULT_NOT_WRITABLE,
+                ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
+                ProfileInstaller.RESULT_IO_EXCEPTION,
+                ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
+                    throw RuntimeException("Baseline Profile wasn't successfully installed")
+                }
+                else -> {
+                    throw RuntimeException(
+                        "unrecognized ProfileInstaller result code: $result"
+                    )
+                }
+            }
+        }
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            if (baselineProfileMode != BaselineProfileMode.Disable) {
+                val installErrorString = broadcastBaselineProfileInstall(packageName)
+                if (installErrorString == null) {
+                    // baseline profile install success, kill process before compiling
+                    Log.d(TAG, "Killing process $packageName")
+                    Shell.executeCommand("am force-stop $packageName")
+                    cmdPackageCompile(packageName, "speed-profile")
+                } else {
+                    if (baselineProfileMode == BaselineProfileMode.Require) {
+                        throw RuntimeException(installErrorString)
+                    } else {
+                        Log.d(TAG, installErrorString)
+                    }
+                }
+            }
+            if (warmupIterations > 0) {
+                repeat(this.warmupIterations) {
+                    warmupBlock()
+                }
+                // For speed profile compilation, ART team recommended to wait for 5 secs when app
+                // is in the foreground, dump the profile, wait for another 5 secs before
+                // speed-profile compilation.
+                Thread.sleep(5000)
+                val response = Shell.executeCommand("killall -s SIGUSR1 $packageName")
+                if (response.isNotBlank()) {
+                    Log.d(TAG, "Received dump profile response $response")
+                    throw RuntimeException("Failed to dump profile for $packageName ($response)")
+                }
+                cmdPackageCompile(packageName, "speed-profile")
+            }
+        }
+    }
+
+    /**
+     * Full ahead-of-time compilation.
+     *
+     * Equates to `cmd package compile -f -m speed <package>` on API 24+.
+     *
+     * On Android M (API 23), this is the only supported compilation mode, as all apps are
+     * fully compiled ahead-of-time.
+     */
+    @Suppress("CanSealedSubClassBeObject") // Leaving possibility for future configuration
+    class Full : CompilationMode() {
+        override fun toString(): String = "Full"
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                cmdPackageCompile(packageName, "speed")
+            }
+            // Noop on older versions: apps are fully compiled at install time on API 23 and below
+        }
     }
 
     /**
@@ -77,10 +260,28 @@
      *
      * The compilation itself is performed with `cmd package compile -f -m speed-profile <package>`
      */
-    public class SpeedProfile(
-        public val warmupIterations: Int = 3
-    ) : CompilationMode("speed-profile") {
-        public override fun toString(): String = "SpeedProfile(iterations=$warmupIterations)"
+    @Deprecated(
+        message = "Use CompilationMode.Partial to partially pre-compile the target app",
+        replaceWith = ReplaceWith(
+            expression = "CompilationMode.Partial(baselineProfileMode=" +
+                "BaselineProfileMode.Ignore, warmupIterations=warmupIterations)",
+            imports = ["androidx.benchmark.macro.BaselineProfileMode"]
+        )
+    )
+    class SpeedProfile(
+        val warmupIterations: Int = 3
+    ) : CompilationMode() {
+        override fun toString(): String = "SpeedProfile(iterations=$warmupIterations)"
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                Partial(BaselineProfileMode.Disable, warmupIterations).compileImpl(
+                    packageName,
+                    warmupBlock
+                )
+            }
+            // Noop on older versions: this is compat behavior with previous library releases
+        }
     }
 
     /**
@@ -91,8 +292,22 @@
      *
      * The compilation itself is performed with `cmd package compile -f -m speed-profile <package>`
      */
-    public object BaselineProfile : CompilationMode("speed-profile") {
-        public override fun toString(): String = "BaselineProfile"
+    @Deprecated(
+        message = "Use CompilationMode.Partial to partially pre-compile the target app",
+        replaceWith = ReplaceWith("CompilationMode.Partial()")
+    )
+    object BaselineProfile : CompilationMode() {
+        override fun toString(): String = "BaselineProfile"
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            if (Build.VERSION.SDK_INT >= 24) {
+                Partial(BaselineProfileMode.Require, 0).compileImpl(
+                    packageName,
+                    warmupBlock
+                )
+            }
+            // Noop on older versions: this is compat behavior with previous library releases
+        }
     }
 
     /**
@@ -100,8 +315,16 @@
      *
      * Equates to `cmd package compile -f -m speed <package>`
      */
-    public object Speed : CompilationMode("speed") {
-        public override fun toString(): String = "Speed"
+    @Deprecated(
+        message = "Use CompilationMode.Full to fully compile the target app",
+        replaceWith = ReplaceWith("CompilationMode.Full")
+    )
+    object Speed : CompilationMode() {
+        override fun toString(): String = "Speed"
+
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            Full().compileImpl(packageName, warmupBlock)
+        }
     }
 
     /**
@@ -110,99 +333,47 @@
      * Note: this mode will only be supported on rooted devices with jit disabled. For this reason,
      * it's only available for internal benchmarking.
      *
+     * TODO: migrate this to an internal-only flag on [None] instead
+     *
      * @suppress
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-    public object Interpreted : CompilationMode(null) {
-        public override fun toString(): String = "Interpreted"
-    }
-}
+    object Interpreted : CompilationMode() {
+        override fun toString(): String = "Interpreted"
 
-/**
- * Compiles the application with the given mode.
- *
- * For more information: https://source.android.com/devices/tech/dalvik/jit-compiler
- */
-internal fun CompilationMode.compile(packageName: String, block: () -> Unit) {
-    if (Build.VERSION.SDK_INT < 24) {
-        // All supported versions prior to 24 were full AOT
-        // TODO: clarify this with CompilationMode errors on these versions
-        return
+        override fun compileImpl(packageName: String, warmupBlock: () -> Unit) {
+            // Nothing to do - handled externally
+        }
     }
 
-    // Clear profile between runs.
-    Log.d(TAG, "Clearing profiles for $packageName")
-    Shell.executeCommand("cmd package compile --reset $packageName")
-    if (this == CompilationMode.None || this == CompilationMode.Interpreted) {
-        return // nothing to do
-    } else if (this == CompilationMode.BaselineProfile) {
-        // For baseline profiles, if the ProfileInstaller library is included in the APK, then we
-        // triggering this broadcast will cause the baseline profile to get installed
-        // synchronously, instead of waiting for the
-        val action = ProfileInstallReceiver.ACTION_INSTALL_PROFILE
-        // Use an explicit broadcast given the app was force-stopped.
-        val name = ProfileInstallReceiver::class.java.name
-        val result = Shell.executeCommand("am broadcast -a $action $packageName/$name")
-            .substringAfter("Broadcast completed: result=")
-            .trim()
-            .toIntOrNull()
-        when (result) {
-            null,
-            // 0 is returned by the platform by default, and also if no broadcast receiver
-            // receives the broadcast.
-            0 -> {
-                throw RuntimeException(
-                    "The baseline profile install broadcast was not received. This most likely " +
-                        "means that the profileinstaller library is not in the target APK. It " +
-                        "must be in order to use CompilationMode.BaselineProfile."
-                )
-            }
-            ProfileInstaller.RESULT_INSTALL_SUCCESS -> {
-                // success !
-            }
-            ProfileInstaller.RESULT_ALREADY_INSTALLED -> {
-                throw RuntimeException(
-                    "Unable to install baseline profiles. This most likely means that the latest " +
-                        "version of the profileinstaller library is not being used. Please " +
-                        "use the latest profileinstaller library version."
-                )
-            }
-            ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION -> {
-                throw RuntimeException("Baseline profiles aren't supported on this device version")
-            }
-            ProfileInstaller.RESULT_BASELINE_PROFILE_NOT_FOUND -> {
-                throw RuntimeException("No baseline profile was found in the target apk.")
-            }
-            ProfileInstaller.RESULT_NOT_WRITABLE,
-            ProfileInstaller.RESULT_DESIRED_FORMAT_UNSUPPORTED,
-            ProfileInstaller.RESULT_IO_EXCEPTION,
-            ProfileInstaller.RESULT_PARSE_EXCEPTION -> {
-                throw RuntimeException("Baseline Profile wasn't successfully installed")
-            }
-            else -> {
-                throw RuntimeException(
-                    "unrecognized ProfileInstaller result code: $result"
-                )
-            }
-        }
-        // Kill process before compiling
-        Log.d(TAG, "Killing process $packageName")
-        Shell.executeCommand("am force-stop $packageName")
-    } else if (this is SpeedProfile) {
-        repeat(this.warmupIterations) {
-            block()
-        }
-        // For speed profile compilation, ART team recommended to wait for 5 secs when app
-        // is in the foreground, dump the profile, wait for another 5 secs before
-        // speed-profile compilation.
-        Thread.sleep(5000)
-        val response = Shell.executeCommand("killall -s SIGUSR1 $packageName")
-        if (response.isNotBlank()) {
-            Log.d(TAG, "Received dump profile response $response")
-            throw RuntimeException("Failed to dump profile for $packageName ($response)")
+    companion object {
+        internal val noop: CompilationMode = if (Build.VERSION.SDK_INT >= 24) None() else Full()
+
+        /**
+         * Represents the default compilation mode for the platform, on an end user's device.
+         *
+         * This is a post-store-install app configuration for this device's SDK
+         * version - [`Partial(BaselineProfileMode.IncludeIfAvailable)`][Partial] on
+         * API 24+, and [Full] prior to API 24 (where all apps are fully AOT compiled).
+         *
+         * On API 24+, Baseline Profile pre-compilation is used if possible, but no error will be
+         * thrown if installation fails.
+         *
+         * Generally, it is preferable to explicitly pass a compilation mode, such as
+         * [Partial(BaselineProfileMode.Include)][Partial] to avoid ambiguity, and e.g. validate an
+         * app's BaselineProfile can be correctly used.
+         */
+        @JvmField
+        val DEFAULT: CompilationMode = if (Build.VERSION.SDK_INT >= 24) {
+            Partial(
+                baselineProfileMode = BaselineProfileMode.UseIfAvailable,
+                warmupIterations = 0
+            )
+        } else {
+            // API 23 is always fully compiled
+            Full()
         }
     }
-    compilePackage(packageName)
 }
 
 /**
@@ -213,7 +384,7 @@
  * @suppress
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
-public fun CompilationMode.isSupportedWithVmSettings(): Boolean {
+fun CompilationMode.isSupportedWithVmSettings(): Boolean {
     val getProp = Shell.executeCommand("getprop dalvik.vm.extra-opts")
     val vmRunningInterpretedOnly = getProp.contains("-Xusejit:false")
 
@@ -245,17 +416,3 @@
         )
     }
 }
-
-/**
- * Compiles the application.
- */
-internal fun CompilationMode.compilePackage(packageName: String) {
-    Log.d(TAG, "Compiling $packageName ($this)")
-    val response = Shell.executeCommand(
-        "cmd package compile -f -m ${compileArgument()} $packageName"
-    )
-    if (!response.contains("Success")) {
-        Log.d(TAG, "Received compile cmd response: $response")
-        throw RuntimeException("Failed to compile $packageName ($response)")
-    }
-}
diff --git a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
index c1ae94e..c603bad 100644
--- a/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
+++ b/benchmark/benchmark-macro/src/main/java/androidx/benchmark/macro/Macrobenchmark.kt
@@ -105,7 +105,7 @@
     testName: String,
     packageName: String,
     metrics: List<Metric>,
-    compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+    compilationMode: CompilationMode,
     iterations: Int,
     launchWithClearTask: Boolean,
     startupModeMetricHint: StartupMode?,
@@ -118,6 +118,7 @@
     require(metrics.isNotEmpty()) {
         "Empty list of metrics passed to metrics param, must pass at least one Metric"
     }
+
     // skip benchmark if not supported by vm settings
     compilationMode.assumeSupportedWithVmSettings()
 
@@ -134,7 +135,7 @@
     scope.killProcess()
 
     userspaceTrace("compile $packageName") {
-        compilationMode.compile(packageName) {
+        compilationMode.resetAndCompile(packageName) {
             setupBlock(scope)
             measureBlock(scope)
         }
@@ -257,10 +258,10 @@
             }
         }
 
-        val warmupIterations = if (compilationMode is CompilationMode.SpeedProfile) {
-            compilationMode.warmupIterations
-        } else {
-            0
+        val warmupIterations = @Suppress("DEPRECATION") when (compilationMode) {
+            is CompilationMode.SpeedProfile -> compilationMode.warmupIterations
+            is CompilationMode.Partial -> compilationMode.warmupIterations
+            else -> 0
         }
 
         ResultWriter.appendReport(
@@ -285,13 +286,13 @@
  * @suppress
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public fun macrobenchmarkWithStartupMode(
+fun macrobenchmarkWithStartupMode(
     uniqueName: String,
     className: String,
     testName: String,
     packageName: String,
     metrics: List<Metric>,
-    compilationMode: CompilationMode = CompilationMode.SpeedProfile(),
+    compilationMode: CompilationMode,
     iterations: Int,
     startupMode: StartupMode?,
     setupBlock: MacrobenchmarkScope.() -> Unit,
@@ -325,8 +326,8 @@
                 // Empirically, this is also the  scenario most significantly affected by this
                 // JIT persistence, so we optimize  specifically for measurement correctness in
                 // this scenario.
-                if (compilationMode == CompilationMode.None) {
-                    compilationMode.compile(packageName) {
+                if (compilationMode is CompilationMode.None) {
+                    compilationMode.resetAndCompile(packageName = packageName) {
                         // This is only compiling for Compilation.None
                         throw IllegalStateException("block never used for CompilationMode.None")
                     }
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
index 436794b..172ebda 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupFullyDrawnBenchmark.kt
@@ -17,6 +17,7 @@
 package androidx.benchmark.integration.macrobenchmark
 
 import android.content.Intent
+import android.os.Build
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -27,6 +28,7 @@
 import androidx.test.uiautomator.UiDevice
 import androidx.test.uiautomator.Until
 import androidx.testutils.getStartupMetrics
+import org.junit.Assume.assumeFalse
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -45,7 +47,11 @@
     val benchmarkRule = MacrobenchmarkRule()
 
     private fun startup(startupMode: StartupMode) = benchmarkRule.measureRepeated(
-        compilationMode = CompilationMode.None,
+        compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+            CompilationMode.None()
+        } else {
+            CompilationMode.Full()
+        },
         packageName = TARGET_PACKAGE_NAME,
         metrics = getStartupMetrics(),
         startupMode = startupMode,
@@ -64,7 +70,12 @@
     }
 
     @Test
-    fun hot() = startup(StartupMode.HOT)
+    fun hot() {
+        // b/204572406 - HOT doesn't work on Angler API 23 in CI, but failure doesn't repro locally
+        assumeFalse(Build.VERSION.SDK_INT == 23 && Build.DEVICE == "angler")
+
+        startup(StartupMode.HOT)
+    }
 
     @Test
     fun warm() = startup(StartupMode.WARM)
diff --git a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
index 3ee5bde..54baa7b 100644
--- a/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
+++ b/benchmark/integration-tests/macrobenchmark/src/androidTest/java/androidx/benchmark/integration/macrobenchmark/TrivialStartupJavaBenchmark.java
@@ -39,7 +39,7 @@
         mBenchmarkRule.measureRepeated(
                 "androidx.benchmark.integration.macrobenchmark.target",
                 Collections.singletonList(new StartupTimingMetric()),
-                new CompilationMode.SpeedProfile(),
+                new CompilationMode.Partial(),
                 StartupMode.COLD,
                 3,
                 scope -> {
diff --git a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
index fcf6e5f..bf911bf 100644
--- a/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
+++ b/buildSrc/public/src/main/kotlin/androidx/build/LibraryVersions.kt
@@ -22,58 +22,58 @@
 object LibraryVersions {
     val ACTIVITY = Version("1.5.0-alpha01")
     val ADS_IDENTIFIER = Version("1.0.0-alpha05")
-    val ANNOTATION = Version("1.4.0-alpha01")
+    val ANNOTATION = Version("1.4.0-alpha02")
     val ANNOTATION_EXPERIMENTAL = Version("1.3.0-alpha01")
     val APPCOMPAT = Version("1.5.0-alpha01")
     val APPSEARCH = Version("1.0.0-alpha05")
     val ARCH_CORE = Version("2.2.0-alpha01")
     val ASYNCLAYOUTINFLATER = Version("1.1.0-alpha01")
     val AUTOFILL = Version("1.2.0-beta02")
-    val BENCHMARK = Version("1.1.0-alpha13")
+    val BENCHMARK = Version("1.1.0-alpha14")
     val BIOMETRIC = Version("1.2.0-alpha05")
     val BROWSER = Version("1.5.0-alpha01")
     val BUILDSRC_TESTS = Version("1.0.0-alpha01")
-    val CAMERA = Version("1.1.0-alpha12")
-    val CAMERA_EXTENSIONS = Version("1.0.0-alpha32")
+    val CAMERA = Version("1.1.0-alpha13")
+    val CAMERA_EXTENSIONS = Version("1.0.0-alpha33")
     val CAMERA_PIPE = Version("1.0.0-alpha01")
-    val CAMERA_VIEW = Version("1.0.0-alpha32")
+    val CAMERA_VIEW = Version("1.0.0-alpha33")
     val CARDVIEW = Version("1.1.0-alpha01")
-    val CAR_APP = Version("1.2.0-alpha02")
+    val CAR_APP = Version("1.2.0-alpha03")
     val COLLECTION = Version("1.3.0-alpha01")
     val COLLECTION2 = Version("1.3.0-alpha01")
     val CONTENTPAGER = Version("1.1.0-alpha01")
     val COMPOSE_MATERIAL3 = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.0.0-alpha03")
     val COMPOSE = Version(System.getenv("COMPOSE_CUSTOM_VERSION") ?: "1.2.0-alpha01")
-    val COORDINATORLAYOUT = Version("1.2.0-rc01")
-    val CORE = Version("1.8.0-alpha02")
+    val COORDINATORLAYOUT = Version("1.3.0-alpha01")
+    val CORE = Version("1.8.0-alpha03")
     val CORE_ANIMATION = Version("1.0.0-alpha03")
     val CORE_ANIMATION_TESTING = Version("1.0.0-alpha03")
     val CORE_APPDIGEST = Version("1.0.0-alpha01")
     val CORE_GOOGLE_SHORTCUTS = Version("1.1.0-alpha02")
-    val CORE_PERFORMANCE = Version("1.0.0-alpha01")
-    val CORE_REMOTEVIEWS = Version("1.0.0-alpha01")
-    val CORE_ROLE = Version("1.1.0-rc01")
+    val CORE_PERFORMANCE = Version("1.0.0-alpha02")
+    val CORE_REMOTEVIEWS = Version("1.0.0-alpha02")
+    val CORE_ROLE = Version("1.2.0-alpha01")
     val CORE_SPLASHSCREEN = Version("1.0.0-alpha03")
     val CURSORADAPTER = Version("1.1.0-alpha01")
     val CUSTOMVIEW = Version("1.2.0-alpha01")
     val DATASTORE = Version("1.1.0-alpha01")
     val DOCUMENTFILE = Version("1.1.0-alpha02")
-    val DRAGANDDROP = Version("1.0.0-alpha02")
+    val DRAGANDDROP = Version("1.0.0-alpha03")
     val DRAWERLAYOUT = Version("1.2.0-alpha01")
     val DYNAMICANIMATION = Version("1.1.0-alpha04")
     val DYNAMICANIMATION_KTX = Version("1.0.0-alpha04")
     val EMOJI = Version("1.2.0-alpha03")
-    val EMOJI2 = Version("1.1.0-alpha01")
+    val EMOJI2 = Version("1.1.0-alpha02")
     val ENTERPRISE = Version("1.1.0-rc01")
     val EXIFINTERFACE = Version("1.4.0-alpha01")
     val FRAGMENT = Version("1.5.0-alpha01")
     val FUTURES = Version("1.2.0-alpha01")
-    val GLANCE = Version("1.0.0-alpha01")
+    val GLANCE = Version("1.0.0-alpha02")
     val GRIDLAYOUT = Version("1.1.0-alpha01")
     val HEALTH_SERVICES_CLIENT = Version("1.0.0-alpha04")
     val HEIFWRITER = Version("1.1.0-alpha02")
     val HILT = Version("1.1.0-alpha01")
-    val HILT_NAVIGATION_COMPOSE = Version("1.0.0-rc01")
+    val HILT_NAVIGATION_COMPOSE = Version("1.1.0-alpha01")
     val INSPECTION = Version("1.0.0")
     val INTERPOLATOR = Version("1.1.0-alpha01")
     val JETIFIER = Version("1.0.0-beta11")
@@ -83,7 +83,7 @@
     val LEANBACK_TAB = Version("1.1.0-beta01")
     val LEANBACK_GRID = Version("1.0.0-alpha02")
     val LEGACY = Version("1.1.0-alpha01")
-    val LOCALBROADCASTMANAGER = Version("1.1.0-rc01")
+    val LOCALBROADCASTMANAGER = Version("1.2.0-alpha01")
     val LIBYUV = Version("0.1.0-dev01")
     val LIFECYCLE = Version("2.5.0-alpha01")
     val LIFECYCLE_VIEWMODEL_COMPOSE = Version("2.5.0-alpha01")
@@ -91,7 +91,7 @@
     val LOADER = Version("1.2.0-alpha01")
     val MEDIA = Version("1.5.0-beta02")
     val MEDIA2 = Version("1.3.0-alpha01")
-    val MEDIAROUTER = Version("1.3.0-alpha01")
+    val MEDIAROUTER = Version("1.3.0-alpha02")
     val METRICS = Version("1.0.0-alpha01")
     val NAVIGATION = Version("2.5.0-alpha01")
     val PAGING = Version("3.2.0-alpha01")
@@ -126,7 +126,7 @@
     val TESTSCREENSHOT = Version("1.0.0-alpha01")
     val TEXT = Version("1.0.0-alpha01")
     val TEXTCLASSIFIER = Version("1.0.0-alpha03")
-    val TRACING = Version("1.1.0-beta02")
+    val TRACING = Version("1.1.0-beta03")
     val TRANSITION = Version("1.5.0-alpha01")
     val TVPROVIDER = Version("1.1.0-alpha02")
     val VECTORDRAWABLE = Version("1.2.0-alpha03")
@@ -136,14 +136,14 @@
     val VIEWPAGER = Version("1.1.0-alpha02")
     val VIEWPAGER2 = Version("1.1.0-beta02")
     val WEAR = Version("1.3.0-alpha02")
-    val WEAR_COMPOSE = Version("1.0.0-alpha13")
+    val WEAR_COMPOSE = Version("1.0.0-alpha14")
     val WEAR_INPUT = Version("1.2.0-alpha03")
     val WEAR_INPUT_TESTING = WEAR_INPUT
     val WEAR_ONGOING = Version("1.1.0-alpha01")
-    val WEAR_PHONE_INTERACTIONS = Version("1.1.0-alpha02")
+    val WEAR_PHONE_INTERACTIONS = Version("1.1.0-alpha03")
     val WEAR_REMOTE_INTERACTIONS = Version("1.1.0-alpha01")
     val WEAR_TILES = Version("1.1.0-alpha01")
-    val WEAR_WATCHFACE = Version("1.1.0-alpha01")
+    val WEAR_WATCHFACE = Version("1.1.0-alpha02")
     val WEBKIT = Version("1.5.0-alpha01")
     val WINDOW = Version("1.1.0-alpha01")
     val WINDOW_EXTENSIONS = Version("1.1.0-alpha01")
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
index 5cd5948..7102a4a 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/CaptureConfigAdapterDeviceTest.kt
@@ -23,6 +23,7 @@
 import androidx.annotation.RequiresApi
 import androidx.camera.camera2.pipe.integration.adapter.CameraControlAdapter
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
 import androidx.camera.core.impl.CameraCaptureCallback
 import androidx.camera.core.impl.CameraCaptureFailure
 import androidx.camera.core.impl.CameraCaptureResult
@@ -138,7 +139,11 @@
             }.build()
 
         // Act
-        cameraControl!!.submitStillCaptureRequests(listOf(captureConfig))
+        cameraControl!!.submitStillCaptureRequests(
+            listOf(captureConfig),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
 
         // Assert
         Truth.assertThat(
diff --git a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
index 4c0462c..1422354 100644
--- a/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/androidTest/java/androidx/camera/camera2/pipe/integration/EvCompDeviceTest.kt
@@ -29,7 +29,6 @@
 import androidx.camera.core.CameraSelector
 import androidx.camera.core.ImageAnalysis
 import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH
 import androidx.camera.core.internal.CameraUseCaseAdapter
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraXUtil
@@ -162,40 +161,6 @@
     }
 
     @Test
-    fun setExposureAndStartFlashSequence_theExposureSettingShouldApply() = runBlocking {
-        val exposureState = camera.cameraInfo.exposureState
-        Assume.assumeTrue(exposureState.isExposureCompensationSupported)
-
-        bindUseCase()
-
-        // Act. Set the exposure compensation
-        val upper = exposureState.exposureCompensationRange.upper
-        cameraControl.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS)
-        // Test the flash API after exposure changed.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH).get(3000, TimeUnit.MILLISECONDS)
-
-        // Assert. Verify the exposure compensation target result is in the capture result.
-        registerListener().verifyCaptureResultParameter(CONTROL_AE_EXPOSURE_COMPENSATION, upper)
-    }
-
-    @Test
-    fun setExposureAndTriggerAf_theExposureSettingShouldApply() = runBlocking {
-        val exposureState = camera.cameraInfo.exposureState
-        Assume.assumeTrue(exposureState.isExposureCompensationSupported)
-
-        bindUseCase()
-
-        // Act. Set the exposure compensation, and then use the AF API after the exposure is
-        // changed.
-        val upper = exposureState.exposureCompensationRange.upper
-        cameraControl.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS)
-        cameraControl.triggerAf().get(3000, TimeUnit.MILLISECONDS)
-
-        // Assert. Verify the exposure compensation target result is in the capture result.
-        registerListener().verifyCaptureResultParameter(CONTROL_AE_EXPOSURE_COMPENSATION, upper)
-    }
-
-    @Test
     fun setExposureAndZoomRatio_theExposureSettingShouldApply() = runBlocking {
         val exposureState = camera.cameraInfo.exposureState
         Assume.assumeTrue(exposureState.isExposureCompensationSupported)
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
index 532cdb2..ab5babb 100644
--- 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
@@ -38,7 +38,6 @@
 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
@@ -48,6 +47,7 @@
 import kotlinx.coroutines.CoroutineStart
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.launch
+import java.util.Collections
 import javax.inject.Inject
 
 /**
@@ -158,34 +158,22 @@
         this.imageCaptureFlashMode = flashMode
     }
 
-    override fun triggerAf(): ListenableFuture<CameraCaptureResult> {
-        warn { "TODO: triggerAf is not yet supported" }
-        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create())
-    }
-
-    override fun startFlashSequence(
-        @ImageCapture.FlashType flashType: Int
-    ): ListenableFuture<Void> {
-        warn { "TODO: startFlashSequence is not yet supported" }
-        return Futures.immediateFuture(null)
-    }
-
-    override fun cancelAfAndFinishFlashSequence(
-        cancelAfTrigger: Boolean,
-        finishFlashSequence: Boolean
-    ) {
-        warn { "TODO: cancelAfAndFinishFlashSequence is not yet supported" }
-    }
-
     override fun setExposureCompensationIndex(exposure: Int): ListenableFuture<Int> =
         Futures.nonCancellationPropagating(
             evCompControl.updateAsync(exposure).asListenableFuture()
         )
 
-    override fun submitStillCaptureRequests(captureConfigs: List<CaptureConfig>) {
+    override fun submitStillCaptureRequests(
+        captureConfigs: List<CaptureConfig>,
+        captureMode: Int,
+        flashType: Int,
+    ): ListenableFuture<List<Void>> {
         val camera = useCaseManager.camera
         checkNotNull(camera) { "Attempted to issue capture requests while the camera isn't ready." }
         camera.capture(captureConfigs)
+
+        // TODO(b/199813515) : implement the preCapture
+        return Futures.immediateFuture(Collections.emptyList())
     }
 
     override fun getSessionConfig(): SessionConfig {
diff --git a/camera/camera-camera2/build.gradle b/camera/camera-camera2/build.gradle
index d18a2f3..63b4d89 100644
--- a/camera/camera-camera2/build.gradle
+++ b/camera/camera-camera2/build.gradle
@@ -38,13 +38,15 @@
     testImplementation(libs.testRunner)
     testImplementation(libs.junit)
     testImplementation(libs.truth)
-    testImplementation("org.robolectric:robolectric:4.6.1") // TODO(b/209062465): fix tests to work with SDK 31 and robolectric 4.7
+    testImplementation(libs.robolectric)
     testImplementation(libs.mockitoCore)
     testImplementation(libs.kotlinCoroutinesTest)
     testImplementation("androidx.annotation:annotation-experimental:1.1.0")
+    testImplementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
     testImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
     testImplementation(project(":camera:camera-testing"))
     testImplementation("androidx.arch.core:core-testing:2.1.0")
+    testImplementation("junit:junit:4.13") // Needed for Assert.assertThrows
     testImplementation("org.apache.maven:maven-ant-tasks:2.1.3")
 
     androidTestImplementation(libs.multidex)
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
index f9d5b24..964ea8a 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
+++ b/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/internal/Camera2CameraControlImplDeviceTest.java
@@ -38,6 +38,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -73,6 +74,7 @@
 import androidx.camera.testing.CameraUtil;
 import androidx.camera.testing.CameraXUtil;
 import androidx.camera.testing.HandlerUtil;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
 import androidx.core.os.HandlerCompat;
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -93,6 +95,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mockito;
 
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
@@ -354,29 +357,70 @@
     public void triggerAf_futureSucceeds() throws Exception {
         Camera2CameraControlImpl camera2CameraControlImpl =
                 createCamera2CameraControlWithPhysicalCamera();
-        ListenableFuture<CameraCaptureResult> future = camera2CameraControlImpl.triggerAf();
+
+        ListenableFuture<CameraCaptureResult> future = CallbackToFutureAdapter.getFuture(c -> {
+            camera2CameraControlImpl.mExecutor.execute(() ->
+                    camera2CameraControlImpl.getFocusMeteringControl().triggerAf(
+                            c, /* overrideAeMode */ false));
+            return "triggerAf";
+        });
+
         future.get(5, TimeUnit.SECONDS);
     }
 
     @Test
-    @LargeTest
-    public void startFlashSequence_futureSucceeds() throws Exception {
-        Camera2CameraControlImpl camera2CameraControlImpl =
-                createCamera2CameraControlWithPhysicalCamera();
-        ListenableFuture<Void> future = camera2CameraControlImpl.startFlashSequence(
+    public void captureMaxQuality_shouldSuccess()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
                 ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
-        future.get(5, TimeUnit.SECONDS);
     }
 
     @Test
-    @LargeTest
-    public void setFlashModeAndStartFlashSequence_futureSucceeds() throws Exception {
-        Camera2CameraControlImpl camera2CameraControlImpl =
-                createCamera2CameraControlWithPhysicalCamera();
-        camera2CameraControlImpl.setFlashMode(ImageCapture.FLASH_MODE_ON);
-        ListenableFuture<Void> future = camera2CameraControlImpl.startFlashSequence(
+    public void captureMiniLatency_shouldSuccess()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
                 ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
-        future.get(5, TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void captureMaxQuality_torchAsFlash_shouldSuccess()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        captureTest(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+                ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
+    }
+
+    @Test
+    public void captureMiniLatency_torchAsFlash_shouldSuccess()
+            throws ExecutionException, InterruptedException, TimeoutException {
+        captureTest(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+                ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH);
+    }
+
+    private void captureTest(int captureMode, int flashType)
+            throws ExecutionException, InterruptedException, TimeoutException {
+        ImageCapture imageCapture = new ImageCapture.Builder().build();
+
+        mCamera = CameraUtil.createCameraAndAttachUseCase(
+                ApplicationProvider.getApplicationContext(), CameraSelector.DEFAULT_BACK_CAMERA,
+                imageCapture);
+
+        Camera2CameraControlImpl camera2CameraControlImpl =
+                (Camera2CameraControlImpl) mCamera.getCameraControl();
+
+        CameraCaptureCallback captureCallback = mock(CameraCaptureCallback.class);
+        CaptureConfig.Builder captureConfigBuilder = new CaptureConfig.Builder();
+        captureConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_STILL_CAPTURE);
+        captureConfigBuilder.addSurface(imageCapture.getSessionConfig().getSurfaces().get(0));
+        captureConfigBuilder.addCameraCaptureCallback(captureCallback);
+
+        ListenableFuture<List<Void>> future = camera2CameraControlImpl.submitStillCaptureRequests(
+                Arrays.asList(captureConfigBuilder.build()), captureMode, flashType);
+
+        // The future should successfully complete
+        future.get(10, TimeUnit.SECONDS);
+        // CameraCaptureCallback.onCaptureCompleted() should be called to signal a capture attempt.
+        verify(captureCallback, timeout(3000).times(1))
+                .onCaptureCompleted(any(CameraCaptureResult.class));
     }
 
     private Camera2CameraControlImpl createCamera2CameraControlWithPhysicalCamera() {
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 e57c7a6..e207dbf 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
@@ -49,6 +49,7 @@
 import androidx.camera.core.Camera;
 import androidx.camera.core.CameraControl;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.ImageCapture;
 import androidx.camera.core.InitializationException;
 import androidx.camera.core.UseCase;
 import androidx.camera.core.impl.CameraCaptureCallback;
@@ -402,7 +403,9 @@
         captureConfigBuilder.addCameraCaptureCallback(captureCallback);
 
         mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
-                Arrays.asList(captureConfigBuilder.build()));
+                Arrays.asList(captureConfigBuilder.build()),
+                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
 
         UseCase useCase2 = createUseCase();
         mCamera2CameraImpl.attachUseCases(Arrays.asList(useCase2));
@@ -439,7 +442,9 @@
         captureConfigBuilder.addCameraCaptureCallback(captureCallback);
 
         mCamera2CameraImpl.getCameraControlInternal().submitStillCaptureRequests(
-                Arrays.asList(captureConfigBuilder.build()));
+                Arrays.asList(captureConfigBuilder.build()),
+                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
         mCamera2CameraImpl.detachUseCases(Arrays.asList(useCase1));
 
         // Unblock camera handle to make camera operation run quickly .
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 d4caf95..9b8d57bd 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
@@ -286,70 +286,6 @@
     }
 
     @Test
-    public void setExposureAndStartFlashSequence_theExposureSettingShouldApply()
-            throws InterruptedException, ExecutionException, TimeoutException,
-            CameraUseCaseAdapter.CameraException {
-        ExposureState exposureState = mCameraInfoInternal.getExposureState();
-        assumeTrue(exposureState.isExposureCompensationSupported());
-
-        FakeTestUseCase useCase = openUseCase();
-        ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
-                TotalCaptureResult.class);
-        CameraCaptureSession.CaptureCallback callback = mock(
-                CameraCaptureSession.CaptureCallback.class);
-        useCase.setCameraCaptureCallback(callback);
-
-        // Wait a little bit for the camera to open.
-        assertTrue(mSessionStateCallback.waitForOnConfigured(1));
-
-        // Set the exposure compensation
-        int upper = exposureState.getExposureCompensationRange().getUpper();
-        mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
-        mCameraControlInternal.startFlashSequence(ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH).get(3000,
-                TimeUnit.MILLISECONDS);
-
-        // Verify the exposure compensation target result is in the capture result.
-        verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
-                any(CameraCaptureSession.class),
-                any(CaptureRequest.class),
-                captureResultCaptor.capture());
-        List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
-        TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
-        assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
-    }
-
-    @Test
-    public void setExposureAndTriggerAf_theExposureSettingShouldApply()
-            throws InterruptedException, ExecutionException, TimeoutException,
-            CameraUseCaseAdapter.CameraException {
-        ExposureState exposureState = mCameraInfoInternal.getExposureState();
-        assumeTrue(exposureState.isExposureCompensationSupported());
-
-        FakeTestUseCase useCase = openUseCase();
-        ArgumentCaptor<TotalCaptureResult> captureResultCaptor = ArgumentCaptor.forClass(
-                TotalCaptureResult.class);
-        CameraCaptureSession.CaptureCallback callback = mock(
-                CameraCaptureSession.CaptureCallback.class);
-        useCase.setCameraCaptureCallback(callback);
-
-        // Wait a little bit for the camera to open.
-        assertTrue(mSessionStateCallback.waitForOnConfigured(1));
-
-        int upper = exposureState.getExposureCompensationRange().getUpper();
-        mCameraControlInternal.setExposureCompensationIndex(upper).get(3000, TimeUnit.MILLISECONDS);
-        mCameraControlInternal.triggerAf().get(3000, TimeUnit.MILLISECONDS);
-
-        // Verify the exposure compensation target result is in the capture result.
-        verify(callback, timeout(3000).atLeastOnce()).onCaptureCompleted(
-                any(CameraCaptureSession.class),
-                any(CaptureRequest.class),
-                captureResultCaptor.capture());
-        List<TotalCaptureResult> totalCaptureResults = captureResultCaptor.getAllValues();
-        TotalCaptureResult result = totalCaptureResults.get(totalCaptureResults.size() - 1);
-        assertThat(result.get(CaptureResult.CONTROL_AE_EXPOSURE_COMPENSATION)).isEqualTo(upper);
-    }
-
-    @Test
     public void setExposureAndZoomRatio_theExposureSettingShouldApply()
             throws InterruptedException, ExecutionException, TimeoutException,
             CameraUseCaseAdapter.CameraException {
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 654e397..5cbc416 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -19,7 +19,6 @@
 import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
-import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH;
 
 import android.graphics.Rect;
 import android.hardware.camera2.CameraCaptureSession;
@@ -42,8 +41,6 @@
 import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
 import androidx.camera.camera2.internal.compat.workaround.AeFpsRange;
 import androidx.camera.camera2.internal.compat.workaround.AutoFlashAEModeDisabler;
-import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture;
-import androidx.camera.camera2.internal.compat.workaround.UseTorchAsFlash;
 import androidx.camera.camera2.interop.Camera2CameraControl;
 import androidx.camera.camera2.interop.CaptureRequestOptions;
 import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -128,19 +125,16 @@
     private final TorchControl mTorchControl;
     private final ExposureControl mExposureControl;
     private final Camera2CameraControl mCamera2CameraControl;
+    private final Camera2CapturePipeline mCamera2CapturePipeline;
     @GuardedBy("mLock")
     private int mUseCount = 0;
     // use volatile modifier to make these variables in sync in all threads.
     private volatile boolean mIsTorchOn = false;
-    private boolean mIsTorchEnabledByFlash = false;
-    private boolean mIsAeTriggeredByFlash = false;
     @ImageCapture.FlashMode
     private volatile int mFlashMode = FLASH_MODE_OFF;
 
     // Workarounds
     private final AeFpsRange mAeFpsRange;
-    private final UseTorchAsFlash mUseTorchAsFlash;
-    private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
     private final AutoFlashAEModeDisabler mAutoFlashAEModeDisabler = new AutoFlashAEModeDisabler();
 
     static final String TAG_SESSION_UPDATE_ID = "CameraControlSessionUpdateId";
@@ -202,10 +196,9 @@
 
         // Workarounds
         mAeFpsRange = new AeFpsRange(cameraQuirks);
-        mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
-        mOverrideAeModeForStillCapture = new OverrideAeModeForStillCapture(cameraQuirks);
-
         mCamera2CameraControl = new Camera2CameraControl(this, mExecutor);
+        mCamera2CapturePipeline = new Camera2CapturePipeline(this, mCameraCharacteristics,
+                cameraQuirks, mExecutor);
         mExecutor.execute(
                 () -> addCaptureResultListener(mCamera2CameraControl.getCaptureRequestListener()));
     }
@@ -387,27 +380,6 @@
         return Futures.nonCancellationPropagating(mTorchControl.enableTorch(torch));
     }
 
-    /**
-     * Issues a {@link CaptureRequest#CONTROL_AF_TRIGGER_START} request to start auto focus scan.
-     *
-     * @return a {@link ListenableFuture} which completes when the request is completed.
-     * Cancelling the ListenableFuture is a no-op.
-     */
-    @Override
-    @NonNull
-    public ListenableFuture<CameraCaptureResult> triggerAf() {
-        if (!isControlInUse()) {
-            return Futures.immediateFailedFuture(
-                    new OperationCanceledException("Camera is not active."));
-        }
-        return Futures.nonCancellationPropagating(CallbackToFutureAdapter.getFuture(
-                completer -> {
-                    mExecutor.execute(() -> mFocusMeteringControl.triggerAf(
-                            completer, /* overrideAeMode */ false));
-                    return "triggerAf";
-                }));
-    }
-
     @ExecutedBy("mExecutor")
     @NonNull
     private ListenableFuture<Void> waitForSessionUpdateId(long sessionUpdateIdToWait) {
@@ -448,94 +420,6 @@
         return false;
     }
 
-    /**
-     * {@inheritDoc}
-     *
-     * <p>Issues a {@link CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_START} request to start auto
-     * exposure scan. In some cases, torch flash will be used instead of issuing
-     * {@code CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START}.
-     *
-     * @param flashType Uses one shot flash or use torch as flash when taking a picture.
-     * @return a {@link ListenableFuture} which completes when the request is completed.
-     * Cancelling the ListenableFuture is a no-op.
-     */
-    @Override
-    @NonNull
-    public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
-        if (!isControlInUse()) {
-            return Futures.immediateFailedFuture(
-                    new OperationCanceledException("Camera is not active."));
-        }
-
-        // Prior to AE precapture, wait until pending flash mode session change is completed. On
-        // some devices, AE precapture may not work properly if the repeating request to change
-        // the flash mode is not completed.
-        ListenableFuture<Void> future = FutureChain.from(mFlashModeChangeSessionUpdateFuture)
-                .transformAsync(v -> {
-                    return CallbackToFutureAdapter.getFuture(
-                            completer -> {
-                                if (mUseTorchAsFlash.shouldUseTorchAsFlash()
-                                        || flashType == FLASH_TYPE_USE_TORCH_AS_FLASH
-                                        || mTemplate == CameraDevice.TEMPLATE_RECORD) {
-                                    Logger.d(TAG, "startFlashSequence: Use torch");
-                                    if (mIsTorchOn) {
-                                        completer.set(null);
-                                    } else {
-                                        mTorchControl.enableTorchInternal(completer, true);
-                                        mIsTorchEnabledByFlash = true;
-                                    }
-                                } else {
-                                    Logger.d(TAG, "startFlashSequence: use triggerAePrecapture");
-                                    mFocusMeteringControl.triggerAePrecapture(completer);
-                                    mIsAeTriggeredByFlash = true;
-                                    mOverrideAeModeForStillCapture.onAePrecaptureStarted();
-                                }
-                                return "startFlashSequence";
-                            });
-                }, mExecutor);
-
-        return Futures.nonCancellationPropagating(future);
-    }
-
-    /**
-     * {@inheritDoc}
-     *
-     * <p>Issues {@link CaptureRequest#CONTROL_AF_TRIGGER_CANCEL} and/or {@link
-     * CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL} request to cancel auto focus or auto
-     * exposure scan.
-     *
-     * <p>When torch is used instead of issuing
-     * {@code CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START} in
-     * {@link #startFlashSequence(int)}, this method will close torch instead of issuing
-     * {@code CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL}.
-     */
-    @Override
-    public void cancelAfAndFinishFlashSequence(final boolean cancelAfTrigger,
-            final boolean finishFlashSequence) {
-        if (!isControlInUse()) {
-            Logger.w(TAG, "Camera is not active.");
-            return;
-        }
-        mExecutor.execute(() -> {
-            boolean cancelAeTrigger = false;
-            if (finishFlashSequence) {
-                if (mIsTorchEnabledByFlash) {
-                    mIsTorchEnabledByFlash = false;
-                    mTorchControl.enableTorchInternal(null, false);
-                }
-                if (mIsAeTriggeredByFlash) {
-                    mIsAeTriggeredByFlash = false;
-                    cancelAeTrigger = true;
-                    mOverrideAeModeForStillCapture.onAePrecaptureFinished();
-                }
-            }
-
-            if (cancelAfTrigger || cancelAeTrigger) {
-                mFocusMeteringControl.cancelAfAeTrigger(cancelAfTrigger, cancelAeTrigger);
-            }
-        });
-    }
-
     @NonNull
     @Override
     public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
@@ -547,45 +431,25 @@
     }
 
     /** {@inheritDoc} */
+    @NonNull
     @Override
-    public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+    public ListenableFuture<List<Void>> submitStillCaptureRequests(
+            @NonNull List<CaptureConfig> captureConfigs,
+            @ImageCapture.CaptureMode int captureMode,
+            @ImageCapture.FlashType int flashType) {
         if (!isControlInUse()) {
             Logger.w(TAG, "Camera is not active.");
-            return;
+            return Futures.immediateFailedFuture(
+                    new OperationCanceledException("Camera is not active."));
         }
-        mExecutor.execute(() -> {
-            List<CaptureConfig> configsToSubmit = new ArrayList<>(captureConfigs);
-            for (int i = 0; i < captureConfigs.size(); i++) {
-                CaptureConfig captureConfig = captureConfigs.get(i);
-                int templateToModify = CaptureConfig.TEMPLATE_TYPE_NONE;
-                if (mTemplate == CameraDevice.TEMPLATE_RECORD && !isLegacyDevice()) {
-                    // Always override template by TEMPLATE_VIDEO_SNAPSHOT when repeating
-                    // template is TEMPLATE_RECORD. Note: TEMPLATE_VIDEO_SNAPSHOT is not
-                    // supported on legacy device.
-                    templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT;
-                } else if (captureConfig.getTemplateType() == CaptureConfig.TEMPLATE_TYPE_NONE) {
-                    templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE;
-                }
 
-                if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE
-                        || mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(mFlashMode)) {
-                    CaptureConfig.Builder configBuilder = CaptureConfig.Builder.from(captureConfig);
-                    if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE) {
-                        configBuilder.setTemplateType(templateToModify);
-                    }
-
-                    // Override AE Mode to ON_ALWAYS_FLASH if necessary.
-                    if (mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(mFlashMode)) {
-                        Camera2ImplConfig.Builder impBuilder = new Camera2ImplConfig.Builder();
-                        impBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
-                                CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
-                        configBuilder.addImplementationOptions(impBuilder.build());
-                    }
-                    configsToSubmit.set(i, configBuilder.build());
-                }
-            }
-            submitCaptureRequestsInternal(configsToSubmit);
-        });
+        // Prior to submitStillCaptures, wait until the pending flash mode session change is
+        // completed. On some devices, AE precapture triggered in submitStillCaptures may not
+        // work properly if the repeating request to change the flash mode is not completed.
+        int flashMode = getFlashMode();
+        return FutureChain.from(mFlashModeChangeSessionUpdateFuture).transformAsync(
+                v -> mCamera2CapturePipeline.submitStillCaptures(
+                        captureConfigs, captureMode, flashMode, flashType), mExecutor);
     }
 
     /** {@inheritDoc} */
@@ -608,6 +472,7 @@
         mTemplate = template;
 
         mFocusMeteringControl.setTemplate(mTemplate);
+        mCamera2CapturePipeline.setTemplate(mTemplate);
     }
 
     @ExecutedBy("mExecutor")
@@ -718,6 +583,10 @@
         updateSessionConfigSynchronous();
     }
 
+    @ExecutedBy("mExecutor")
+    boolean isTorchOn() {
+        return mIsTorchOn;
+    }
 
     @ExecutedBy("mExecutor")
     void submitCaptureRequestsInternal(final List<CaptureConfig> captureConfigs) {
@@ -904,12 +773,6 @@
         return mCurrentSessionUpdateId;
     }
 
-    private boolean isLegacyDevice() {
-        Integer level =
-                mCameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
-        return level != null && level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
-    }
-
     /** An interface to listen to camera capture results. */
     public interface CaptureResultListener {
         /**
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
new file mode 100644
index 0000000..43a658a
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal;
+
+import static androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY;
+import static androidx.camera.core.ImageCapture.CaptureMode;
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
+import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
+import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH;
+import static androidx.camera.core.ImageCapture.FlashMode;
+import static androidx.camera.core.ImageCapture.FlashType;
+import static androidx.camera.core.impl.CameraCaptureMetaData.AfMode;
+import static androidx.camera.core.impl.CameraCaptureMetaData.AfState;
+
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureRequest;
+import android.hardware.camera2.CaptureResult;
+import android.hardware.camera2.TotalCaptureResult;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.OptIn;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.internal.annotation.CameraExecutor;
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture;
+import androidx.camera.camera2.internal.compat.workaround.UseTorchAsFlash;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.CameraCaptureCallback;
+import androidx.camera.core.impl.CameraCaptureFailure;
+import androidx.camera.core.impl.CameraCaptureMetaData.AeState;
+import androidx.camera.core.impl.CameraCaptureMetaData.AwbState;
+import androidx.camera.core.impl.CameraCaptureResult;
+import androidx.camera.core.impl.CaptureConfig;
+import androidx.camera.core.impl.Quirks;
+import androidx.camera.core.impl.annotation.ExecutedBy;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.impl.utils.futures.FutureChain;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Implementation detail of the submitStillCaptures method.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+class Camera2CapturePipeline {
+
+    private static final String TAG = "Camera2CapturePipeline";
+
+    @NonNull
+    private final Camera2CameraControlImpl mCameraControl;
+
+    @NonNull
+    private final UseTorchAsFlash mUseTorchAsFlash;
+
+    @NonNull
+    private final Quirks mCameraQuirk;
+
+    @NonNull
+    @CameraExecutor
+    private final Executor mExecutor;
+
+    private final boolean mIsLegacyDevice;
+
+    private int mTemplate = CameraDevice.TEMPLATE_PREVIEW;
+
+    /**
+     * Constructs a Camera2CapturePipeline for single capture use.
+     */
+    Camera2CapturePipeline(@NonNull Camera2CameraControlImpl cameraControl,
+            @NonNull CameraCharacteristicsCompat cameraCharacteristics,
+            @NonNull Quirks cameraQuirks,
+            @CameraExecutor @NonNull Executor executor) {
+        mCameraControl = cameraControl;
+        Integer level =
+                cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
+        mIsLegacyDevice = level != null
+                && level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY;
+        mExecutor = executor;
+        mCameraQuirk = cameraQuirks;
+        mUseTorchAsFlash = new UseTorchAsFlash(cameraQuirks);
+    }
+
+    @ExecutedBy("mExecutor")
+    public void setTemplate(int template) {
+        mTemplate = template;
+    }
+
+    /**
+     * Submit a list of capture configs to the camera, it returns a ListenableFuture
+     * which will be completed after all the captures were done.
+     *
+     * @return the future will be completed after all the captures are completed, It would
+     * fail with a {@link androidx.camera.core.ImageCapture#ERROR_CAMERA_CLOSED} when the
+     * capture was canceled, or {@link androidx.camera.core.ImageCapture#ERROR_CAPTURE_FAILED}
+     * when the capture was failed.
+     */
+    @ExecutedBy("mExecutor")
+    @NonNull
+    public ListenableFuture<List<Void>> submitStillCaptures(
+            @NonNull List<CaptureConfig> captureConfigs, @CaptureMode int captureMode,
+            @FlashMode int flashMode, @FlashType int flashType) {
+
+        OverrideAeModeForStillCapture aeQuirk = new OverrideAeModeForStillCapture(mCameraQuirk);
+        Pipeline pipeline = new Pipeline(mTemplate, mExecutor, mCameraControl, mIsLegacyDevice,
+                aeQuirk);
+
+        if (captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
+            pipeline.addTask(new AfTask(mCameraControl));
+        }
+
+        if (isTorchAsFlash(flashType)) {
+            pipeline.addTask(new TorchTask(mCameraControl, flashMode));
+        } else {
+            pipeline.addTask(new AePreCaptureTask(mCameraControl, flashMode, aeQuirk));
+        }
+
+        return Futures.nonCancellationPropagating(
+                pipeline.executeCapture(captureConfigs, flashMode));
+    }
+
+    /**
+     * The pipeline for single capturing.
+     */
+    @VisibleForTesting
+    static class Pipeline {
+        private static final long CHECK_3A_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(1);
+        private static final long CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(5);
+
+        private final int mTemplate;
+        private final Executor mExecutor;
+        private final Camera2CameraControlImpl mCameraControl;
+        private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
+        private final boolean mIsLegacyDevice;
+        private long mTimeout3A = CHECK_3A_TIMEOUT_IN_NS;
+        @SuppressWarnings("WeakerAccess") /* synthetic access */
+        final List<PipelineTask> mTasks = new ArrayList<>();
+
+        private final PipelineTask mPipelineSubTask = new PipelineTask() {
+
+            @NonNull
+            @Override
+            public ListenableFuture<Boolean> preCapture(
+                    @Nullable TotalCaptureResult captureResult) {
+                ArrayList<ListenableFuture<Boolean>> futures = new ArrayList<>();
+                for (PipelineTask task : mTasks) {
+                    futures.add(task.preCapture(captureResult));
+                }
+                return Futures.transform(Futures.allAsList(futures),
+                        results -> results.contains(true), CameraXExecutors.directExecutor());
+            }
+
+            @Override
+            public boolean isCaptureResultNeeded() {
+                for (PipelineTask task : mTasks) {
+                    if (task.isCaptureResultNeeded()) {
+                        return true;
+                    }
+                }
+                return false;
+            }
+
+            @Override
+            public void postCapture() {
+                for (PipelineTask task : mTasks) {
+                    task.postCapture();
+                }
+            }
+        };
+
+        Pipeline(int template, @NonNull Executor executor,
+                @NonNull Camera2CameraControlImpl cameraControl, boolean isLegacyDevice,
+                @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
+            mTemplate = template;
+            mExecutor = executor;
+            mCameraControl = cameraControl;
+            mIsLegacyDevice = isLegacyDevice;
+            mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
+        }
+
+        /**
+         * Add the AE/AF/Torch tasks if required.
+         *
+         * @param task implements the PipelineTask interface
+         */
+        void addTask(@NonNull PipelineTask task) {
+            mTasks.add(task);
+        }
+
+        /**
+         * Set the timeout for the 3A converge.
+         *
+         * @param timeout3A in nano seconds
+         */
+        private void setTimeout3A(long timeout3A) {
+            mTimeout3A = timeout3A;
+        }
+
+        @SuppressWarnings("FutureReturnValueIgnored")
+        @ExecutedBy("mExecutor")
+        @NonNull
+        ListenableFuture<List<Void>> executeCapture(@NonNull List<CaptureConfig> captureConfigs,
+                @FlashMode int flashMode) {
+            ListenableFuture<TotalCaptureResult> preCapture = Futures.immediateFuture(null);
+            if (!mTasks.isEmpty()) {
+                ListenableFuture<TotalCaptureResult> getResult =
+                        mPipelineSubTask.isCaptureResultNeeded() ? waitForResult(
+                                ResultListener.NO_TIMEOUT, null) : Futures.immediateFuture(null);
+
+                preCapture = FutureChain.from(getResult).transformAsync(captureResult -> {
+                    if (isFlashRequired(flashMode, captureResult)) {
+                        setTimeout3A(CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS);
+                    }
+                    return mPipelineSubTask.preCapture(captureResult);
+                }, mExecutor).transformAsync(is3aConvergeRequired -> {
+                    if (is3aConvergeRequired) {
+                        return waitForResult(mTimeout3A, this::is3AConverged);
+                    }
+                    return Futures.immediateFuture(null);
+                }, mExecutor);
+            }
+
+            ListenableFuture<List<Void>> future = FutureChain.from(preCapture).transformAsync(
+                    v -> submitConfigsInternal(captureConfigs, flashMode), mExecutor);
+
+
+            /* Always call postCapture(), it will unlock3A if it was locked in preCapture.*/
+            future.addListener(() -> {
+                mPipelineSubTask.postCapture();
+            }, mExecutor);
+
+            return future;
+        }
+
+        @ExecutedBy("mExecutor")
+        @NonNull
+        ListenableFuture<List<Void>> submitConfigsInternal(
+                @NonNull List<CaptureConfig> captureConfigs, @FlashMode int flashMode) {
+            List<ListenableFuture<Void>> futureList = new ArrayList<>();
+            List<CaptureConfig> configsToSubmit = new ArrayList<>();
+            for (CaptureConfig captureConfig : captureConfigs) {
+                CaptureConfig.Builder configBuilder = CaptureConfig.Builder.from(captureConfig);
+                applyStillCaptureTemplate(configBuilder, captureConfig);
+                if (mOverrideAeModeForStillCapture.shouldSetAeModeAlwaysFlash(flashMode)) {
+                    applyAeModeQuirk(configBuilder);
+                }
+
+                futureList.add(CallbackToFutureAdapter.getFuture(completer -> {
+                    configBuilder.addCameraCaptureCallback(new CameraCaptureCallback() {
+                        @Override
+                        public void onCaptureCompleted(@NonNull CameraCaptureResult result) {
+                            completer.set(null);
+                        }
+
+                        @Override
+                        public void onCaptureFailed(@NonNull CameraCaptureFailure failure) {
+                            String msg =
+                                    "Capture request failed with reason " + failure.getReason();
+                            completer.setException(
+                                    new ImageCaptureException(ERROR_CAPTURE_FAILED, msg, null));
+                        }
+
+                        @Override
+                        public void onCaptureCancelled() {
+                            String msg = "Capture request is cancelled because camera is closed";
+                            completer.setException(
+                                    new ImageCaptureException(ERROR_CAMERA_CLOSED, msg, null));
+                        }
+                    });
+                    return "submitStillCapture";
+                }));
+                configsToSubmit.add(configBuilder.build());
+            }
+            mCameraControl.submitCaptureRequestsInternal(configsToSubmit);
+
+            return Futures.allAsList(futureList);
+        }
+
+        @ExecutedBy("mExecutor")
+        private void applyStillCaptureTemplate(@NonNull CaptureConfig.Builder configBuilder,
+                @NonNull CaptureConfig captureConfig) {
+            int templateToModify = CaptureConfig.TEMPLATE_TYPE_NONE;
+            if (mTemplate == CameraDevice.TEMPLATE_RECORD && !mIsLegacyDevice) {
+                // Always override template by TEMPLATE_VIDEO_SNAPSHOT when
+                // repeating template is TEMPLATE_RECORD. Note:
+                // TEMPLATE_VIDEO_SNAPSHOT is not supported on legacy device.
+                templateToModify = CameraDevice.TEMPLATE_VIDEO_SNAPSHOT;
+            } else if (captureConfig.getTemplateType() == CaptureConfig.TEMPLATE_TYPE_NONE) {
+                templateToModify = CameraDevice.TEMPLATE_STILL_CAPTURE;
+            }
+
+            if (templateToModify != CaptureConfig.TEMPLATE_TYPE_NONE) {
+                configBuilder.setTemplateType(templateToModify);
+            }
+        }
+
+        @ExecutedBy("mExecutor")
+        @OptIn(markerClass = ExperimentalCamera2Interop.class)
+        private void applyAeModeQuirk(@NonNull CaptureConfig.Builder configBuilder) {
+            Camera2ImplConfig.Builder impBuilder = new Camera2ImplConfig.Builder();
+            impBuilder.setCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE,
+                    CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
+            configBuilder.addImplementationOptions(impBuilder.build());
+        }
+
+        @ExecutedBy("mExecutor")
+        @NonNull
+        private ListenableFuture<TotalCaptureResult> waitForResult(long waitTimeout,
+                @Nullable ResultListener.Checker checker) {
+            ResultListener resultListener = new ResultListener(waitTimeout, checker);
+            mCameraControl.addCaptureResultListener(resultListener);
+            return resultListener.getFuture();
+        }
+
+        private boolean is3AConverged(@Nullable TotalCaptureResult totalCaptureResult) {
+            if (totalCaptureResult == null) {
+                return false;
+            }
+
+            Camera2CameraCaptureResult captureResult = new Camera2CameraCaptureResult(
+                    totalCaptureResult);
+
+            // If afMode is OFF or UNKNOWN , no need for waiting.
+            // otherwise wait until af is locked or focused.
+            boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
+                    || captureResult.getAfMode() == AfMode.UNKNOWN
+                    || captureResult.getAfState() == AfState.PASSIVE_FOCUSED
+                    || captureResult.getAfState() == AfState.PASSIVE_NOT_FOCUSED
+                    || captureResult.getAfState() == AfState.LOCKED_FOCUSED
+                    || captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED;
+
+            // Unknown means cannot get valid state from CaptureResult
+            boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
+                    || captureResult.getAeState() == AeState.FLASH_REQUIRED
+                    || captureResult.getAeState() == AeState.UNKNOWN;
+
+            // Unknown means cannot get valid state from CaptureResult
+            boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
+                    || captureResult.getAwbState() == AwbState.UNKNOWN;
+
+            Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
+                    + " AF =" + captureResult.getAfState()
+                    + " AWB=" + captureResult.getAwbState());
+            return isAfReady && isAeReady && isAwbReady;
+        }
+    }
+
+    interface PipelineTask {
+        /**
+         * @return A {@link ListenableFuture} that will be fulfilled with a Boolean result, the
+         * result true if it needs to wait for 3A converge after the task is executed, otherwise
+         * false.
+         */
+        @ExecutedBy("mExecutor")
+        @NonNull
+        ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult);
+
+        /**
+         * @return true if the preCapture method requires a CaptureResult. When it return false,
+         * that means the {@link #preCapture(TotalCaptureResult)} ()} can accept a null input, we
+         * don't need to capture a CaptureResult for this task.
+         */
+        @ExecutedBy("mExecutor")
+        boolean isCaptureResultNeeded();
+
+        @ExecutedBy("mExecutor")
+        void postCapture();
+    }
+
+    /**
+     * Task to triggerAF preCapture if it is required
+     */
+    static class AfTask implements PipelineTask {
+
+        private final Camera2CameraControlImpl mCameraControl;
+        private boolean mIsExecuted = false;
+
+        AfTask(@NonNull Camera2CameraControlImpl cameraControl) {
+            mCameraControl = cameraControl;
+        }
+
+        @ExecutedBy("mExecutor")
+        @NonNull
+        @Override
+        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+            // Always return true for this task since we always need to wait for the focused
+            // signal after the task is executed.
+            ListenableFuture<Boolean> ret = Futures.immediateFuture(true);
+
+            if (captureResult == null) {
+                return ret;
+            }
+
+            Integer afMode = captureResult.get(CaptureResult.CONTROL_AF_MODE);
+            if (afMode == null) {
+                return ret;
+            }
+            switch (afMode) {
+                case CaptureResult.CONTROL_AF_MODE_AUTO:
+                case CaptureResult.CONTROL_AF_MODE_MACRO:
+                    Logger.d(TAG, "TriggerAf? AF mode auto");
+                    Integer afState = captureResult.get(CaptureResult.CONTROL_AF_STATE);
+                    if (afState != null && afState == CaptureResult.CONTROL_AF_STATE_INACTIVE) {
+                        Logger.d(TAG, "Trigger AF");
+
+                        mIsExecuted = true;
+                        mCameraControl.getFocusMeteringControl().triggerAf(null, false);
+                        return ret;
+                    }
+                    break;
+                default:
+                    // fall out
+            }
+
+            return ret;
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public boolean isCaptureResultNeeded() {
+            return true;
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public void postCapture() {
+            if (mIsExecuted) {
+                Logger.d(TAG, "cancel TriggerAF");
+                mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(true, false);
+            }
+        }
+    }
+
+    /**
+     * Task to open the Torch if flash is required.
+     */
+    static class TorchTask implements PipelineTask {
+
+        private final Camera2CameraControlImpl mCameraControl;
+        private final @FlashMode int mFlashMode;
+        private boolean mIsExecuted = false;
+
+        TorchTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode) {
+            mCameraControl = cameraControl;
+            mFlashMode = flashMode;
+        }
+
+        @ExecutedBy("mExecutor")
+        @NonNull
+        @Override
+        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+            if (isFlashRequired(mFlashMode, captureResult)) {
+                if (mCameraControl.isTorchOn()) {
+                    Logger.d(TAG, "Torch already on, not turn on");
+                } else {
+                    Logger.d(TAG, "Turn on torch");
+                    mIsExecuted = true;
+
+                    ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+                        mCameraControl.getTorchControl().enableTorchInternal(completer, true);
+                        return "TorchOn";
+                    });
+                    return FutureChain.from(future).transform(input -> true,
+                            CameraXExecutors.directExecutor());
+                }
+            }
+
+            return Futures.immediateFuture(false);
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public boolean isCaptureResultNeeded() {
+            return mFlashMode == FLASH_MODE_AUTO;
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public void postCapture() {
+            if (mIsExecuted) {
+                mCameraControl.getTorchControl().enableTorchInternal(null, false);
+                Logger.d(TAG, "Turn off torch");
+            }
+        }
+    }
+
+    /**
+     * Task to trigger AePreCapture if flash is required.
+     */
+    static class AePreCaptureTask implements PipelineTask {
+
+        private final Camera2CameraControlImpl mCameraControl;
+        private final OverrideAeModeForStillCapture mOverrideAeModeForStillCapture;
+        private final @FlashMode int mFlashMode;
+        private boolean mIsExecuted = false;
+
+        AePreCaptureTask(@NonNull Camera2CameraControlImpl cameraControl, @FlashMode int flashMode,
+                @NonNull OverrideAeModeForStillCapture overrideAeModeForStillCapture) {
+            mCameraControl = cameraControl;
+            mFlashMode = flashMode;
+            mOverrideAeModeForStillCapture = overrideAeModeForStillCapture;
+        }
+
+        @ExecutedBy("mExecutor")
+        @NonNull
+        @Override
+        public ListenableFuture<Boolean> preCapture(@Nullable TotalCaptureResult captureResult) {
+            if (isFlashRequired(mFlashMode, captureResult)) {
+                Logger.d(TAG, "Trigger AE");
+                mIsExecuted = true;
+
+                ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(completer -> {
+                    mCameraControl.getFocusMeteringControl().triggerAePrecapture(completer);
+                    mOverrideAeModeForStillCapture.onAePrecaptureStarted();
+                    return "AePreCapture";
+                });
+                return FutureChain.from(future).transform(input -> true,
+                        CameraXExecutors.directExecutor());
+            }
+
+            return Futures.immediateFuture(false);
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public boolean isCaptureResultNeeded() {
+            return mFlashMode == FLASH_MODE_AUTO;
+        }
+
+        @ExecutedBy("mExecutor")
+        @Override
+        public void postCapture() {
+            if (mIsExecuted) {
+                Logger.d(TAG, "cancel TriggerAePreCapture");
+                mCameraControl.getFocusMeteringControl().cancelAfAeTrigger(false, true);
+                mOverrideAeModeForStillCapture.onAePrecaptureFinished();
+            }
+        }
+    }
+
+    static boolean isFlashRequired(@FlashMode int flashMode, @Nullable TotalCaptureResult result) {
+        switch (flashMode) {
+            case FLASH_MODE_ON:
+                return true;
+            case FLASH_MODE_AUTO:
+                Integer aeState = (result != null) ? result.get(CaptureResult.CONTROL_AE_STATE)
+                        : null;
+                return aeState != null && aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED;
+            case FLASH_MODE_OFF:
+                return false;
+        }
+        throw new AssertionError(flashMode);
+    }
+
+    /**
+     * A listener receives the result of the repeating request. The results will be sent to the
+     * Checker to identify if the mFuture can be completed.
+     */
+    static class ResultListener implements Camera2CameraControlImpl.CaptureResultListener {
+
+        /**
+         * The totalCaptureResults will be sent to the Checker#check() method, return true in the
+         * Checker#check() will complete the mFuture.
+         */
+        interface Checker {
+            boolean check(@NonNull TotalCaptureResult totalCaptureResult);
+        }
+
+        static final long NO_TIMEOUT = 0L;
+
+        private CallbackToFutureAdapter.Completer<TotalCaptureResult> mCompleter;
+        private final ListenableFuture<TotalCaptureResult> mFuture =
+                CallbackToFutureAdapter.getFuture(completer -> {
+                    mCompleter = completer;
+                    return "waitFor3AResult";
+                });
+        private final long mTimeLimitNs;
+        private final Checker mChecker;
+        private volatile Long mTimestampOfFirstUpdateNs = null;
+
+        /**
+         * @param timeLimitNs timeout threshold in Nanos
+         * @param checker     the checker to define the condition to complete the mFuture, set null
+         *                    will complete the mFuture once it receives any totalCaptureResults.
+         */
+        ResultListener(long timeLimitNs, @Nullable Checker checker) {
+            mTimeLimitNs = timeLimitNs;
+            mChecker = checker;
+        }
+
+        @NonNull
+        public ListenableFuture<TotalCaptureResult> getFuture() {
+            return mFuture;
+        }
+
+        @Override
+        public boolean onCaptureResult(@NonNull TotalCaptureResult captureResult) {
+            Long currentTimestampNs = captureResult.get(CaptureResult.SENSOR_TIMESTAMP);
+            if (currentTimestampNs != null && mTimestampOfFirstUpdateNs == null) {
+                mTimestampOfFirstUpdateNs = currentTimestampNs;
+            }
+
+            Long timestampOfFirstUpdateNs = mTimestampOfFirstUpdateNs;
+            if (NO_TIMEOUT != mTimeLimitNs && timestampOfFirstUpdateNs != null
+                    && currentTimestampNs != null
+                    && currentTimestampNs - timestampOfFirstUpdateNs > mTimeLimitNs) {
+                mCompleter.set(null);
+                Logger.d(TAG, "Wait for capture result timeout, current:" + currentTimestampNs
+                        + " first: " + timestampOfFirstUpdateNs);
+                return true;
+            }
+
+            if (mChecker != null && !mChecker.check(captureResult)) {
+                return false;
+            }
+
+            mCompleter.set(captureResult);
+            return true;
+        }
+    }
+
+    private boolean isTorchAsFlash(@FlashType int flashType) {
+        return mUseTorchAsFlash.shouldUseTorchAsFlash() || mTemplate == CameraDevice.TEMPLATE_RECORD
+                || flashType == FLASH_TYPE_USE_TORCH_AS_FLASH;
+    }
+
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
index fb4fa35..2f73e88 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompat.java
@@ -50,7 +50,9 @@
      * @see android.hardware.camera2.CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP
      */
     public InputConfigurationCompat(int width, int height, int format) {
-        if (Build.VERSION.SDK_INT >= 23) {
+        if (Build.VERSION.SDK_INT >= 31) {
+            mImpl = new InputConfigurationCompatApi31Impl(width, height, format);
+        } else if (Build.VERSION.SDK_INT >= 23) {
             mImpl = new InputConfigurationCompatApi23Impl(width, height, format);
         } else {
             mImpl = new InputConfigurationCompatBaseImpl(width, height, format);
@@ -80,6 +82,10 @@
         if (Build.VERSION.SDK_INT < 23) {
             return null;
         }
+        if (Build.VERSION.SDK_INT >= 31) {
+            return new InputConfigurationCompat(
+                    new InputConfigurationCompatApi31Impl(inputConfiguration));
+        }
         return new InputConfigurationCompat(
                 new InputConfigurationCompatApi23Impl(inputConfiguration));
     }
@@ -112,6 +118,18 @@
     }
 
     /**
+     * Whether this input configuration is of multi-resolution.
+     *
+     * <p>An multi-resolution InputConfiguration means that the reprocessing session created from it
+     * allows input images of different sizes.</p>
+     *
+     * @return  this input configuration is multi-resolution or not.
+     */
+    public boolean isMultiResolution() {
+        return mImpl.isMultiResolution();
+    }
+
+    /**
      * Check if this InputConfiguration is equal to another InputConfiguration.
      *
      * <p>Two input configurations are equal if and only if they have the same widths, heights, and
@@ -145,6 +163,7 @@
      *
      * @return string representation of {@link InputConfigurationCompat}
      */
+    @NonNull
     @Override
     public String toString() {
         return mImpl.toString();
@@ -171,6 +190,8 @@
 
         int getFormat();
 
+        boolean isMultiResolution();
+
         @Nullable
         Object getInputConfiguration();
     }
@@ -205,6 +226,11 @@
         }
 
         @Override
+        public boolean isMultiResolution() {
+            return false;
+        }
+
+        @Override
         public Object getInputConfiguration() {
             return null;
         }
@@ -234,6 +260,7 @@
             return h;
         }
 
+        @NonNull
         @SuppressLint("DefaultLocale") // Implementation matches framework
         @Override
         public String toString() {
@@ -243,7 +270,7 @@
     }
 
     @RequiresApi(23)
-    private static final class InputConfigurationCompatApi23Impl implements
+    private static class InputConfigurationCompatApi23Impl implements
             InputConfigurationCompatImpl {
 
         private final InputConfiguration mObject;
@@ -271,6 +298,11 @@
             return mObject.getFormat();
         }
 
+        @Override
+        public boolean isMultiResolution() {
+            return false;
+        }
+
         @Nullable
         @Override
         public Object getInputConfiguration() {
@@ -291,10 +323,29 @@
             return mObject.hashCode();
         }
 
+        @NonNull
         @Override
         public String toString() {
             return mObject.toString();
         }
     }
 
+    @RequiresApi(31)
+    private static final class InputConfigurationCompatApi31Impl extends
+            InputConfigurationCompatApi23Impl {
+
+        InputConfigurationCompatApi31Impl(@NonNull Object inputConfiguration) {
+            super(inputConfiguration);
+        }
+
+        InputConfigurationCompatApi31Impl(int width, int height, int format) {
+            super(width, height, format);
+        }
+
+        @Override
+        public boolean isMultiResolution() {
+            return ((InputConfiguration) getInputConfiguration()).isMultiResolution();
+        }
+    }
+
 }
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt
deleted file mode 100644
index 4a735b2..0000000
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraControlImplTest.kt
+++ /dev/null
@@ -1,663 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.camera2.internal
-
-import android.content.Context
-import android.graphics.SurfaceTexture
-import android.hardware.camera2.CameraCaptureSession
-import android.hardware.camera2.CameraCharacteristics
-import android.hardware.camera2.CameraDevice
-import android.hardware.camera2.CameraManager
-import android.hardware.camera2.CameraMetadata
-import android.hardware.camera2.CaptureRequest
-import android.hardware.camera2.TotalCaptureResult
-import android.os.Build
-import android.os.Handler
-import android.os.HandlerThread
-import android.view.Surface
-import androidx.camera.camera2.impl.Camera2ImplConfig
-import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
-import androidx.camera.camera2.internal.compat.quirk.AutoFlashUnderExposedQuirk
-import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
-import androidx.camera.camera2.internal.compat.quirk.UseTorchAsFlashQuirk
-import androidx.camera.core.ImageCapture
-import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
-import androidx.camera.core.ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH
-import androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH
-import androidx.camera.core.impl.CameraCaptureCallback
-import androidx.camera.core.impl.CameraCaptureFailure
-import androidx.camera.core.impl.CameraCaptureResult
-import androidx.camera.core.impl.CameraControlInternal
-import androidx.camera.core.impl.CaptureConfig
-import androidx.camera.core.impl.ImmediateSurface
-import androidx.camera.core.impl.Quirks
-import androidx.camera.core.impl.SessionConfig
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
-import androidx.camera.testing.HandlerUtil
-import androidx.core.os.HandlerCompat
-import androidx.test.core.app.ApplicationProvider
-import com.google.common.truth.Truth.assertThat
-import com.google.common.truth.Truth.assertWithMessage
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.ArgumentMatchers.any
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
-import org.mockito.Mockito.reset
-import org.mockito.Mockito.verify
-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
-
-private const val CAMERA_ID_0 = "0"
-
-@RunWith(RobolectricTestRunner::class)
-@DoNotInstrument
-@Config(
-    minSdk = Build.VERSION_CODES.LOLLIPOP
-)
-class Camera2CameraControlImplTest {
-
-    private val context = ApplicationProvider.getApplicationContext() as Context
-    private val controlUpdateCallback =
-        mock(CameraControlInternal.ControlUpdateCallback::class.java)
-    private lateinit var cameraControl: Camera2CameraControlImpl
-    private lateinit var handlerThread: HandlerThread
-    private lateinit var handler: Handler
-
-    @Before
-    fun setUp() {
-        initCameras()
-
-        handlerThread = HandlerThread("ControlThread").apply { start() }
-        handler = HandlerCompat.createAsync(handlerThread.looper)
-
-        createCameraControl()
-    }
-
-    @After
-    fun tearDown() {
-        if (::handlerThread.isInitialized) {
-            handlerThread.quitSafely()
-        }
-    }
-
-    @Test
-    fun triggerAf_captureRequestSent() {
-        // Act.
-        cameraControl.triggerAf()
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertAfTrigger()
-    }
-
-    @Test
-    fun cancelAf_captureRequestSent() {
-        // Act.
-        cameraControl.triggerAf()
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        reset(controlUpdateCallback)
-
-        cameraControl.cancelAfAndFinishFlashSequence(true, false)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertCancelAfTrigger()
-    }
-
-    @Test
-    fun startFlashSequence_aePrecaptureSent() {
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertAePrecaptureTrigger()
-    }
-
-    @Test
-    fun startFlashSequence_flashModeWasSet() {
-        // Act 1
-        cameraControl.flashMode = ImageCapture.FLASH_MODE_ON
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert 1: ensures AePrecapture is not invoked.
-        verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-
-        // Act 2: Send the CaptureResult
-        triggerRepeatRequestResult()
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert 2: AePrecapture is triggered.
-        assertAePrecaptureTrigger()
-    }
-
-    private fun triggerRepeatRequestResult() {
-        val tagBundle = cameraControl.sessionConfig.repeatingCaptureConfig.tagBundle
-        val mockCaptureRequest = mock(CaptureRequest::class.java)
-        `when`(mockCaptureRequest.tag).thenReturn(tagBundle)
-        val mockCaptureResult = mock(TotalCaptureResult::class.java)
-        `when`(mockCaptureResult.request).thenReturn(mockCaptureRequest)
-        for (cameraCaptureCallback in cameraControl.sessionConfig.repeatingCameraCaptureCallbacks) {
-            val callback = CaptureCallbackConverter.toCaptureCallback(cameraCaptureCallback)
-            callback.onCaptureCompleted(
-                mock(CameraCaptureSession::class.java),
-                mockCaptureRequest, mockCaptureResult
-            )
-        }
-    }
-
-    @Config(minSdk = 23)
-    @Test
-    fun finishFlashSequence_cancelAePrecaptureSent() {
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        reset(controlUpdateCallback)
-
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertCancelAePrecaptureTrigger()
-    }
-
-    @Test
-    fun cancelAfAndFinishFlashSequence_cancelAfAndAePrecaptureSent() {
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        reset(controlUpdateCallback)
-
-        cameraControl.cancelAfAndFinishFlashSequence(true, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertCancelAfTrigger()
-        assertCancelAePrecaptureTrigger()
-    }
-
-    @Test
-    fun startFlashSequence_withTorchAsFlashQuirk_enableTorchSent() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertTorchEnable()
-    }
-
-    @Test
-    fun startFlashSequence_withFlashTypeTorch_enableTorchSent() {
-        // Arrange.
-        createCameraControl()
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertTorchEnable()
-    }
-
-    @Test
-    fun startFlashSequence_withTemplateRecord_enableTorchSent() {
-        // Arrange.
-        cameraControl.setTemplate(CameraDevice.TEMPLATE_RECORD)
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertTorchEnable()
-    }
-
-    @Test
-    fun finishFlashSequence_withUseTorchAsFlashQuirk_disableTorch() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        reset(controlUpdateCallback)
-
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertTorchDisable()
-    }
-
-    @Test
-    fun finishFlashSequence_withFlashTypeTorch_disableTorch() {
-        // Arrange.
-        createCameraControl()
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        reset(controlUpdateCallback)
-
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        assertTorchDisable()
-    }
-
-    @Test
-    fun startFlashSequence_withUseTorchAsFlashQuirk_torchIsAlreadyOn() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {})))
-        cameraControl.enableTorchInternal(true)
-        HandlerUtil.waitForLooperToIdle(handler)
-        reset(controlUpdateCallback)
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-        verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-
-        // Arrange.
-        reset(controlUpdateCallback)
-
-        // Act.
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-        verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-    }
-
-    @Test
-    fun startFlashSequence_withFlashTypeTorch_torchIsAlreadyOn() {
-        // Arrange.
-        createCameraControl()
-        cameraControl.enableTorchInternal(true)
-        HandlerUtil.waitForLooperToIdle(handler)
-        reset(controlUpdateCallback)
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_USE_TORCH_AS_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-        verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-
-        // Arrange.
-        reset(controlUpdateCallback)
-
-        // Act.
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        verify(controlUpdateCallback, never()).onCameraControlCaptureRequests(any())
-        verify(controlUpdateCallback, never()).onCameraControlUpdateSessionConfig()
-    }
-
-    @Test
-    fun submitStillCaptureRequests_withTemplate_templateSent() {
-        // Arrange.
-        val imageCaptureConfig = CaptureConfig.Builder().let {
-            it.templateType = CameraDevice.TEMPLATE_MANUAL
-            it.build()
-        }
-
-        // Act.
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        val captureConfig = getIssuedCaptureConfig()
-        assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_MANUAL)
-    }
-
-    @Test
-    fun submitStillCaptureRequests_withNoTemplate_templateStillCaptureSent() {
-        // Arrange.
-        val imageCaptureConfig = CaptureConfig.Builder().build()
-
-        // Act.
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        val captureConfig = getIssuedCaptureConfig()
-        assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_STILL_CAPTURE)
-    }
-
-    @Test
-    fun submitStillCaptureRequests_withTemplateRecord_templateVideoSnapshotSent() {
-        // Arrange.
-        cameraControl.setTemplate(CameraDevice.TEMPLATE_RECORD)
-        val imageCaptureConfig = CaptureConfig.Builder().build()
-
-        // Act.
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        val captureConfig = getIssuedCaptureConfig()
-        assertThat(captureConfig.templateType).isEqualTo(CameraDevice.TEMPLATE_VIDEO_SNAPSHOT)
-    }
-
-    @Test
-    fun overrideAeModeForStillCapture_quirkAbsent_notOverride() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(emptyList())) // Not have the quirk.
-        cameraControl.flashMode = FLASH_MODE_AUTO
-        triggerRepeatRequestResult() // make sures flashMode is updated.
-        val imageCaptureConfig = CaptureConfig.Builder().build()
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-        reset(controlUpdateCallback)
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        // AE mode should not be overridden
-        val captureConfig = getIssuedCaptureConfig()
-        assertThat(
-            captureConfig.toCamera2Config()
-                .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
-        ).isNull()
-    }
-
-    @Test
-    fun overrideAeModeForStillCapture_aePrecaptureStarted_override() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
-        cameraControl.flashMode = FLASH_MODE_AUTO
-        triggerRepeatRequestResult() // make sures flashMode is updated.
-        val imageCaptureConfig = getCaptureConfigWithParameters()
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler)
-        reset(controlUpdateCallback)
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        // AE mode should be overridden to CONTROL_AE_MODE_ON_ALWAYS_FLASH
-        val issuedCaptureConfig = getIssuedCaptureConfig()
-        assertThat(
-            issuedCaptureConfig.toCamera2Config()
-                .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
-        ).isEqualTo(CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
-        issuedCaptureConfig.assertContainAllParametersFrom(imageCaptureConfig)
-    }
-
-    private fun getCaptureConfigWithParameters(): CaptureConfig {
-        val builder = CaptureConfig.Builder()
-        builder.addCameraCaptureCallback(object : CameraCaptureCallback() {
-            override fun onCaptureCompleted(cameraCaptureResult: CameraCaptureResult) {
-                super.onCaptureCompleted(cameraCaptureResult)
-            }
-
-            override fun onCaptureFailed(failure: CameraCaptureFailure) {
-                super.onCaptureFailed(failure)
-            }
-
-            override fun onCaptureCancelled() {
-                super.onCaptureCancelled()
-            }
-        })
-        builder.addSurface(ImmediateSurface(Surface(SurfaceTexture(0))))
-        builder.templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
-        builder.addTag("test", "testValue")
-        return builder.build()
-    }
-    private fun CaptureConfig.assertContainAllParametersFrom(captureConfig: CaptureConfig) {
-        assertThat(captureConfig.surfaces).isEqualTo(surfaces)
-        for (listOption in captureConfig.implementationOptions.listOptions()) {
-            assertThat(captureConfig.implementationOptions.retrieveOption(listOption))
-                .isEqualTo(implementationOptions.retrieveOption(listOption))
-        }
-        assertThat(captureConfig.cameraCaptureCallbacks)
-            .isEqualTo(cameraCaptureCallbacks)
-        assertThat(captureConfig.isUseRepeatingSurface)
-            .isEqualTo(isUseRepeatingSurface)
-        for (key in captureConfig.tagBundle.listKeys()) {
-            assertThat(captureConfig.tagBundle.getTag(key)).isEqualTo(tagBundle.getTag(key))
-        }
-        assertThat(captureConfig.templateType).isEqualTo(templateType)
-    }
-
-    @Test
-    fun overrideAeModeForStillCapture_aePrecaptureFinish_notOverride() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
-        cameraControl.flashMode = FLASH_MODE_AUTO
-        triggerRepeatRequestResult() // make sures flashMode is updated.
-        val imageCaptureConfig = CaptureConfig.Builder().build()
-
-        // Act.
-        cameraControl.startFlashSequence(FLASH_TYPE_ONE_SHOT_FLASH)
-        HandlerUtil.waitForLooperToIdle(handler) // required to make sure states are changed
-        cameraControl.cancelAfAndFinishFlashSequence(false, true)
-        HandlerUtil.waitForLooperToIdle(handler)
-        reset(controlUpdateCallback)
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        // AE mode should not be overridden
-        val issuedCaptureConfig = getIssuedCaptureConfig()
-        assertThat(
-            issuedCaptureConfig.toCamera2Config()
-                .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
-        ).isNull()
-    }
-
-    @Test
-    fun overrideAeModeForStillCapture_noAePrecaptureTriggered_notOverride() {
-        // Arrange.
-        createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk())))
-        cameraControl.flashMode = FLASH_MODE_AUTO
-        triggerRepeatRequestResult() // make sures flashMode is updated.
-        val imageCaptureConfig = CaptureConfig.Builder().build()
-
-        // Act.
-        cameraControl.submitStillCaptureRequests(listOf(imageCaptureConfig))
-        HandlerUtil.waitForLooperToIdle(handler)
-
-        // Assert.
-        // AE mode should not be overridden
-        val issuedCaptureConfig = getIssuedCaptureConfig()
-        assertThat(
-            issuedCaptureConfig.toCamera2Config()
-                .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
-        ).isNull()
-    }
-
-    private fun assertAfTrigger() {
-        assertCamera2ConfigValue(
-            getIssuedCaptureConfig().toCamera2Config(),
-            CaptureRequest.CONTROL_AF_TRIGGER,
-            CaptureRequest.CONTROL_AF_TRIGGER_START
-        )
-    }
-
-    private fun assertCancelAfTrigger() {
-        assertCamera2ConfigValue(
-            getIssuedCaptureConfig().toCamera2Config(),
-            CaptureRequest.CONTROL_AF_TRIGGER,
-            CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
-        )
-    }
-
-    private fun assertAePrecaptureTrigger() {
-        assertCamera2ConfigValue(
-            getIssuedCaptureConfig().toCamera2Config(),
-            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
-            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
-        )
-    }
-
-    private fun assertCancelAePrecaptureTrigger() {
-        if (Build.VERSION.SDK_INT < 23) {
-            return
-        }
-
-        assertCamera2ConfigValue(
-            getIssuedCaptureConfig().toCamera2Config(),
-            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
-            CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
-        )
-    }
-
-    private fun assertTorchEnable() {
-        val camera2Config = getIssuedSessionConfig().toCamera2Config()
-
-        assertCamera2ConfigValue(
-            camera2Config,
-            CaptureRequest.CONTROL_AE_MODE,
-            CaptureRequest.CONTROL_AE_MODE_ON,
-        )
-
-        assertCamera2ConfigValue(
-            camera2Config,
-            CaptureRequest.FLASH_MODE,
-            CameraMetadata.FLASH_MODE_TORCH,
-        )
-    }
-
-    private fun assertTorchDisable() {
-        val camera2Config = getIssuedCaptureConfig().toCamera2Config()
-
-        assertCamera2ConfigValue(
-            camera2Config,
-            CaptureRequest.CONTROL_AE_MODE,
-            CaptureRequest.CONTROL_AE_MODE_ON
-        )
-
-        assertCamera2ConfigValue(
-            camera2Config,
-            CaptureRequest.FLASH_MODE,
-            CaptureRequest.FLASH_MODE_OFF
-        )
-    }
-
-    private fun getIssuedCaptureConfig(): CaptureConfig {
-        @Suppress("UNCHECKED_CAST")
-        val captor =
-            ArgumentCaptor.forClass(List::class.java) as ArgumentCaptor<List<CaptureConfig>>
-        verify(controlUpdateCallback).onCameraControlCaptureRequests(captor.capture())
-        return captor.value[0]
-    }
-
-    private fun getIssuedSessionConfig(): SessionConfig {
-        verify(controlUpdateCallback).onCameraControlUpdateSessionConfig()
-        return cameraControl.sessionConfig
-    }
-
-    private fun <T> assertCamera2ConfigValue(
-        camera2Config: Camera2ImplConfig,
-        key: CaptureRequest.Key<T>,
-        expectedValue: T,
-        assertMsg: String = ""
-    ) {
-        assertWithMessage(assertMsg).that(camera2Config.getCaptureRequestOption(key, null))
-            .isEqualTo(expectedValue)
-    }
-
-    private fun createCameraControl(
-        cameraId: String = CAMERA_ID_0,
-        quirks: Quirks? = null
-    ) {
-        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
-        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
-        val characteristicsCompat = CameraCharacteristicsCompat
-            .toCameraCharacteristicsCompat(characteristics)
-        val cameraQuirk = quirks ?: CameraQuirks.get(cameraId, characteristicsCompat)
-        val executorService = CameraXExecutors.newHandlerExecutor(handler)
-
-        cameraControl = Camera2CameraControlImpl(
-            characteristicsCompat,
-            executorService,
-            executorService,
-            controlUpdateCallback,
-            cameraQuirk
-        ).apply {
-            setActive(true)
-            incrementUseCount()
-        }
-    }
-
-    private fun initCameras() {
-        Shadow.extract<ShadowCameraManager>(
-            context.getSystemService(Context.CAMERA_SERVICE)
-        ).apply {
-            addCamera(CAMERA_ID_0, intiCharacteristic0())
-        }
-    }
-
-    private fun intiCharacteristic0(): CameraCharacteristics {
-        return ShadowCameraCharacteristics.newCameraCharacteristics().also {
-            Shadow.extract<ShadowCameraCharacteristics>(it).apply {
-                set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true)
-                set(
-                    CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
-                    intArrayOf(
-                        CaptureRequest.CONTROL_AE_MODE_OFF,
-                        CaptureRequest.CONTROL_AE_MODE_ON,
-                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH,
-                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
-                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE,
-                        CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
-                    )
-                )
-                set(
-                    CameraCharacteristics.LENS_FACING,
-                    CameraMetadata.LENS_FACING_BACK
-                )
-            }
-        }
-    }
-}
-
-private fun CaptureConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
-
-private fun SessionConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
new file mode 100644
index 0000000..c9bac65
--- /dev/null
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CapturePipelineTest.kt
@@ -0,0 +1,1016 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal
+
+import android.content.Context
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.os.Build
+import android.view.Surface
+import androidx.camera.camera2.impl.Camera2ImplConfig
+import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat
+import androidx.camera.camera2.internal.compat.quirk.AutoFlashUnderExposedQuirk
+import androidx.camera.camera2.internal.compat.quirk.CameraQuirks
+import androidx.camera.camera2.internal.compat.quirk.UseTorchAsFlashQuirk
+import androidx.camera.camera2.internal.compat.workaround.OverrideAeModeForStillCapture
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
+import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
+import androidx.camera.core.ImageCapture.FLASH_MODE_ON
+import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.impl.CameraCaptureFailure
+import androidx.camera.core.impl.CameraCaptureResult
+import androidx.camera.core.impl.CameraControlInternal
+import androidx.camera.core.impl.CaptureConfig
+import androidx.camera.core.impl.DeferrableSurface
+import androidx.camera.core.impl.ImmediateSurface
+import androidx.camera.core.impl.Quirks
+import androidx.camera.core.impl.SessionConfig
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.concurrent.futures.await
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.ListenableFuture
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+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.concurrent.CountDownLatch
+import java.util.concurrent.ExecutionException
+import java.util.concurrent.Executors
+import java.util.concurrent.ScheduledFuture
+import java.util.concurrent.TimeUnit
+
+private const val CAMERA_ID_0 = "0"
+
+@RunWith(RobolectricTestRunner::class)
+@DoNotInstrument
+@Config(
+    minSdk = Build.VERSION_CODES.LOLLIPOP,
+)
+class Camera2CapturePipelineTest {
+
+    private val context = ApplicationProvider.getApplicationContext() as Context
+    private val executorService = Executors.newSingleThreadScheduledExecutor()
+
+    private val baseRepeatingResult: Map<CaptureResult.Key<*>, Any> = mapOf(
+        CaptureResult.CONTROL_MODE to CaptureResult.CONTROL_MODE_AUTO,
+        CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+        CaptureResult.CONTROL_AWB_MODE to CaptureResult.CONTROL_AWB_MODE_AUTO,
+    )
+
+    private val resultConverged: Map<CaptureResult.Key<*>, Any> = mapOf(
+        CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+        CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+        CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_CONVERGED,
+        CaptureResult.CONTROL_AWB_STATE to CaptureResult.CONTROL_AWB_STATE_CONVERGED,
+    )
+
+    private val fakeStillCaptureSurface = ImmediateSurface(Surface(SurfaceTexture(0)))
+
+    private val singleRequest = CaptureConfig.Builder().apply {
+        templateType = CameraDevice.TEMPLATE_STILL_CAPTURE
+        addSurface(fakeStillCaptureSurface)
+    }.build()
+
+    private var runningRepeatingStream: ScheduledFuture<*>? = null
+        set(value) {
+            runningRepeatingStream?.cancel(false)
+            field = value
+        }
+
+    @Before
+    fun setUp() {
+        initCameras()
+    }
+
+    @After
+    fun tearDown() {
+        runningRepeatingStream = null
+        executorService.shutdownNow()
+    }
+
+    @Test
+    fun pipelineTest_preCapturePostCaptureShouldCalled() {
+        // Arrange.
+        val fakeTask = object : Camera2CapturePipeline.PipelineTask {
+            val preCaptureCountDown = CountDownLatch(1)
+            val postCaptureCountDown = CountDownLatch(1)
+
+            override fun preCapture(captureResult: TotalCaptureResult?): ListenableFuture<Boolean> {
+                preCaptureCountDown.countDown()
+                return Futures.immediateFuture(true)
+            }
+
+            override fun isCaptureResultNeeded(): Boolean {
+                return true
+            }
+
+            override fun postCapture() {
+                postCaptureCountDown.countDown()
+            }
+        }
+
+        val cameraControl = createCameraControl().apply {
+            simulateRepeatingResult(resultParameters = resultConverged)
+        }
+
+        val pipeline = Camera2CapturePipeline.Pipeline(
+            CameraDevice.TEMPLATE_PREVIEW,
+            Dispatchers.Default.asExecutor(),
+            cameraControl,
+            false,
+            OverrideAeModeForStillCapture(Quirks(emptyList())),
+        ).apply {
+            addTask(fakeTask)
+        }
+
+        // Act.
+        pipeline.executeCapture(
+            listOf(singleRequest),
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Assert.
+        assertTrue(fakeTask.preCaptureCountDown.await(3, TimeUnit.SECONDS))
+        assertTrue(fakeTask.postCaptureCountDown.await(3, TimeUnit.SECONDS))
+    }
+
+    @Test
+    fun maxQuality_afInactive_shouldTriggerAf(): Unit = runBlocking {
+        val cameraControl = createCameraControl().apply {
+
+            // Arrange. Simulate the scenario that we need to triggerAF.
+            simulateRepeatingResult(
+                initialDelay = 100,
+                resultParameters = mapOf(
+                    CaptureResult.CONTROL_AF_MODE to CaptureResult.CONTROL_AF_MODE_AUTO,
+                    CaptureResult.CONTROL_AF_STATE to CaptureResult.CONTROL_AF_STATE_INACTIVE,
+                )
+            )
+
+            // Act.
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            )
+        }
+
+        // Assert 1, verify the CONTROL_AF_TRIGGER is triggered
+        immediateCompleteCapture.verifyRequestResult {
+            it.requestContains(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_START
+            )
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2, that CONTROL_AF_TRIGGER should be cancelled finally.
+        immediateCompleteCapture.verifyRequestResult {
+            it.requestContains(
+                CaptureRequest.CONTROL_AF_TRIGGER,
+                CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
+            )
+        }
+    }
+
+    @Test
+    fun miniLatency_flashOn_shouldTriggerAe() {
+        flashOn_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+    }
+
+    @Test
+    fun maxQuality_flashOn_shouldTriggerAe() {
+        flashOn_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    private fun flashOn_shouldTriggerAe(imageCaptureMode: Int) {
+        val cameraControl = createCameraControl().apply {
+            // Arrange.
+            flashMode = FLASH_MODE_ON
+
+            // Act.
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                imageCaptureMode,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            )
+            simulateRepeatingResult(initialDelay = 100)
+        }
+
+        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+        immediateCompleteCapture.verifyRequestResult {
+            it.requestContains(
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+            )
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+        if (Build.VERSION.SDK_INT >= 23) {
+            immediateCompleteCapture.verifyRequestResult {
+                it.requestContains(
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+                )
+            }
+        }
+    }
+
+    @Test
+    fun miniLatency_flashAutoFlashRequired_shouldTriggerAe() {
+        flashAutoFlashRequired_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+    }
+
+    @Test
+    fun maxQuality_flashAutoFlashRequired_shouldTriggerAe(): Unit = runBlocking {
+        flashAutoFlashRequired_shouldTriggerAe(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    private fun flashAutoFlashRequired_shouldTriggerAe(imageCaptureMode: Int) {
+        val cameraControl = createCameraControl().apply {
+            // Arrange.
+            flashMode = FLASH_MODE_AUTO
+            simulateRepeatingResult(
+                initialDelay = 100,
+                resultParameters = mapOf(
+                    CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                )
+            )
+
+            // Act.
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                imageCaptureMode,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            )
+        }
+
+        // Assert 1, verify the CONTROL_AE_PRECAPTURE_TRIGGER is triggered
+        immediateCompleteCapture.verifyRequestResult {
+            it.requestContains(
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
+            )
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2 that CONTROL_AE_PRECAPTURE_TRIGGER should be cancelled finally.
+        if (Build.VERSION.SDK_INT >= 23) {
+            immediateCompleteCapture.verifyRequestResult {
+                it.requestContains(
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                    CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_CANCEL
+                )
+            }
+        }
+    }
+
+    @Test
+    fun miniLatency_withTorchAsFlashQuirk_shouldOpenTorch() {
+        withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+    }
+
+    @Test
+    fun maxQuality_withTorchAsFlashQuirk_shouldOpenTorch() {
+        withTorchAsFlashQuirk_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    private fun withTorchAsFlashQuirk_shouldOpenTorch(imageCaptureMode: Int) {
+        val cameraControl = createCameraControl(
+            // Arrange.
+            quirks = Quirks(listOf(object : UseTorchAsFlashQuirk {}))
+        ).apply {
+            flashMode = FLASH_MODE_ON
+            simulateRepeatingResult(initialDelay = 100)
+
+            // Act.
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                imageCaptureMode,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            )
+        }
+
+        // Assert 1 torch should be turned on
+        cameraControl.waitForSessionConfig {
+            it.isTorchParameterEnabled()
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2 torch should be turned off
+        immediateCompleteCapture.verifyRequestResult {
+            it.isTorchParameterDisabled()
+        }
+    }
+
+    @Test
+    fun miniLatency_withTemplateRecord_shouldOpenTorch() {
+        withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+    }
+
+    @Test
+    fun maxQuality_withTemplateRecord_shouldOpenTorch() {
+        withTemplateRecord_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    private fun withTemplateRecord_shouldOpenTorch(imageCaptureMode: Int) {
+
+        val cameraControl = createCameraControl().apply {
+            // Arrange.
+            setTemplate(CameraDevice.TEMPLATE_RECORD)
+            flashMode = FLASH_MODE_ON
+            simulateRepeatingResult(initialDelay = 100)
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                imageCaptureMode,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            )
+        }
+
+        // Assert 1 torch should be turned on
+        cameraControl.waitForSessionConfig {
+            it.isTorchParameterEnabled()
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2 torch should be turned off
+        immediateCompleteCapture.verifyRequestResult {
+            it.isTorchParameterDisabled()
+        }
+    }
+
+    @Test
+    fun miniLatency_withFlashTypeTorch_shouldOpenTorch() {
+        withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
+    }
+
+    @Test
+    fun maxQuality_withFlashTypeTorch_shouldOpenTorch() {
+        withFlashTypeTorch_shouldOpenTorch(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY)
+    }
+
+    private fun withFlashTypeTorch_shouldOpenTorch(imageCaptureMode: Int) {
+        val cameraControl = createCameraControl().apply {
+            flashMode = FLASH_MODE_ON
+            simulateRepeatingResult(initialDelay = 100)
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                imageCaptureMode,
+                ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH,
+            )
+        }
+
+        // Assert 1 torch should be turned on
+        cameraControl.waitForSessionConfig {
+            it.isTorchParameterEnabled()
+        }
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert 2 torch should be turned off
+        immediateCompleteCapture.verifyRequestResult {
+            it.isTorchParameterDisabled()
+        }
+    }
+
+    @Test
+    fun miniLatency_shouldNoPreCapture(): Unit = runBlocking {
+        // Arrange.
+        val cameraControl = createCameraControl().apply {
+            simulateRepeatingResult(initialDelay = 100)
+        }
+
+        // Act.
+        cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        ).await()
+
+        // Assert, there is only 1 single capture request.
+        assertThat(immediateCompleteCapture.allResults.size).isEqualTo(1)
+    }
+
+    @Test
+    fun submitStillCaptureRequests_withTemplate_templateSent(): Unit = runBlocking {
+        // Arrange.
+        val imageCaptureConfig = CaptureConfig.Builder().let {
+            it.addSurface(fakeStillCaptureSurface)
+            it.templateType = CameraDevice.TEMPLATE_MANUAL
+            it.build()
+        }
+        val cameraControl = createCameraControl().apply {
+            simulateRepeatingResult(initialDelay = 100)
+        }
+
+        // Act.
+        cameraControl.submitStillCaptureRequests(
+            listOf(imageCaptureConfig),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        ).await()
+
+        // Assert.
+        immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+            captureConfigList.filter {
+                it.surfaces.contains(fakeStillCaptureSurface)
+            }.map { captureConfig ->
+                captureConfig.templateType
+            }.contains(CameraDevice.TEMPLATE_MANUAL)
+        }
+    }
+
+    @Test
+    fun submitStillCaptureRequests_withNoTemplate_templateStillCaptureSent(): Unit = runBlocking {
+        // Arrange.
+        val imageCaptureConfig = CaptureConfig.Builder().apply {
+            addSurface(fakeStillCaptureSurface)
+        }.build()
+        val cameraControl = createCameraControl().apply {
+            simulateRepeatingResult(initialDelay = 100)
+        }
+
+        // Act.
+        cameraControl.submitStillCaptureRequests(
+            listOf(imageCaptureConfig),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        ).await()
+
+        // Assert.
+        immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+            captureConfigList.filter {
+                it.surfaces.contains(fakeStillCaptureSurface)
+            }.map { captureConfig ->
+                captureConfig.templateType
+            }.contains(CameraDevice.TEMPLATE_STILL_CAPTURE)
+        }
+    }
+
+    @Test
+    fun submitStillCaptureRequests_withTemplateRecord_templateVideoSnapshotSent(): Unit =
+        runBlocking {
+        createCameraControl().apply {
+            // Arrange.
+            setTemplate(CameraDevice.TEMPLATE_RECORD)
+            simulateRepeatingResult(initialDelay = 100)
+
+            // Act.
+            submitStillCaptureRequests(
+                listOf(singleRequest),
+                ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+                ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+            ).await()
+        }
+
+        // Assert.
+        immediateCompleteCapture.verifyRequestResult { captureConfigList ->
+            captureConfigList.filter {
+                it.surfaces.contains(fakeStillCaptureSurface)
+            }.map { captureConfig ->
+                captureConfig.templateType
+            }.contains(CameraDevice.TEMPLATE_VIDEO_SNAPSHOT)
+        }
+    }
+
+    @Test
+    fun captureFailure_taskShouldFailure() {
+        // Arrange.
+        val immediateFailureCapture =
+            object : CameraControlInternal.ControlUpdateCallback {
+
+                override fun onCameraControlUpdateSessionConfig() {
+                }
+
+                override fun onCameraControlCaptureRequests(
+                    captureConfigs: MutableList<CaptureConfig>
+                ) {
+                    captureConfigs.forEach { captureConfig ->
+                        captureConfig.cameraCaptureCallbacks.forEach {
+                            it.onCaptureFailed(
+                                CameraCaptureFailure(
+                                    CameraCaptureFailure.Reason.ERROR
+                                )
+                            )
+                        }
+                    }
+                }
+            }
+        val cameraControl = createCameraControl(updateCallback = immediateFailureCapture)
+
+        // Act.
+        val future = cameraControl.submitStillCaptureRequests(
+            listOf(CaptureConfig.Builder().build()),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Assert.
+        val exception = assertThrows(ExecutionException::class.java) {
+            future.get(1, TimeUnit.SECONDS)
+        }
+        assertTrue(exception.cause is ImageCaptureException)
+        assertThat((exception.cause as ImageCaptureException).imageCaptureError).isEqualTo(
+            ImageCapture.ERROR_CAPTURE_FAILED
+        )
+    }
+
+    @Test
+    fun captureCancel_taskShouldFailureWithCAMERA_CLOSED() {
+        // Arrange.
+        val immediateCancelCapture =
+            object : CameraControlInternal.ControlUpdateCallback {
+
+                override fun onCameraControlUpdateSessionConfig() {
+                }
+
+                override fun onCameraControlCaptureRequests(
+                    captureConfigs: MutableList<CaptureConfig>
+                ) {
+                    captureConfigs.forEach { captureConfig ->
+                        captureConfig.cameraCaptureCallbacks.forEach {
+                            it.onCaptureCancelled()
+                        }
+                    }
+                }
+            }
+        val cameraControl = createCameraControl(updateCallback = immediateCancelCapture)
+
+        // Act.
+        val future = cameraControl.submitStillCaptureRequests(
+            listOf(CaptureConfig.Builder().build()),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Assert.
+        val exception = assertThrows(ExecutionException::class.java) {
+            future.get(1, TimeUnit.SECONDS)
+        }
+        assertTrue(exception.cause is ImageCaptureException)
+        assertThat((exception.cause as ImageCaptureException).imageCaptureError).isEqualTo(
+            ImageCapture.ERROR_CAMERA_CLOSED
+        )
+    }
+
+    @Test
+    fun overrideAeModeForStillCapture_quirkAbsent_notOverride(): Unit = runBlocking {
+        // Arrange. Not have the quirk.
+        val cameraControl = createCameraControl(quirks = Quirks(emptyList())).apply {
+            flashMode = FLASH_MODE_ON // Set flash ON to enable aePreCapture
+            simulateRepeatingResult(initialDelay = 100) // Make sures flashMode is updated.
+        }
+
+        // Act.
+        val deferred = cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        deferred.await()
+
+        // Assert.
+        // AE mode should not be overridden
+        immediateCompleteCapture.allResults.toList().flatten().filter {
+            it.surfaces.contains(fakeStillCaptureSurface)
+        }.let { stillCaptureRequests ->
+            assertThat(stillCaptureRequests).isNotEmpty()
+            stillCaptureRequests.forEach { config ->
+                assertThat(
+                    config.toCamera2Config()
+                        .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+                ).isNull()
+            }
+        }
+    }
+
+    @Test
+    fun overrideAeModeForStillCapture_aePrecaptureStarted_override(): Unit = runBlocking {
+        // Arrange.
+        val cameraControl = createCameraControl(
+            quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))
+        ).apply {
+            flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+            simulateRepeatingResult(
+                initialDelay = 100,
+                resultParameters = mapOf(
+                    CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                )
+            ) // Make sures flashMode is updated and the flash is required.
+        }
+
+        // Act.
+        cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        // Assert.
+        // AE mode should be overridden to CONTROL_AE_MODE_ON_ALWAYS_FLASH
+        immediateCompleteCapture.verifyRequestResult { configList ->
+            configList.requestContains(
+                CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
+            ) && configList.surfaceContains(fakeStillCaptureSurface)
+        }
+    }
+
+    @Test
+    fun overrideAeModeForStillCapture_aePrecaptureFinish_notOverride(): Unit = runBlocking {
+        // Arrange.
+        val cameraControl = createCameraControl(
+            quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))
+        ).apply {
+            flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+            simulateRepeatingResult(
+                initialDelay = 100,
+                resultParameters = mapOf(
+                    CaptureResult.CONTROL_AE_STATE to CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                )
+            ) // Make sures flashMode is updated and the flash is required.
+        }
+        val firstCapture = cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+        firstCapture.await()
+        immediateCompleteCapture.allResults.clear() // Clear the result of the firstCapture
+
+        // Act.
+        // Set flash OFF to disable aePreCapture for testing
+        cameraControl.flashMode = FLASH_MODE_OFF
+        cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        ).await()
+
+        // Assert. The second capturing should not override the AE mode.
+        immediateCompleteCapture.allResults.toList().flatten().filter {
+            it.surfaces.contains(fakeStillCaptureSurface)
+        }.let { stillCaptureRequests ->
+            assertThat(stillCaptureRequests).isNotEmpty()
+            stillCaptureRequests.forEach { config ->
+                assertThat(
+                    config.toCamera2Config()
+                        .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+                ).isNull()
+            }
+        }
+    }
+
+    @Test
+    fun overrideAeModeForStillCapture_noAePrecaptureTriggered_notOverride(): Unit = runBlocking {
+        // Arrange.
+        val cameraControl =
+            createCameraControl(quirks = Quirks(listOf(AutoFlashUnderExposedQuirk()))).apply {
+                flashMode = FLASH_MODE_AUTO // Set flash auto to enable aePreCapture
+
+                // Make sures flashMode is updated but the flash is not required.
+                simulateRepeatingResult(initialDelay = 100)
+            }
+
+        // Act.
+        val deferred = cameraControl.submitStillCaptureRequests(
+            listOf(singleRequest),
+            ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY,
+            ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH,
+        )
+
+        // Switch the repeating result to 3A converged state.
+        cameraControl.simulateRepeatingResult(
+            initialDelay = 500,
+            resultParameters = resultConverged
+        )
+
+        deferred.await()
+
+        // Assert.
+        // AE mode should not be overridden
+        immediateCompleteCapture.allResults.toList().flatten().filter {
+            it.surfaces.contains(fakeStillCaptureSurface)
+        }.let { stillCaptureRequests ->
+            assertThat(stillCaptureRequests).isNotEmpty()
+            stillCaptureRequests.forEach { config ->
+                assertThat(
+                    config.toCamera2Config()
+                        .getCaptureRequestOption(CaptureRequest.CONTROL_AE_MODE)
+                ).isNull()
+            }
+        }
+    }
+
+    private fun Camera2CameraControlImpl.waitForSessionConfig(
+        checkResult: (sessionConfig: SessionConfig) -> Boolean = { true }
+    ) {
+        while (true) {
+            immediateCompleteCapture.waitForSessionConfigUpdate()
+            if (checkResult(sessionConfig)) {
+                return
+            }
+        }
+    }
+
+    private fun SessionConfig.isTorchParameterEnabled(): Boolean {
+        val config = toCamera2Config()
+
+        return config.getCaptureRequestOption(
+            CaptureRequest.CONTROL_AE_MODE,
+            null
+        ) == CaptureRequest.CONTROL_AE_MODE_ON && config.getCaptureRequestOption(
+            CaptureRequest.FLASH_MODE,
+            null
+        ) == CameraMetadata.FLASH_MODE_TORCH
+    }
+
+    private fun List<CaptureConfig>.isTorchParameterDisabled() =
+        requestContains(
+            CaptureRequest.CONTROL_AE_MODE,
+            CaptureRequest.CONTROL_AE_MODE_ON,
+        ) && requestContains(
+            CaptureRequest.FLASH_MODE,
+            CaptureRequest.FLASH_MODE_OFF,
+        )
+
+    private fun List<CaptureConfig>.requestContains(
+        key: CaptureRequest.Key<*>,
+        value: Any?
+    ): Boolean {
+        forEach { config ->
+            if (value == config.toCamera2Config().getCaptureRequestOption(key, null)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun List<CaptureConfig>.surfaceContains(
+        surface: DeferrableSurface
+    ): Boolean {
+        forEach { config ->
+            if (config.surfaces.contains(surface)) {
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun Camera2CameraControlImpl.simulateRepeatingResult(
+        initialDelay: Long = 100,
+        period: Long = 100, // in milliseconds
+        resultParameters: Map<CaptureResult.Key<*>, Any> = mutableMapOf(),
+    ) {
+        executorService.schedule({
+            runningRepeatingStream = executorService.scheduleAtFixedRate({
+                val tagBundle = sessionConfig.repeatingCaptureConfig.tagBundle
+                val requestOptions = sessionConfig.repeatingCaptureConfig.implementationOptions
+                val resultOptions = baseRepeatingResult.toMutableMap().apply {
+                    putAll(resultParameters)
+                }
+                sendRepeatingResult(tagBundle, requestOptions.toParameters(), resultOptions)
+            }, 0, period, TimeUnit.MILLISECONDS)
+        }, initialDelay, TimeUnit.MILLISECONDS)
+    }
+
+    private fun Camera2CameraControlImpl.sendRepeatingResult(
+        requestTag: Any? = null,
+        requestParameters: Map<CaptureRequest.Key<*>, Any>,
+        resultParameters: Map<CaptureResult.Key<*>, Any>,
+    ) {
+        val request = mock(CaptureRequest::class.java)
+        Mockito.`when`(request.tag).thenReturn(requestTag)
+        requestParameters.forEach { (key, any) ->
+            Mockito.`when`(request.get(key)).thenReturn(any)
+        }
+
+        val result = mock(TotalCaptureResult::class.java)
+        Mockito.`when`(result.request).thenReturn(request)
+        resultParameters.forEach { (key, any) ->
+            Mockito.`when`(result.get(key)).thenReturn(any)
+        }
+
+        sessionConfig.repeatingCameraCaptureCallbacks.toList().forEach {
+            CaptureCallbackConverter.toCaptureCallback(it).onCaptureCompleted(
+                mock(CameraCaptureSession::class.java), request, result
+            )
+        }
+    }
+
+    private fun CaptureConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
+
+    private fun SessionConfig.toCamera2Config() = Camera2ImplConfig(implementationOptions)
+
+    private fun createCameraControl(
+        cameraId: String = CAMERA_ID_0,
+        quirks: Quirks? = null,
+        updateCallback: CameraControlInternal.ControlUpdateCallback = immediateCompleteCapture,
+    ): Camera2CameraControlImpl {
+        val cameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+        val characteristicsCompat = CameraCharacteristicsCompat
+            .toCameraCharacteristicsCompat(characteristics)
+        val cameraQuirk = quirks ?: CameraQuirks.get(cameraId, characteristicsCompat)
+        val executorService = Executors.newSingleThreadScheduledExecutor()
+
+        return Camera2CameraControlImpl(
+            characteristicsCompat,
+            executorService,
+            executorService,
+            updateCallback,
+            cameraQuirk
+        ).apply {
+            setActive(true)
+            incrementUseCount()
+        }
+    }
+
+    private fun initCameras() {
+        Shadow.extract<ShadowCameraManager>(
+            context.getSystemService(Context.CAMERA_SERVICE)
+        ).apply {
+            addCamera(CAMERA_ID_0, intiCharacteristic0())
+        }
+    }
+
+    private fun intiCharacteristic0(): CameraCharacteristics {
+        return ShadowCameraCharacteristics.newCameraCharacteristics().also {
+            Shadow.extract<ShadowCameraCharacteristics>(it).apply {
+                set(CameraCharacteristics.FLASH_INFO_AVAILABLE, true)
+                set(
+                    CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES,
+                    intArrayOf(
+                        CaptureRequest.CONTROL_AE_MODE_OFF,
+                        CaptureRequest.CONTROL_AE_MODE_ON,
+                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH,
+                        CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH,
+                        CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE,
+                        CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH
+                    )
+                )
+                set(
+                    CameraCharacteristics.LENS_FACING,
+                    CameraMetadata.LENS_FACING_BACK
+                )
+            }
+        }
+    }
+
+    private val immediateCompleteCapture =
+        object : CameraControlInternal.ControlUpdateCallback {
+            private val lock = Any()
+            val allResults: MutableList<List<CaptureConfig>> = mutableListOf()
+            val waitingList = mutableListOf<Pair<CountDownLatch,
+                    (captureRequests: List<CaptureConfig>) -> Boolean>>()
+            var updateSessionCountDown: CountDownLatch? = null
+
+            fun verifyRequestResult(
+                timeout: Long = TimeUnit.SECONDS.toMillis(5),
+                verifyResults: (captureRequests: List<CaptureConfig>) -> Boolean = { true }
+            ) {
+                synchronized(lock) {
+                    allResults.forEach {
+                        if (verifyResults(it)) {
+                            return
+                        }
+                    }
+                }
+                val resultPair = Pair(CountDownLatch(1), verifyResults)
+                waitingList.add(resultPair)
+                assertTrue(resultPair.first.await(timeout, TimeUnit.MILLISECONDS))
+                waitingList.remove(resultPair)
+            }
+
+            fun waitForSessionConfigUpdate(timeout: Long = TimeUnit.SECONDS.toMillis(5)) {
+                if (updateSessionCountDown == null) {
+                    updateSessionCountDown = CountDownLatch(1)
+                }
+                assertTrue(updateSessionCountDown!!.await(timeout, TimeUnit.MILLISECONDS))
+            }
+
+            override fun onCameraControlUpdateSessionConfig() {
+                updateSessionCountDown?.countDown()
+                updateSessionCountDown = null
+            }
+
+            override fun onCameraControlCaptureRequests(
+                captureConfigs: MutableList<CaptureConfig>
+            ) {
+                synchronized(lock) {
+                    allResults.add(captureConfigs)
+                }
+                waitingList.toList().forEach {
+                    if (it.second(captureConfigs)) {
+                        it.first.countDown()
+                    }
+                }
+
+                // Complete the single capture with an empty result.
+                captureConfigs.forEach { captureConfig ->
+                    captureConfig.cameraCaptureCallbacks.forEach {
+                        it.onCaptureCompleted(CameraCaptureResult.EmptyCameraCaptureResult())
+                    }
+                }
+            }
+        }
+
+    /**
+     * Convert the Config to the CaptureRequest key-value map.
+     */
+    private fun androidx.camera.core.impl.Config.toParameters(): Map<CaptureRequest.Key<*>, Any> {
+        val parameters = mutableMapOf<CaptureRequest.Key<*>, Any>()
+        for (configOption in listOptions()) {
+            val requestKey = configOption.token as? CaptureRequest.Key<*> ?: continue
+            val value = retrieveOption(configOption) ?: continue
+            parameters[requestKey] = value
+        }
+
+        return parameters
+    }
+}
\ No newline at end of file
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
index 4564f50..0738fcd 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/compat/params/InputConfigurationCompatTest.java
@@ -20,6 +20,7 @@
 
 import android.graphics.ImageFormat;
 import android.hardware.camera2.params.InputConfiguration;
+import android.hardware.camera2.params.MultiResolutionStreamInfo;
 import android.os.Build;
 
 import org.junit.Test;
@@ -28,6 +29,9 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
+import java.util.ArrayList;
+import java.util.List;
+
 @RunWith(RobolectricTestRunner.class)
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
@@ -36,6 +40,7 @@
     private static final int WIDTH = 1024;
     private static final int HEIGHT = 768;
     private static final int FORMAT = ImageFormat.YUV_420_888;
+    private static final String CAMERA_ID = "0";
 
     @Test
     public void canCreateInputConfigurationCompat() {
@@ -75,7 +80,7 @@
     }
 
     @Test
-    @Config(minSdk = Build.VERSION_CODES.M)
+    @Config(minSdk = Build.VERSION_CODES.M, maxSdk = Build.VERSION_CODES.R)
     public void baseImplHashCodeMatchesFramework() {
         InputConfigurationCompat.InputConfigurationCompatBaseImpl baseImpl =
                 new InputConfigurationCompat.InputConfigurationCompatBaseImpl(WIDTH, HEIGHT,
@@ -87,7 +92,7 @@
     }
 
     @Test
-    @Config(minSdk = Build.VERSION_CODES.M)
+    @Config(minSdk = Build.VERSION_CODES.M, maxSdk = Build.VERSION_CODES.R)
     public void baseImplToStringMatchesFramework() {
         InputConfigurationCompat.InputConfigurationCompatBaseImpl baseImpl =
                 new InputConfigurationCompat.InputConfigurationCompatBaseImpl(WIDTH, HEIGHT,
@@ -97,4 +102,17 @@
 
         assertThat(baseImpl.toString()).isEqualTo(config.toString());
     }
+
+    @Test
+    @Config(minSdk = Build.VERSION_CODES.S)
+    public void isMultiResolutionMatchesFramework() {
+        List<MultiResolutionStreamInfo> multiResolutionInputs = new ArrayList<>();
+        multiResolutionInputs.add(new MultiResolutionStreamInfo(WIDTH, HEIGHT, CAMERA_ID));
+
+        InputConfiguration inputConfig = new InputConfiguration(multiResolutionInputs, FORMAT);
+        InputConfigurationCompat compat = InputConfigurationCompat.wrap(inputConfig);
+
+        assertThat(compat).isNotNull();
+        assertThat(compat.isMultiResolution()).isEqualTo(inputConfig.isMultiResolution());
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 7c78c0d..65f8b14 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -54,7 +54,6 @@
 import android.net.Uri;
 import android.os.Build;
 import android.os.Looper;
-import android.os.SystemClock;
 import android.provider.MediaStore;
 import android.util.Log;
 import android.util.Pair;
@@ -75,13 +74,6 @@
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
 import androidx.camera.core.impl.CameraCaptureCallback;
-import androidx.camera.core.impl.CameraCaptureFailure;
-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.CameraCaptureResult;
-import androidx.camera.core.impl.CameraCaptureResult.EmptyCameraCaptureResult;
 import androidx.camera.core.impl.CameraInfoInternal;
 import androidx.camera.core.impl.CameraInternal;
 import androidx.camera.core.impl.CaptureBundle;
@@ -107,7 +99,6 @@
 import androidx.camera.core.impl.utils.Threads;
 import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.FutureChain;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.core.internal.IoConfig;
 import androidx.camera.core.internal.TargetConfig;
@@ -131,10 +122,8 @@
 import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Deque;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
-import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.Executor;
@@ -245,8 +234,6 @@
     public static final Defaults DEFAULT_CONFIG = new Defaults();
     private static final String TAG = "ImageCapture";
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
-    private static final long CHECK_3A_TIMEOUT_IN_MS = 1000L;
-    private static final long CHECK_3A_WITH_FLASH_TIMEOUT_IN_MS = 5000L;
     private static final int MAX_IMAGES = 2;
     // TODO(b/149336664) Move the quality to a compatibility class when there is a per device case.
     private static final byte JPEG_QUALITY_MAXIMIZE_QUALITY_MODE = 100;
@@ -256,8 +243,6 @@
     @FlashMode
     private static final int DEFAULT_FLASH_MODE = FLASH_MODE_OFF;
 
-    private final CaptureCallbackChecker mSessionCallbackChecker = new CaptureCallbackChecker();
-
     private final ImageReaderProxy.OnImageAvailableListener mClosingListener = (imageReader -> {
         try (ImageProxy image = imageReader.acquireLatestImage()) {
             Log.d(TAG, "Discarding ImageProxy which was inadvertently acquired: " + image);
@@ -272,15 +257,6 @@
     @CaptureMode
     private final int mCaptureMode;
 
-    /**
-     * A flag to check 3A converged or not.
-     *
-     * <p>In order to speed up the taking picture process, trigger AF / AE should be skipped when
-     * the flag is disabled. Set it to be enabled in the maximum quality mode and disabled in the
-     * minimum latency mode.
-     */
-    private final boolean mEnableCheck3AConverged;
-
     @GuardedBy("mLockedFlashMode")
     private final AtomicReference<Integer> mLockedFlashMode = new AtomicReference<>(null);
 
@@ -368,11 +344,6 @@
                 useCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor()));
         mSequentialIoExecutor = CameraXExecutors.newSequentialExecutor(mIoExecutor);
 
-        if (mCaptureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
-            mEnableCheck3AConverged = true; // check 3A convergence in MAX_QUALITY mode
-        } else {
-            mEnableCheck3AConverged = false; // skip 3A convergence in MIN_LATENCY mode
-        }
     }
 
     @UiThread
@@ -381,7 +352,6 @@
             @NonNull ImageCaptureConfig config, @NonNull Size resolution) {
         Threads.checkMainThread();
         SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
-        sessionConfigBuilder.addRepeatingCameraCaptureCallback(mSessionCallbackChecker);
         YuvToJpegProcessor softwareJpegProcessor = null;
 
         // Setup the ImageReader to do processing
@@ -1123,7 +1093,7 @@
         }
     }
 
-    private void unlockFlashMode() {
+    void unlockFlashMode() {
         synchronized (mLockedFlashMode) {
             Integer lockedFlashMode = mLockedFlashMode.getAndSet(null);
             if (lockedFlashMode == null) {
@@ -1208,20 +1178,19 @@
                             },
                             CameraXExecutors.mainThreadExecutor());
 
-                    TakePictureState state = new TakePictureState();
-                    ListenableFuture<Void> future = FutureChain.from(preTakePicture(state))
-                            .transformAsync(v -> issueTakePicture(imageCaptureRequest), mExecutor);
+                    lockFlashMode();
+                    ListenableFuture<Void> future = issueTakePicture(imageCaptureRequest);
 
                     Futures.addCallback(future,
                             new FutureCallback<Void>() {
                                 @Override
                                 public void onSuccess(Void result) {
-                                    postTakePicture(state);
+                                    unlockFlashMode();
                                 }
 
                                 @Override
                                 public void onFailure(Throwable throwable) {
-                                    postTakePicture(state);
+                                    unlockFlashMode();
 
                                     completer.setException(throwable);
                                 }
@@ -1425,8 +1394,8 @@
     static int getError(Throwable throwable) {
         if (throwable instanceof CameraClosedException) {
             return ERROR_CAMERA_CLOSED;
-        } else if (throwable instanceof CaptureFailedException) {
-            return ERROR_CAPTURE_FAILED;
+        } else if (throwable instanceof ImageCaptureException) {
+            return ((ImageCaptureException) throwable).getImageCaptureError();
         } else {
             return ERROR_UNKNOWN;
         }
@@ -1541,183 +1510,6 @@
     }
 
     /**
-     * Routine before taking picture.
-     *
-     * <p>For example, trigger 3A scan, open torch and check 3A converged if necessary.
-     */
-    private ListenableFuture<Void> preTakePicture(final TakePictureState state) {
-        lockFlashMode();
-        return FutureChain.from(getPreCaptureStateIfNeeded())
-                .transformAsync(captureResult -> {
-                    state.mPreCaptureState = captureResult;
-                    triggerAfIfNeeded(state);
-                    if (isFlashRequired(state)) {
-                        return startFlashSequence(state);
-                    }
-                    return Futures.immediateFuture(null);
-                }, mExecutor)
-                .transformAsync(v -> check3AConverged(state), mExecutor)
-                // Ignore the 3A convergence result.
-                .transform(is3AConverged -> null, mExecutor);
-    }
-
-    /**
-     * Routine after picture was taken.
-     *
-     * <p>For example, cancel 3A scan, close torch if necessary.
-     */
-    void postTakePicture(final TakePictureState state) {
-        cancelAfAndFinishFlashSequence(state);
-        unlockFlashMode();
-    }
-
-    /**
-     * Gets a capture result or not according to current configuration.
-     *
-     * <p>Conditions to get a capture result.
-     *
-     * <p>(1) The enableCheck3AConverged is enabled because it needs to know current AF mode and
-     * state.
-     *
-     * <p>(2) The flashMode is AUTO because it needs to know the current AE state.
-     */
-    // Currently this method is used to prevent there is no repeating surface to get capture result.
-    // If app is in min-latency mode and flash ALWAYS/OFF mode, it can still take picture without
-    // checking the capture result. Remove this check once no repeating surface issue is fixed.
-    private ListenableFuture<CameraCaptureResult> getPreCaptureStateIfNeeded() {
-        if (mEnableCheck3AConverged || getFlashMode() == FLASH_MODE_AUTO) {
-            return mSessionCallbackChecker.checkCaptureResult(
-                    new CaptureCallbackChecker.CaptureResultChecker<CameraCaptureResult>() {
-                        @Override
-                        public CameraCaptureResult check(
-                                @NonNull CameraCaptureResult captureResult) {
-                            if (Logger.isDebugEnabled(TAG)) {
-                                Logger.d(TAG, "preCaptureState, AE=" + captureResult.getAeState()
-                                        + " AF =" + captureResult.getAfState()
-                                        + " AWB=" + captureResult.getAwbState());
-                            }
-                            return captureResult;
-                        }
-                    });
-        }
-        return Futures.immediateFuture(null);
-    }
-
-    boolean isFlashRequired(@NonNull TakePictureState state) {
-        switch (getFlashMode()) {
-            case FLASH_MODE_ON:
-                return true;
-            case FLASH_MODE_AUTO:
-                return state.mPreCaptureState.getAeState() == AeState.FLASH_REQUIRED;
-            case FLASH_MODE_OFF:
-                return false;
-        }
-        throw new AssertionError(getFlashMode());
-    }
-
-    ListenableFuture<Boolean> check3AConverged(TakePictureState state) {
-        if (!mEnableCheck3AConverged && !state.mIsFlashSequenceStarted) {
-            return Futures.immediateFuture(false);
-        }
-
-        long waitTimeout = CHECK_3A_TIMEOUT_IN_MS;
-        if (state.mIsFlashSequenceStarted) {
-            waitTimeout = CHECK_3A_WITH_FLASH_TIMEOUT_IN_MS;
-        }
-
-        return mSessionCallbackChecker.checkCaptureResult(
-                new CaptureCallbackChecker.CaptureResultChecker<Boolean>() {
-                    @Override
-                    public Boolean check(@NonNull CameraCaptureResult captureResult) {
-                        if (Logger.isDebugEnabled(TAG)) {
-                            Logger.d(TAG, "checkCaptureResult, AE=" + captureResult.getAeState()
-                                    + " AF =" + captureResult.getAfState()
-                                    + " AWB=" + captureResult.getAwbState());
-                        }
-
-                        if (is3AConverged(captureResult)) {
-                            return true;
-                        }
-                        // Return null to continue check.
-                        return null;
-                    }
-                },
-                waitTimeout,
-                false);
-    }
-
-    boolean is3AConverged(CameraCaptureResult captureResult) {
-        if (captureResult == null) {
-            return false;
-        }
-
-        // If afMode is OFF or UNKNOWN , no need for waiting.
-        // otherwise wait until af is locked or focused.
-        boolean isAfReady = captureResult.getAfMode() == AfMode.OFF
-                || captureResult.getAfMode() == AfMode.UNKNOWN
-                || captureResult.getAfState() == AfState.PASSIVE_FOCUSED
-                || captureResult.getAfState() == AfState.PASSIVE_NOT_FOCUSED
-                || captureResult.getAfState() == AfState.LOCKED_FOCUSED
-                || captureResult.getAfState() == AfState.LOCKED_NOT_FOCUSED;
-
-        // Unknown means cannot get valid state from CaptureResult
-        boolean isAeReady = captureResult.getAeState() == AeState.CONVERGED
-                || captureResult.getAeState() == AeState.FLASH_REQUIRED
-                || captureResult.getAeState() == AeState.UNKNOWN;
-
-        // Unknown means cannot get valid state from CaptureResult
-        boolean isAwbReady = captureResult.getAwbState() == AwbState.CONVERGED
-                || captureResult.getAwbState() == AwbState.UNKNOWN;
-
-        return (isAfReady && isAeReady && isAwbReady);
-    }
-
-    /**
-     * Issues the AF scan if needed.
-     *
-     * <p>If enableCheck3AConverged is disabled or it is in CAF mode, AF scan should not be
-     * triggered. Trigger AF scan only in {@link AfMode#ON_MANUAL_AUTO} and current AF state is
-     * {@link AfState#INACTIVE}. If the AF mode is {@link AfMode#ON_MANUAL_AUTO} and AF state is not
-     * inactive, it means that a manual or auto focus request may be in progress or completed.
-     */
-    void triggerAfIfNeeded(TakePictureState state) {
-        if (mEnableCheck3AConverged
-                && state.mPreCaptureState.getAfMode() == AfMode.ON_MANUAL_AUTO
-                && state.mPreCaptureState.getAfState() == AfState.INACTIVE) {
-            triggerAf(state);
-        }
-    }
-
-    /** Issues a request to start auto focus scan. */
-    private void triggerAf(TakePictureState state) {
-        Logger.d(TAG, "triggerAf");
-        state.mIsAfTriggered = true;
-        ListenableFuture<CameraCaptureResult> future = getCameraControl().triggerAf();
-        // Add listener to avoid FutureReturnValueIgnored error.
-        future.addListener(() -> {
-        }, CameraXExecutors.directExecutor());
-    }
-
-    /** Issues a request to start auto exposure scan. */
-    @NonNull
-    ListenableFuture<Void> startFlashSequence(@NonNull TakePictureState state) {
-        Logger.d(TAG, "startFlashSequence");
-        state.mIsFlashSequenceStarted = true;
-        return getCameraControl().startFlashSequence(mFlashType);
-    }
-
-    /** Issues a request to cancel auto focus and/or auto exposure scan. */
-    void cancelAfAndFinishFlashSequence(@NonNull TakePictureState state) {
-        if (!state.mIsAfTriggered && !state.mIsFlashSequenceStarted) {
-            return;
-        }
-        getCameraControl().cancelAfAndFinishFlashSequence(state.mIsAfTriggered,
-                state.mIsFlashSequenceStarted);
-        state.mIsAfTriggered = false;
-        state.mIsFlashSequenceStarted = false;
-    }
-
-    /**
      * Initiates a set of captures that will be used to create the output of
      * {@link #takePicture(OutputFileOptions, Executor, OnImageSavedCallback)} and its variants.
      *
@@ -1729,7 +1521,6 @@
     ListenableFuture<Void> issueTakePicture(@NonNull ImageCaptureRequest imageCaptureRequest) {
         Logger.d(TAG, "issueTakePicture");
 
-        final List<ListenableFuture<Void>> futureList = new ArrayList<>();
         final List<CaptureConfig> captureConfigs = new ArrayList<>();
         String tagBundleKey = null;
 
@@ -1793,61 +1584,14 @@
                 builder.addTag(tagBundleKey, captureStage.getId());
             }
             builder.addCameraCaptureCallback(mMetadataMatchingCaptureCallback);
-
-            ListenableFuture<Void> future = CallbackToFutureAdapter.getFuture(
-                    completer -> {
-                        CameraCaptureCallback completerCallback = new CameraCaptureCallback() {
-                            @Override
-                            public void onCaptureCompleted(
-                                    @NonNull CameraCaptureResult result) {
-                                completer.set(null);
-                            }
-
-                            @Override
-                            public void onCaptureFailed(
-                                    @NonNull CameraCaptureFailure failure) {
-                                String msg = "Capture request failed with reason "
-                                        + failure.getReason();
-                                completer.setException(new CaptureFailedException(msg));
-                            }
-
-                            @Override
-                            public void onCaptureCancelled() {
-                                String msg = "Capture request is cancelled because "
-                                        + "camera is closed";
-                                completer.setException(new CameraClosedException(msg));
-                            }
-                        };
-                        builder.addCameraCaptureCallback(completerCallback);
-
-                        captureConfigs.add(builder.build());
-                        return "issueTakePicture[stage=" + captureStage.getId() + "]";
-                    });
-            futureList.add(future);
-
+            captureConfigs.add(builder.build());
         }
 
-        getCameraControl().submitStillCaptureRequests(captureConfigs);
-        return Futures.transform(Futures.allAsList(futureList),
+        return Futures.transform(getCameraControl().submitStillCaptureRequests(
+                captureConfigs, mCaptureMode, mFlashType),
                 input -> null, CameraXExecutors.directExecutor());
     }
 
-    /** This exception is thrown when request is failed (reported by framework) */
-    static final class CaptureFailedException extends RuntimeException {
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        CaptureFailedException(String s, Throwable e) {
-            super(s, e);
-        }
-
-        /** @hide */
-        @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-        CaptureFailedException(String s) {
-            super(s);
-        }
-    }
-
-
     private CaptureBundle getCaptureBundle(CaptureBundle defaultCaptureBundle) {
         List<CaptureStage> captureStages = mCaptureBundle.getCaptureStages();
         if (captureStages == null || captureStages.isEmpty()) {
@@ -2295,148 +2039,6 @@
         }
     }
 
-    /**
-     * An intermediate action recorder while taking picture. It is used to restore certain states.
-     * For example, cancel AF/AE scan, and close flash light.
-     */
-    static final class TakePictureState {
-        CameraCaptureResult mPreCaptureState = EmptyCameraCaptureResult.create();
-        boolean mIsAfTriggered = false;
-        boolean mIsFlashSequenceStarted = false;
-    }
-
-    /**
-     * A helper class to check camera capture result.
-     *
-     * <p>CaptureCallbackChecker is an implementation of {@link CameraCaptureCallback} that checks a
-     * specified list of condition and sets a ListenableFuture when the conditions have been met. It
-     * is mainly used to continuously capture callbacks to detect specific conditions. It also
-     * handles the timeout condition if the check condition does not satisfy the given timeout, and
-     * returns the given default value if the timeout is met.
-     */
-    static final class CaptureCallbackChecker extends CameraCaptureCallback {
-        private static final long NO_TIMEOUT = 0L;
-
-        /** Capture listeners. */
-        private final Set<CaptureResultListener> mCaptureResultListeners = new HashSet<>();
-
-        @Override
-        public void onCaptureCompleted(@NonNull CameraCaptureResult cameraCaptureResult) {
-            deliverCaptureResultToListeners(cameraCaptureResult);
-        }
-
-        /**
-         * Check the capture results of current session capture callback by giving a {@link
-         * CaptureResultChecker}.
-         *
-         * @param checker a CaptureResult checker that returns an object with type T if the check is
-         *                complete, returning null to continue the check process.
-         * @param <T>     the type parameter for CaptureResult checker.
-         * @return a listenable future for capture result check process.
-         */
-        <T> ListenableFuture<T> checkCaptureResult(CaptureResultChecker<T> checker) {
-            return checkCaptureResult(checker, NO_TIMEOUT, null);
-        }
-
-        /**
-         * Check the capture results of current session capture callback with timeout limit by
-         * giving a {@link CaptureResultChecker}.
-         *
-         * @param checker     a CaptureResult checker that returns an object with type T if the
-         *                    check is
-         *                    complete, returning null to continue the check process.
-         * @param timeoutInMs used to force stop checking.
-         * @param defValue    the default return value if timeout occur.
-         * @param <T>         the type parameter for CaptureResult checker.
-         * @return a listenable future for capture result check process.
-         */
-        <T> ListenableFuture<T> checkCaptureResult(
-                final CaptureResultChecker<T> checker, final long timeoutInMs, final T defValue) {
-            if (timeoutInMs < NO_TIMEOUT) {
-                throw new IllegalArgumentException("Invalid timeout value: " + timeoutInMs);
-            }
-            final long startTimeInMs =
-                    (timeoutInMs != NO_TIMEOUT) ? SystemClock.elapsedRealtime() : 0L;
-
-            return CallbackToFutureAdapter.getFuture(
-                    completer -> {
-                        addListener(
-                                new CaptureResultListener() {
-                                    @Override
-                                    public boolean onCaptureResult(
-                                            @NonNull CameraCaptureResult captureResult) {
-                                        T result = checker.check(captureResult);
-                                        if (result != null) {
-                                            completer.set(result);
-                                            return true;
-                                        } else if (startTimeInMs > 0
-                                                && SystemClock.elapsedRealtime() - startTimeInMs
-                                                > timeoutInMs) {
-                                            completer.set(defValue);
-                                            return true;
-                                        }
-                                        // Return false to continue check.
-                                        return false;
-                                    }
-                                });
-                        return "checkCaptureResult";
-                    });
-        }
-
-        /**
-         * Delivers camera capture result to {@link CaptureCallbackChecker#mCaptureResultListeners}.
-         */
-        private void deliverCaptureResultToListeners(@NonNull CameraCaptureResult captureResult) {
-            synchronized (mCaptureResultListeners) {
-                Set<CaptureResultListener> removeSet = null;
-                for (CaptureResultListener listener : new HashSet<>(mCaptureResultListeners)) {
-                    // Remove listener if the callback return true
-                    if (listener.onCaptureResult(captureResult)) {
-                        if (removeSet == null) {
-                            removeSet = new HashSet<>();
-                        }
-                        removeSet.add(listener);
-                    }
-                }
-                if (removeSet != null) {
-                    mCaptureResultListeners.removeAll(removeSet);
-                }
-            }
-        }
-
-        /** Add capture result listener. */
-        void addListener(CaptureResultListener listener) {
-            synchronized (mCaptureResultListeners) {
-                mCaptureResultListeners.add(listener);
-            }
-        }
-
-        /** An interface to check camera capture result. */
-        public interface CaptureResultChecker<T> {
-
-            /**
-             * The callback to check camera capture result.
-             *
-             * @param captureResult the camera capture result.
-             * @return the check result, return null to continue checking.
-             */
-            @Nullable
-            T check(@NonNull CameraCaptureResult captureResult);
-        }
-
-        /** An interface to listen to camera capture results. */
-        private interface CaptureResultListener {
-
-            /**
-             * Callback to handle camera capture results.
-             *
-             * @param captureResult camera capture result.
-             * @return true to finish listening, false to continue listening.
-             */
-            boolean onCaptureResult(@NonNull CameraCaptureResult captureResult);
-        }
-    }
-
     @VisibleForTesting
     static class ImageCaptureRequest {
         @RotationValue
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
index d770f51..d54398b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/CameraControlInternal.java
@@ -26,11 +26,14 @@
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.FocusMeteringResult;
 import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCapture.CaptureMode;
 import androidx.camera.core.ImageCapture.FlashMode;
+import androidx.camera.core.ImageCapture.FlashType;
 import androidx.camera.core.impl.utils.futures.Futures;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -55,42 +58,23 @@
     void setFlashMode(@FlashMode int flashMode);
 
     /**
-     * Performs a AF trigger.
+     * Performs still capture requests with the desired capture mode.
      *
-     * @return a {@link ListenableFuture} which completes when the request is completed.
-     * Cancelling the ListenableFuture is a no-op.
+     * @param captureConfigs capture configuration used for creating CaptureRequest
+     * @param captureMode the mode to capture the image, possible value is
+     * {@link ImageCapture#CAPTURE_MODE_MINIMIZE_LATENCY} or
+     * {@link ImageCapture#CAPTURE_MODE_MAXIMIZE_QUALITY}
+     * @param flashType the options when flash is required for taking a picture.
+     * @return ListenableFuture that would be completed while all the captures are completed. It
+     * would fail with a {@link androidx.camera.core.ImageCapture#ERROR_CAMERA_CLOSED} when the
+     * capture was canceled, or a {@link androidx.camera.core.ImageCapture#ERROR_CAPTURE_FAILED}
+     * when the capture was failed.
      */
     @NonNull
-    ListenableFuture<CameraCaptureResult> triggerAf();
-
-    /**
-     * Starts a flash sequence.
-     *
-     * @param flashType Uses one shot flash or use torch as flash when taking a picture.
-     * @return a {@link ListenableFuture} which completes when the request is completed.
-     * Cancelling the ListenableFuture is a no-op.
-     */
-    @NonNull
-    ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType);
-
-    /** Cancels AF trigger AND/OR finishes flash sequence.* */
-    void cancelAfAndFinishFlashSequence(boolean cancelAfTrigger, boolean finishFlashSequence);
-
-    /**
-     * Set a exposure compensation to the camera
-     *
-     * @param exposure the exposure compensation value to set
-     * @return a ListenableFuture which is completed when the new exposure compensation reach the
-     * target.
-     */
-    @NonNull
-    @Override
-    ListenableFuture<Integer> setExposureCompensationIndex(int exposure);
-
-    /**
-     * Performs still capture requests.
-     */
-    void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs);
+    ListenableFuture<List<Void>> submitStillCaptureRequests(
+            @NonNull List<CaptureConfig> captureConfigs,
+            @CaptureMode int captureMode,
+            @FlashType int flashType);
 
     /**
      * Gets the current SessionConfig.
@@ -141,31 +125,19 @@
             return Futures.immediateFuture(null);
         }
 
-        @Override
-        @NonNull
-        public ListenableFuture<CameraCaptureResult> triggerAf() {
-            return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create());
-        }
-
-        @Override
-        @NonNull
-        public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
-            return Futures.immediateFuture(null);
-        }
-
-        @Override
-        public void cancelAfAndFinishFlashSequence(boolean cancelAfTrigger,
-                boolean finishFlashSequence) {
-        }
-
         @NonNull
         @Override
         public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
             return Futures.immediateFuture(0);
         }
 
+        @NonNull
         @Override
-        public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+        public ListenableFuture<List<Void>> submitStillCaptureRequests(
+                @NonNull List<CaptureConfig> captureConfigs,
+                @CaptureMode int captureMode,
+                @FlashType int flashType) {
+            return Futures.immediateFuture(Collections.emptyList());
         }
 
         @NonNull
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
index 875f924..2ee6507 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeCameraControl.java
@@ -26,6 +26,7 @@
 import androidx.camera.core.FocusMeteringAction;
 import androidx.camera.core.FocusMeteringResult;
 import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.Logger;
 import androidx.camera.core.impl.CameraCaptureCallback;
 import androidx.camera.core.impl.CameraCaptureFailure;
@@ -36,6 +37,7 @@
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
 
 import com.google.common.util.concurrent.ListenableFuture;
 
@@ -56,6 +58,8 @@
     private ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
     private OnNewCaptureRequestListener mOnNewCaptureRequestListener;
     private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
+    private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
+            new ArrayList<>();
 
     public FakeCameraControl(@NonNull ControlUpdateCallback controlUpdateCallback) {
         mControlUpdateCallback = controlUpdateCallback;
@@ -69,6 +73,12 @@
                 cameraCaptureCallback.onCaptureCancelled();
             }
         }
+        for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+            completer.setException(
+                    new ImageCaptureException(ImageCapture.ERROR_CAMERA_CLOSED, "Simulate "
+                            + "capture cancelled", null));
+        }
+        mSubmittedCompleterList.clear();
         mSubmittedCaptureRequests.clear();
     }
 
@@ -81,17 +91,26 @@
                         CameraCaptureFailure.Reason.ERROR));
             }
         }
+        for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+            completer.setException(new ImageCaptureException(ImageCapture.ERROR_CAPTURE_FAILED,
+                    "Simulate capture fail", null));
+        }
+        mSubmittedCompleterList.clear();
         mSubmittedCaptureRequests.clear();
     }
 
     /** Notifies all submitted requests onCaptureCompleted */
-    public void notifyAllRequestsOnCaptureCompleted(CameraCaptureResult result) {
+    public void notifyAllRequestsOnCaptureCompleted(@NonNull CameraCaptureResult result) {
         for (CaptureConfig captureConfig : mSubmittedCaptureRequests) {
             for (CameraCaptureCallback cameraCaptureCallback :
                     captureConfig.getCameraCaptureCallbacks()) {
                 cameraCaptureCallback.onCaptureCompleted(result);
             }
         }
+        for (CallbackToFutureAdapter.Completer<Void> completer : mSubmittedCompleterList) {
+            completer.set(null);
+        }
+        mSubmittedCompleterList.clear();
         mSubmittedCaptureRequests.clear();
     }
 
@@ -114,40 +133,31 @@
         return Futures.immediateFuture(null);
     }
 
-    @Override
-    @NonNull
-    public ListenableFuture<CameraCaptureResult> triggerAf() {
-        Logger.d(TAG, "triggerAf()");
-        return Futures.immediateFuture(CameraCaptureResult.EmptyCameraCaptureResult.create());
-    }
-
-    @Override
-    @NonNull
-    public ListenableFuture<Void> startFlashSequence(@ImageCapture.FlashType int flashType) {
-        Logger.d(TAG, "startFlashSequence()");
-        return Futures.immediateFuture(null);
-    }
-
-    @Override
-    public void cancelAfAndFinishFlashSequence(final boolean cancelAfTrigger,
-            final boolean finishFlashSequence) {
-        Logger.d(TAG, "cancelAfAndFinishFlashSequence(" + cancelAfTrigger + ", "
-                + finishFlashSequence + ")");
-    }
-
     @NonNull
     @Override
     public ListenableFuture<Integer> setExposureCompensationIndex(int exposure) {
         return Futures.immediateFuture(null);
     }
 
+    @NonNull
     @Override
-    public void submitStillCaptureRequests(@NonNull List<CaptureConfig> captureConfigs) {
+    public ListenableFuture<List<Void>> submitStillCaptureRequests(
+            @NonNull List<CaptureConfig> captureConfigs,
+            int captureMode, int flashType) {
         mSubmittedCaptureRequests.addAll(captureConfigs);
         mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs);
+        List<ListenableFuture<Void>> fakeFutures = new ArrayList<>();
+        for (int i = 0; i < captureConfigs.size(); i++) {
+            fakeFutures.add(CallbackToFutureAdapter.getFuture(completer -> {
+                mSubmittedCompleterList.add(completer);
+                return "fakeFuture";
+            }));
+        }
+
         if (mOnNewCaptureRequestListener != null) {
             mOnNewCaptureRequestListener.onNewCaptureRequests(captureConfigs);
         }
+        return Futures.allAsList(fakeFutures);
     }
 
     @NonNull
diff --git a/camera/integration-tests/extensionstestapp/build.gradle b/camera/integration-tests/extensionstestapp/build.gradle
index f049cea..af4a3b4 100644
--- a/camera/integration-tests/extensionstestapp/build.gradle
+++ b/camera/integration-tests/extensionstestapp/build.gradle
@@ -52,11 +52,12 @@
     implementation(libs.kotlinStdlib)
     implementation("androidx.appcompat:appcompat:1.2.0")
     implementation("androidx.activity:activity-ktx:1.2.0")
-    implementation('androidx.concurrent:concurrent-futures-ktx:1.1.0')
-    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
+    implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
+    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
 
     // Guava
     implementation(libs.guavaAndroid)
+    implementation("androidx.viewpager2:viewpager2:1.0.0")
 
     androidTestImplementation(libs.testExtJunit)
     androidTestImplementation(libs.testCore)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
index 8394543..52fcd2b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultActivity.kt
@@ -193,13 +193,15 @@
 
     companion object {
 
-        fun getLensFacingStringFromId(lensFacing: Int): String = when (lensFacing) {
+        fun getLensFacingStringFromInt(lensFacing: Int): String = when (lensFacing) {
             CameraMetadata.LENS_FACING_BACK -> "BACK"
             CameraMetadata.LENS_FACING_FRONT -> "FRONT"
             CameraMetadata.LENS_FACING_EXTERNAL -> "EXTERNAL"
             else -> throw IllegalArgumentException("Invalid lens facing!!")
         }
 
+        const val INVALID_LENS_FACING = -1
+
         const val INTENT_EXTRA_KEY_CAMERA_ID = "CameraId"
         const val INTENT_EXTRA_KEY_LENS_FACING = "LensFacing"
         const val INTENT_EXTRA_KEY_EXTENSION_MODE = "ExtensionMode"
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
index 8027ec5..43520df 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/CameraValidationResultAdapter.kt
@@ -22,7 +22,7 @@
 import android.widget.BaseAdapter
 import android.widget.TextView
 import androidx.camera.integration.extensions.R
-import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromId
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_SUPPORTED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
@@ -72,7 +72,7 @@
         }
 
         val padding = 10
-        val lensFacingName = cameraLensFacingMap[item.key]?.let { getLensFacingStringFromId(it) }
+        val lensFacingName = cameraLensFacingMap[item.key]?.let { getLensFacingStringFromInt(it) }
         textView.text = "Camera ${item.key} [$lensFacingName]"
         textView.setPadding(padding, 0, padding, 0)
         textView.compoundDrawablePadding = padding
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
index 93bee76..c3a2051 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ExtensionValidationResultActivity.kt
@@ -32,14 +32,13 @@
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_RESULT_MAP
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_TEST_RESULT
-import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromId
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
 import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_SUPPORTED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
 import androidx.core.app.ActivityCompat
 
-private const val INVALID_LENS_FACING = -1
-
 class ExtensionValidationResultActivity : AppCompatActivity() {
     private val extensionTestResultMap = linkedMapOf<Int, Int>()
     private val result = Intent()
@@ -69,7 +68,7 @@
         setResult(requestCode, result)
 
         supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
-        supportActionBar!!.subtitle = "Camera $cameraId [${getLensFacingStringFromId(lensFacing)}]"
+        supportActionBar!!.subtitle = "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]"
 
         val layoutInflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
         adapter = ExtensionValidationResultAdapter(layoutInflater, extensionTestResultMap)
@@ -113,6 +112,7 @@
     private fun startCaptureValidationActivity(cameraId: String, mode: Int) {
         val intent = Intent(this, ImageValidationActivity::class.java)
         intent.putExtra(INTENT_EXTRA_KEY_CAMERA_ID, cameraId)
+        intent.putExtra(INTENT_EXTRA_KEY_LENS_FACING, lensFacing)
         intent.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, mode)
         intent.putExtra(INTENT_EXTRA_KEY_REQUEST_CODE, imageValidationActivityRequestCode)
 
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 6272aae..325b2d7 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -54,7 +54,10 @@
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_URI
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_LENS_FACING
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
 import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.TestResults.Companion.createCameraSelectorById
 import androidx.camera.integration.extensions.validation.TestResults.Companion.getExtensionModeStringFromId
@@ -79,6 +82,7 @@
     private var extensionMode = INVALID_EXTENSION_MODE
     private var extensionEnabled = true
     private val result = Intent()
+    private var lensFacing = INVALID_LENS_FACING
     private lateinit var cameraProvider: ProcessCameraProvider
     private lateinit var extensionsManager: ExtensionsManager
     private lateinit var cameraId: String
@@ -105,6 +109,7 @@
         setContentView(R.layout.image_capture_activity)
 
         cameraId = intent?.getStringExtra(INTENT_EXTRA_KEY_CAMERA_ID)!!
+        lensFacing = intent.getIntExtra(INTENT_EXTRA_KEY_LENS_FACING, INVALID_LENS_FACING)
         extensionMode = intent.getIntExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, INVALID_EXTENSION_MODE)
 
         result.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, extensionMode)
@@ -114,7 +119,8 @@
 
         supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
         supportActionBar!!.subtitle =
-            "Camera $cameraId [${getExtensionModeStringFromId(extensionMode)}]"
+            "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]" +
+                "[${getExtensionModeStringFromId(extensionMode)}]"
 
         viewFinder = findViewById(R.id.view_finder)
 
@@ -213,9 +219,17 @@
                 ContextCompat.getMainExecutor(this),
                 object : ImageCapture.OnImageCapturedCallback() {
                     override fun onCaptureSuccess(image: ImageProxy) {
+                        val filenamePrefix =
+                            "[Camera-$cameraId][${getLensFacingStringFromInt(lensFacing)}]" +
+                                "[${getExtensionModeStringFromId(extensionMode)}]"
+                        val filename = if (extensionEnabled) {
+                            "$filenamePrefix[Enabled]"
+                        } else {
+                            "$filenamePrefix[Disabled]"
+                        }
                         val tempFile = File.createTempFile(
-                            getExtensionModeStringFromId(extensionMode),
-                            ".jpg",
+                            filename,
+                            "",
                             codeCacheDir
                         )
                         val outputStream = FileOutputStream(tempFile)
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 2a62393..1f5f738 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -16,14 +16,23 @@
 
 package androidx.camera.integration.extensions.validation
 
+import android.content.ContentValues
 import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
+import android.content.res.Configuration
 import android.net.Uri
+import android.os.Build
 import android.os.Bundle
+import android.provider.MediaStore
 import android.util.Log
+import android.view.GestureDetector
+import android.view.Menu
+import android.view.MenuItem
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
 import android.widget.ImageButton
 import android.widget.ImageView
+import android.widget.Toast
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.integration.extensions.R
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
@@ -31,18 +40,35 @@
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_IMAGE_URI
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_LENS_FACING
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_REQUEST_CODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_TEST_RESULT
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INVALID_LENS_FACING
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.getLensFacingStringFromInt
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
+import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
 import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.getExtensionModeStringFromId
 import androidx.core.app.ActivityCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+import androidx.viewpager2.adapter.FragmentStateAdapter
+import androidx.viewpager2.widget.ViewPager2
+import java.io.File
+import java.io.FileInputStream
+import java.io.OutputStream
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import kotlin.math.max
+import kotlin.math.min
 
 private const val TAG = "ImageValidationActivity"
 
@@ -50,18 +76,27 @@
 
     private var extensionMode = INVALID_EXTENSION_MODE
     private val result = Intent()
+    private var lensFacing = INVALID_LENS_FACING
     private lateinit var cameraId: String
-    private lateinit var photoViewer: ImageView
     private lateinit var failButton: ImageButton
     private lateinit var passButton: ImageButton
     private lateinit var captureButton: ImageButton
+    private lateinit var viewPager: ViewPager2
+    private lateinit var photoImageView: ImageView
     private val imageCaptureActivityRequestCode = ImageCaptureActivity::class.java.hashCode() % 1000
+    private val imageUris = arrayListOf<Pair<Uri, Int>>()
+    private var scaledBitmapWidth = 0
+    private var scaledBitmapHeight = 0
+    private var currentScale = 1.0f
+    private var translationX = 0.0f
+    private var translationY = 0.0f
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.image_validation_activity)
 
         cameraId = intent?.getStringExtra(INTENT_EXTRA_KEY_CAMERA_ID)!!
+        lensFacing = intent.getIntExtra(INTENT_EXTRA_KEY_LENS_FACING, INVALID_LENS_FACING)
         extensionMode = intent.getIntExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, INVALID_EXTENSION_MODE)
 
         result.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, extensionMode)
@@ -71,10 +106,14 @@
 
         supportActionBar?.title = "${resources.getString(R.string.extensions_validator)}"
         supportActionBar!!.subtitle =
-            "Camera $cameraId [${getExtensionModeStringFromId(extensionMode)}]"
+            "Camera $cameraId [${getLensFacingStringFromInt(lensFacing)}]" +
+                "[${getExtensionModeStringFromId(extensionMode)}]"
 
-        photoViewer = findViewById(R.id.photo_viewer)
+        viewPager = findViewById(R.id.photo_view_pager)
+        photoImageView = findViewById(R.id.photo_image_view)
+
         setupButtonControls()
+        setupGestureControls()
         startCaptureImageActivity(cameraId, extensionMode)
     }
 
@@ -103,14 +142,118 @@
 
         // Returns without capturing a picture
         if (uri == null) {
-            finish()
+            // Closes the activity if there is no image captured.
+            if (imageUris.isEmpty()) {
+                finish()
+            }
             return
         }
 
         val rotationDegrees = data?.getIntExtra(INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES, 0)!!
+        imageUris.add(Pair(uri, rotationDegrees))
 
-        photoViewer.setImageBitmap(decodeImageFromUri(uri))
-        photoViewer.rotation = rotationDegrees.toFloat()
+        viewPager.adapter = PhotoPagerAdapter(this)
+        viewPager.currentItem = imageUris.size - 1
+        viewPager.visibility = View.VISIBLE
+        viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
+            override fun onPageSelected(position: Int) {
+                super.onPageSelected(position)
+                updatePhotoImageView()
+            }
+        })
+
+        updatePhotoImageView()
+        resetAndHidePhotoImageView()
+    }
+
+    override fun onConfigurationChanged(newConfig: Configuration) {
+        super.onConfigurationChanged(newConfig)
+        resetAndHidePhotoImageView()
+        updateScaledBitmapDims(scaledBitmapWidth, scaledBitmapHeight)
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        menuInflater.inflate(R.menu.image_validation_menu, menu)
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            R.id.menu_save_image -> {
+                saveCurrentImage()
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    private fun saveCurrentImage() {
+        val formatter: Format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+        val savedFileName =
+            "${imageUris[viewPager.currentItem].first.lastPathSegment}" +
+                "[${formatter.format(Calendar.getInstance().time)}].jpg"
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            val contentValues = ContentValues()
+            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, savedFileName)
+            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+            contentValues.put(
+                MediaStore.MediaColumns.RELATIVE_PATH,
+                "Pictures/ExtensionsValidation"
+            )
+            contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+            val outputUri =
+                contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
+            val resultToastMsg: String
+
+            if (outputUri == null) {
+                resultToastMsg = "Failed to export image - $savedFileName!"
+            } else {
+                if (copyTempFileToOutputUri(imageUris[viewPager.currentItem].first, outputUri)) {
+                    resultToastMsg =
+                        "Image is saved as Pictures/ExtensionsValidation/$savedFileName."
+                } else {
+                    resultToastMsg = "Failed to export image - $savedFileName!"
+                }
+                contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+                contentResolver.update(outputUri, contentValues, null, null)
+            }
+            Toast.makeText(this, resultToastMsg, Toast.LENGTH_LONG).show()
+        } else {
+            Log.e(TAG, "The known devices which support Extensions should be at least" +
+                " Android Q!")
+        }
+    }
+
+    /**
+     * Copies temp file to output [Uri].
+     *
+     * @return false if the [Uri] is not writable.
+     */
+    private fun copyTempFileToOutputUri(tempFileUri: Uri, uri: Uri): Boolean {
+        contentResolver.openOutputStream(uri).use { outputStream ->
+            if (tempFileUri.path == null || outputStream == null) {
+                return false
+            }
+
+            val tempFile = File(tempFileUri.path!!)
+            copyTempFileToOutputStream(tempFile, outputStream)
+        }
+        return true
+    }
+
+    private fun copyTempFileToOutputStream(
+        tempFile: File,
+        outputStream: OutputStream
+    ) {
+        FileInputStream(tempFile).use { `in` ->
+            val buf = ByteArray(1024)
+            var len: Int
+            while (`in`.read(buf).also { len = it } > 0) {
+                outputStream.write(buf, 0, len)
+            }
+        }
     }
 
     private fun setupButtonControls() {
@@ -135,6 +278,7 @@
     private fun startCaptureImageActivity(cameraId: String, mode: Int) {
         val intent = Intent(this, ImageCaptureActivity::class.java)
         intent.putExtra(INTENT_EXTRA_KEY_CAMERA_ID, cameraId)
+        intent.putExtra(INTENT_EXTRA_KEY_LENS_FACING, lensFacing)
         intent.putExtra(INTENT_EXTRA_KEY_EXTENSION_MODE, mode)
         intent.putExtra(INTENT_EXTRA_KEY_REQUEST_CODE, imageCaptureActivityRequestCode)
 
@@ -146,10 +290,186 @@
         )
     }
 
-    private fun decodeImageFromUri(uri: Uri): Bitmap {
-        val parcelFileDescriptor = this.contentResolver.openFileDescriptor(uri, "r")
-        val bitmap = BitmapFactory.decodeFileDescriptor(parcelFileDescriptor?.fileDescriptor)
-        parcelFileDescriptor?.close()
-        return bitmap
+    /** Adapter class used to present a fragment containing one photo or video as a page */
+    inner class PhotoPagerAdapter(fragmentActivity: FragmentActivity) :
+        FragmentStateAdapter(fragmentActivity) {
+        override fun getItemCount(): Int {
+            return imageUris.size
+        }
+
+        override fun createFragment(position: Int): Fragment {
+            // Set scale gesture listener to the fragments inside the ViewPager2 so that we can
+            // switch to another photo view which supports the translation function in the X
+            // direction. Otherwise, the fragments inside the ViewPager2 will eat the X direction
+            // movement events for the ViewPager2's page switch function. But we'll need the
+            // translation function in X direction after the photo is zoomed in.
+            val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener =
+                object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+                    override fun onScale(detector: ScaleGestureDetector): Boolean {
+                        updatePhotoViewScale(detector.scaleFactor)
+                        return true
+                    }
+                }
+
+            return PhotoFragment(
+                imageUris[position].first,
+                imageUris[position].second,
+                scaleGestureListener
+            )
+        }
+    }
+
+    private fun setupGestureControls() {
+        // Registers the scale gesture event to allow the users to scale the photo image view
+        // between 1.0 and 3.0 times.
+        val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener =
+            object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
+                override fun onScale(detector: ScaleGestureDetector): Boolean {
+                    updatePhotoViewScale(detector.scaleFactor)
+                    return true
+                }
+            }
+
+        // Registers double tap event to reset and hide the photo image view.
+        val onDoubleTapGestureListener: GestureDetector.OnGestureListener =
+            object : GestureDetector.SimpleOnGestureListener() {
+                override fun onDoubleTap(e: MotionEvent): Boolean {
+                    resetAndHidePhotoImageView()
+                    return true
+                }
+            }
+
+        val scaleDetector = ScaleGestureDetector(this, scaleGestureListener)
+        val doubleTapDetector = GestureDetector(this, onDoubleTapGestureListener)
+        var previousX = 0.0f
+        var previousY = 0.0f
+
+        photoImageView.setOnTouchListener { _, e: MotionEvent? ->
+            if (photoImageView.visibility != View.VISIBLE) {
+                return@setOnTouchListener false
+            }
+
+            val doubleTapProcessed = doubleTapDetector.onTouchEvent(e)
+            val scaleGestureProcessed = scaleDetector.onTouchEvent(e)
+
+            when (e?.actionMasked) {
+                MotionEvent.ACTION_DOWN -> {
+                    previousX = e.x
+                    previousY = e.y
+                }
+                MotionEvent.ACTION_MOVE -> {
+                    updatePhotoImageViewTranslation(e.x, e.y, previousX, previousY)
+                }
+            }
+
+            doubleTapProcessed || scaleGestureProcessed
+        }
+    }
+
+    internal fun updatePhotoViewScale(scaleFactor: Float) {
+        currentScale *= scaleFactor
+
+        // Don't let the object get too small or too large.
+        currentScale = max(1.0f, min(currentScale, 3.0f))
+
+        photoImageView.scaleX = currentScale
+        photoImageView.scaleY = currentScale
+
+        // Shows the photoImageView when the scale is larger than 1.0f. Hides the photoImageView
+        // when the scale has been reduced as 1.0f.
+        if (photoImageView.visibility != View.VISIBLE && currentScale > 1.0f) {
+            photoImageView.visibility = View.VISIBLE
+            viewPager.visibility = View.INVISIBLE
+        } else if (photoImageView.visibility == View.VISIBLE && currentScale == 1.0f) {
+            resetAndHidePhotoImageView()
+        }
+    }
+
+    private fun updatePhotoImageViewTranslation(
+        x: Float,
+        y: Float,
+        previousX: Float,
+        previousY: Float
+    ) {
+        val newTranslationX = translationX + x - previousX
+
+        if (scaledBitmapWidth * currentScale > photoImageView.width) {
+            val maxTranslationX = (scaledBitmapWidth * currentScale - photoImageView.width) / 2
+
+            translationX = if (newTranslationX >= 0) {
+                if (maxTranslationX - newTranslationX >= 0) {
+                    newTranslationX
+                } else {
+                    maxTranslationX
+                }
+            } else {
+                if (maxTranslationX + newTranslationX >= 0) {
+                    newTranslationX
+                } else {
+                    -maxTranslationX
+                }
+            }
+            photoImageView.translationX = translationX
+        }
+
+        val newTranslationY = translationY + y - previousY
+
+        if (scaledBitmapHeight * currentScale > photoImageView.height) {
+            val maxTranslationY = (scaledBitmapHeight * currentScale - photoImageView.height) / 2
+
+            translationY = if (newTranslationY >= 0) {
+                if (maxTranslationY - newTranslationY >= 0) {
+                    newTranslationY
+                } else {
+                    maxTranslationY
+                }
+            } else {
+                if (maxTranslationY + newTranslationY >= 0) {
+                    newTranslationY
+                } else {
+                    -maxTranslationY
+                }
+            }
+            photoImageView.translationY = translationY
+        }
+    }
+
+    internal fun updatePhotoImageView() {
+        val bitmap = decodeImageToBitmap(
+            this@ImageValidationActivity.contentResolver,
+            imageUris[viewPager.currentItem].first,
+            imageUris[viewPager.currentItem].second
+        )
+
+        photoImageView.setImageBitmap(bitmap)
+        updateScaledBitmapDims(bitmap.width, bitmap.height)
+
+        // Updates the index and file name to the subtitle
+        supportActionBar!!.subtitle = "[${viewPager.currentItem + 1}/${imageUris.size}]" +
+            "${imageUris[viewPager.currentItem].first.lastPathSegment}"
+    }
+
+    private fun updateScaledBitmapDims(width: Int, height: Int) {
+        val scale: Float
+        if (width * photoImageView.height / photoImageView.width > height) {
+            scale = photoImageView.width.toFloat() / width
+        } else {
+            scale = photoImageView.height.toFloat() / height
+        }
+
+        scaledBitmapWidth = (width * scale).toInt()
+        scaledBitmapHeight = (height * scale).toInt()
+    }
+
+    internal fun resetAndHidePhotoImageView() {
+        viewPager.visibility = View.VISIBLE
+        photoImageView.visibility = View.INVISIBLE
+        photoImageView.scaleX = 1.0f
+        photoImageView.scaleY = 1.0f
+        photoImageView.translationX = 0.0f
+        photoImageView.translationY = 0.0f
+        currentScale = 1.0f
+        translationX = 0.0f
+        translationY = 0.0f
     }
 }
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt
new file mode 100644
index 0000000..6c3a5c7
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/PhotoFragment.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.validation
+
+import android.app.Activity
+import android.content.ContentResolver
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Matrix
+import android.net.Uri
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.camera.integration.extensions.R
+import androidx.fragment.app.Fragment
+import androidx.viewpager2.widget.ViewPager2
+
+/**
+ * Fragment used for each individual page showing a photo inside [ImageValidationActivity].
+ *
+ * @param imageUri The image uri to be displayed in this photo fragment
+ * @param rotationDegrees The rotation degrees to rotate the image to the upright direction
+ * @param scaleGestureListener The scale gesture listener which allow the caller activity to
+ * receive the scale events to switch to another photo view which supports the translation
+ * function in the X direction. It is because this fragment will be put inside a [ViewPager2]
+ * and it will eat the X direction movement events for the [ViewPager2]'s page switch function. But
+ * we'll need the translation function in X direction after the photo is zoomed in.
+ */
+class PhotoFragment constructor(
+    private val imageUri: Uri,
+    private val rotationDegrees: Int,
+    private val scaleGestureListener: ScaleGestureDetector.SimpleOnScaleGestureListener?
+) :
+    Fragment() {
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View = inflater.inflate(R.layout.single_photo_viewer, container, false)
+
+    private lateinit var photoViewer: ImageView
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        photoViewer = view.findViewById(R.id.imageView)
+        photoViewer.setImageBitmap(
+            decodeImageToBitmap(
+                (requireActivity() as Activity).contentResolver,
+                imageUri,
+                rotationDegrees
+            )
+        )
+
+        setPhotoViewerScaleGestureListener()
+    }
+
+    private fun setPhotoViewerScaleGestureListener() {
+        scaleGestureListener?.let {
+            val scaleDetector = ScaleGestureDetector(requireContext(), scaleGestureListener)
+            photoViewer.setOnTouchListener { _, e: MotionEvent? ->
+                scaleDetector.onTouchEvent(e)
+            }
+        }
+    }
+
+    companion object {
+        fun decodeImageToBitmap(
+            contentResolver: ContentResolver,
+            imageUri: Uri,
+            rotationDegrees: Int
+        ): Bitmap {
+            val parcelFileDescriptor = contentResolver.openFileDescriptor(imageUri, "r")
+            val bitmap = BitmapFactory.decodeFileDescriptor(parcelFileDescriptor?.fileDescriptor)
+            parcelFileDescriptor?.close()
+
+            // Rotates the bitmap to the correct orientation
+            val matrix = Matrix()
+            matrix.postRotate(rotationDegrees.toFloat())
+            return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
index cc0a543..b8d0119 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/image_validation_activity.xml
@@ -22,11 +22,18 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
-    <ImageView
-        android:id="@+id/photo_viewer"
+    <androidx.viewpager2.widget.ViewPager2
+        android:id="@+id/photo_view_pager"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
 
+    <ImageView
+        android:id="@+id/photo_image_view"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible"
+        />
+
     <ImageButton
         android:id="@+id/fail_button"
         android:layout_width="@dimen/round_button_medium"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml
new file mode 100644
index 0000000..f4bc18f
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/single_photo_viewer.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/constraintLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ImageView
+        android:id="@+id/imageView"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml
new file mode 100644
index 0000000..aca8d62
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/image_validation_menu.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_save_image"
+        android:title="Save image" />
+</menu>
\ No newline at end of file
diff --git a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
index 4ce76a7..23a3b70 100644
--- a/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
+++ b/car/app/app-automotive/src/main/java/androidx/car/app/hardware/AutomotiveCarHardwareManager.java
@@ -43,8 +43,8 @@
     private final AutomotiveCarSensors mCarSensors;
 
     public AutomotiveCarHardwareManager(@NonNull Context context) {
-        requireNonNull(context);
-        mCarInfo = new AutomotiveCarInfo(new PropertyManager(context));
+        Context appContext = requireNonNull(context.getApplicationContext());
+        mCarInfo = new AutomotiveCarInfo(new PropertyManager(appContext));
         mCarSensors = new AutomotiveCarSensors();
     }
 
diff --git a/car/app/app-samples/showcase/automotive/build.gradle b/car/app/app-samples/showcase/automotive/build.gradle
index a3c02a9..5dd1a78 100644
--- a/car/app/app-samples/showcase/automotive/build.gradle
+++ b/car/app/app-samples/showcase/automotive/build.gradle
@@ -24,8 +24,10 @@
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 29
         targetSdkVersion 31
-        versionCode 106     // Increment this to generate signed builds for uploading to Playstore
-        versionName "106"
+        // Increment this to generate signed builds for uploading to Playstore
+        // Make sure this is different from the showcase-mobile version
+        versionCode 107
+        versionName "107"
     }
 
     buildTypes {
diff --git a/car/app/app-samples/showcase/automotive/github_build.gradle b/car/app/app-samples/showcase/automotive/github_build.gradle
index 289cbdb..d41d257 100644
--- a/car/app/app-samples/showcase/automotive/github_build.gradle
+++ b/car/app/app-samples/showcase/automotive/github_build.gradle
@@ -23,8 +23,10 @@
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 29
         targetSdkVersion 31
-        versionCode 106     // Increment this to generate signed builds for uploading to Playstore
-        versionName "106"
+        // Increment this to generate signed builds for uploading to Playstore
+        // Make sure this is different from the showcase-mobile version
+        versionCode 107
+        versionName "107"
     }
 
     buildTypes {
diff --git a/car/app/app-samples/showcase/mobile/build.gradle b/car/app/app-samples/showcase/mobile/build.gradle
index 27d12da..74bf224 100644
--- a/car/app/app-samples/showcase/mobile/build.gradle
+++ b/car/app/app-samples/showcase/mobile/build.gradle
@@ -24,7 +24,9 @@
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
         targetSdkVersion 31
-        versionCode 106     // Increment this to generate signed builds for uploading to Playstore
+        // Increment this to generate signed builds for uploading to Playstore
+        // Make sure this is different from the showcase-automotive version
+        versionCode 106
         versionName "106"
     }
 
diff --git a/car/app/app-samples/showcase/mobile/github_build.gradle b/car/app/app-samples/showcase/mobile/github_build.gradle
index 4d8d3a7..83498fb 100644
--- a/car/app/app-samples/showcase/mobile/github_build.gradle
+++ b/car/app/app-samples/showcase/mobile/github_build.gradle
@@ -23,7 +23,9 @@
         applicationId "androidx.car.app.sample.showcase"
         minSdkVersion 23
         targetSdkVersion 31
-        versionCode 106     // Increment this to generate signed builds for uploading to Playstore
+        // Increment this to generate signed builds for uploading to Playstore
+        // Make sure this is different from the showcase-automotive version
+        versionCode 106
         versionName "106"
     }
 
diff --git a/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java b/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
index 6c51b0a..3a11339 100644
--- a/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
+++ b/car/app/app/src/main/java/androidx/car/app/navigation/model/PanModeDelegate.java
@@ -32,7 +32,7 @@
     /**
      * Notifies that the user has entered or exited pan mode.
      *
-     * @param isInPanMode   the updated checked state
+     * @param isInPanMode   the latest pan mode state.
      * @param callback      the {@link OnDoneCallback} to trigger when the client finishes handling
      *                      the event
      */
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
index bd258f6..4a4ede6 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/Animatable.kt
@@ -458,4 +458,6 @@
      *    [BoundReached] being the end reason.
      */
     val endReason: AnimationEndReason
-)
\ No newline at end of file
+) {
+    override fun toString(): String = "AnimationResult(endReason=$endReason, endState=$endState)"
+}
\ No newline at end of file
diff --git a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
index ab8f9c4..49ddd38 100644
--- a/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
+++ b/compose/animation/animation-core/src/commonMain/kotlin/androidx/compose/animation/core/AnimationState.kt
@@ -95,6 +95,16 @@
      */
     val velocity: T
         get() = typeConverter.convertFromVector(velocityVector)
+
+    override fun toString(): String {
+        return "AnimationState(" +
+            "value=$value, " +
+            "velocity=$velocity, " +
+            "isRunning=$isRunning, " +
+            "lastFrameTimeNanos=$lastFrameTimeNanos, " +
+            "finishedTimeNanos=$finishedTimeNanos" +
+            ")"
+    }
 }
 
 /**
diff --git a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
index bc0c712..4d6a0d6 100644
--- a/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
+++ b/compose/animation/animation-core/src/test/java/androidx/compose/animation/core/AnimatableTest.kt
@@ -20,6 +20,7 @@
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.geometry.Offset
+import com.google.common.truth.Truth.assertThat
 import junit.framework.TestCase.assertEquals
 import junit.framework.TestCase.assertFalse
 import junit.framework.TestCase.assertTrue
@@ -292,4 +293,34 @@
         }
         assertEquals(animatable.lowerBound!!, animatable.value)
     }
+
+    @Test
+    fun animationResult_toString() {
+        val animatable = AnimationResult(
+            endReason = AnimationEndReason.Finished,
+            endState = AnimationState(42f)
+        )
+        val string = animatable.toString()
+        assertThat(string).contains(AnimationResult::class.java.simpleName)
+        assertThat(string).contains("endReason=Finished")
+        assertThat(string).contains("endState=")
+    }
+
+    @Test
+    fun animationState_toString() {
+        val state = AnimationState(
+            initialValue = 42f,
+            initialVelocity = 2f,
+            lastFrameTimeNanos = 4000L,
+            finishedTimeNanos = 3000L,
+            isRunning = true
+        )
+        val string = state.toString()
+        assertThat(string).contains(AnimationState::class.java.simpleName)
+        assertThat(string).contains("value=42.0")
+        assertThat(string).contains("velocity=2.0")
+        assertThat(string).contains("lastFrameTimeNanos=4000")
+        assertThat(string).contains("finishedTimeNanos=3000")
+        assertThat(string).contains("isRunning=true")
+    }
 }
diff --git a/compose/foundation/foundation-layout/build.gradle b/compose/foundation/foundation-layout/build.gradle
index 27abb26..fa9377e 100644
--- a/compose/foundation/foundation-layout/build.gradle
+++ b/compose/foundation/foundation-layout/build.gradle
@@ -51,6 +51,12 @@
         androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.0.0")
         androidTestImplementation(project(":compose:test-utils"))
         androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+        // old version of common-java8 conflicts with newer version, because both have
+        // DefaultLifecycleEventObserver.
+        // Outside of androidx this is resolved via constraint added to lifecycle-common,
+        // but it doesn't work in androidx.
+        // See aosp/1804059
+        androidTestImplementation(project(":lifecycle:lifecycle-common-java8"))
         androidTestImplementation(libs.testRules)
         androidTestImplementation(libs.testRunner)
         androidTestImplementation(libs.junit)
diff --git a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
index 91d535a..3bed06c 100644
--- a/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
+++ b/compose/foundation/foundation-layout/src/commonMain/kotlin/androidx/compose/foundation/layout/RowColumnImpl.kt
@@ -782,8 +782,8 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         val otherModifier = other as? LayoutWeightImpl ?: return false
-        return weight != otherModifier.weight &&
-            fill != otherModifier.fill
+        return weight == otherModifier.weight &&
+            fill == otherModifier.fill
     }
 
     override fun hashCode(): Int {
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
index dc008b7..ff02ea8 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/textfield/TextFieldScrollTest.kt
@@ -18,15 +18,18 @@
 
 import android.os.Build
 import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.TextFieldScrollerPosition
 import androidx.compose.foundation.text.TextLayoutResultProxy
 import androidx.compose.foundation.text.maxLinesHeight
@@ -53,11 +56,17 @@
 import androidx.compose.ui.platform.LocalViewConfiguration
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertCountEquals
+import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.captureToImage
+import androidx.compose.ui.test.isPopup
 import androidx.compose.ui.test.junit4.ComposeContentTestRule
 import androidx.compose.ui.test.junit4.StateRestorationTester
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
 import androidx.compose.ui.test.performTouchInput
 import androidx.compose.ui.test.swipe
 import androidx.compose.ui.test.swipeDown
@@ -225,7 +234,7 @@
             }
         }
 
-        rule.runOnIdle {}
+        rule.waitForIdle()
 
         rule.onNodeWithTag(tag)
             .captureToImage()
@@ -260,7 +269,7 @@
             }
         }
 
-        rule.runOnIdle {}
+        rule.waitForIdle()
 
         rule.onNodeWithTag(tag)
             .captureToImage()
@@ -545,6 +554,118 @@
         }
     }
 
+    @Test
+    fun textField_cursorHandle_hidden_whenScrolledOutOfView() {
+        val size = 100
+        val tag = "Text"
+
+        with(rule.density) {
+            rule.setContent {
+                BasicTextField(
+                    value = longText,
+                    onValueChange = {},
+                    modifier = Modifier
+                        .padding(size.toDp())
+                        .size(size.toDp())
+                        .testTag(tag)
+                )
+            }
+        }
+
+        // Click to focus and show handle.
+        rule.onNodeWithTag(tag)
+            .performClick()
+
+        // Check that handle is displayed (if it's not, we can't check if it gets hidden).
+        rule.onNode(isPopup())
+            .assertIsDisplayed()
+        // TODO(b/139861182) Assert handle position is within field bounds?
+
+        // Scroll up by twice the height to move the cursor out of the visible area.
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(x = 0f, y = -size * 2f))
+            }
+
+        // Check that cursor is hidden.
+        rule.onNode(isPopup()).assertDoesNotExist()
+
+        // Scroll back and make sure the handles are shown again.
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                moveBy(Offset(x = 0f, y = size * 2f))
+            }
+
+        rule.onNode(isPopup()).assertIsDisplayed()
+    }
+
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun textField_selectionHandles_hidden_whenScrolledOutOfView() {
+        val size = 200
+        val tag = "Text"
+
+        fun assertHandlesDisplayed() {
+            rule.onAllNodes(isPopup())
+                .assertCountEquals(2)
+                .apply {
+                    (0 until 2)
+                        .map(::get)
+                        .forEach {
+                            it.assertIsDisplayed()
+                            // TODO(b/139861182) Assert handle position is within field bounds?
+                        }
+                }
+        }
+
+        with(rule.density) {
+            rule.setContent {
+                BasicTextField(
+                    value = longText,
+                    onValueChange = {},
+                    modifier = Modifier
+                        .border(0.dp, Color.Gray)
+                        .padding(size.toDp())
+                        .border(0.dp, Color.Black)
+                        .size(size.toDp())
+                        .testTag(tag)
+                )
+            }
+        }
+
+        // Select something to ensure both handles are visible.
+        rule.onNodeWithTag(tag)
+            // TODO(b/209698586) Use performTextInputSelection once that method actually updates
+            //  the text handles.
+            // .performTextInputSelection(TextRange(0, 1))
+            .performTouchInput {
+                longClick()
+            }
+
+        // Check that both handles are displayed (if not, we can't check that they get hidden).
+        assertHandlesDisplayed()
+
+        // Scroll up by twice the height to move the cursor out of the visible area.
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                down(center)
+                moveBy(Offset(x = 0f, y = -size * 2f))
+            }
+
+        // Check that cursor is hidden.
+        rule.onAllNodes(isPopup())
+            .assertCountEquals(0)
+
+        // Scroll back and make sure the handles are shown again.
+        rule.onNodeWithTag(tag)
+            .performTouchInput {
+                moveBy(Offset(x = 0f, y = size * 2f))
+            }
+
+        assertHandlesDisplayed()
+    }
+
     private fun ComposeContentTestRule.setupHorizontallyScrollableContent(
         scrollerPosition: TextFieldScrollerPosition,
         text: String,
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 048a88f..babaf59 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
@@ -344,8 +344,13 @@
                 } else {
                     manager.hideSelectionToolbar()
                 }
-                state.showSelectionHandleStart = manager.isSelectionHandleInVisibleBound(true)
-                state.showSelectionHandleEnd = manager.isSelectionHandleInVisibleBound(false)
+                state.showSelectionHandleStart =
+                    manager.isSelectionHandleInVisibleBound(isStartHandle = true)
+                state.showSelectionHandleEnd =
+                    manager.isSelectionHandleInVisibleBound(isStartHandle = false)
+            } else if (state.handleState == HandleState.Cursor) {
+                state.showCursorHandle =
+                    manager.isSelectionHandleInVisibleBound(isStartHandle = true)
             }
             state.layoutResult?.let { layoutResult ->
                 state.inputSession?.let { inputSession ->
@@ -703,15 +708,23 @@
     var showFloatingToolbar = false
 
     /**
-     * A flag to check if the start selection handle should show.
+     * True if the position of the selection start handle is within a visible part of the window
+     * (i.e. not scrolled out of view) and the handle should be drawn.
      */
     var showSelectionHandleStart by mutableStateOf(false)
 
     /**
-     * A flag to check if the end selection handle should show.
+     * True if the position of the selection end handle is within a visible part of the window
+     * (i.e. not scrolled out of view) and the handle should be drawn.
      */
     var showSelectionHandleEnd by mutableStateOf(false)
 
+    /**
+     * True if the position of the cursor is within a visible part of the window (i.e. not scrolled
+     * out of view) and the handle should be drawn.
+     */
+    var showCursorHandle by mutableStateOf(false)
+
     val keyboardActionRunner: KeyboardActionRunner = KeyboardActionRunner()
 
     var onValueChange: (TextFieldValue) -> Unit = {}
@@ -849,17 +862,11 @@
 
 @Composable
 internal fun TextFieldCursorHandle(manager: TextFieldSelectionManager) {
-    val offset = manager.offsetMapping.originalToTransformed(manager.value.selection.start)
-    val observer = remember(manager) { manager.cursorDragObserver() }
-    manager.state?.layoutResult?.value?.let {
-        val cursorRect = it.getCursorRect(
-            offset.coerceIn(0, it.layoutInput.text.length)
-        )
-        val x = with(LocalDensity.current) {
-            cursorRect.left + DefaultCursorThickness.toPx() / 2
-        }
+    if (manager.state?.showCursorHandle == true) {
+        val observer = remember(manager) { manager.cursorDragObserver() }
+        val position = manager.getCursorPosition(LocalDensity.current)
         CursorHandle(
-            handlePosition = Offset(x, cursorRect.bottom),
+            handlePosition = position,
             modifier = Modifier.pointerInput(observer) {
                 detectDragGesturesWithObserver(observer)
             },
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
index 353a734..63e6ce0 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/selection/SelectionManager.kt
@@ -132,7 +132,7 @@
                 false
             }
         }
-        .selectionMagnifier(this)
+        .then(if (shouldShowMagnifier) Modifier.selectionMagnifier(this) else Modifier)
 
     private var previousPosition: Offset? = null
     /**
@@ -189,6 +189,8 @@
     var draggingHandle: Handle? by mutableStateOf(null)
         private set
 
+    private val shouldShowMagnifier get() = draggingHandle != null
+
     init {
         selectionRegistrar.onPositionChangeCallback = { selectableId ->
             if (
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 d973268..1c9dc08 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
@@ -16,6 +16,7 @@
 
 package androidx.compose.foundation.text.selection
 
+import androidx.compose.foundation.text.DefaultCursorThickness
 import androidx.compose.foundation.text.Handle
 import androidx.compose.foundation.text.HandleState
 import androidx.compose.foundation.text.InternalFoundationTextApi
@@ -49,6 +50,7 @@
 import androidx.compose.ui.text.input.getTextAfterSelection
 import androidx.compose.ui.text.input.getTextBeforeSelection
 import androidx.compose.ui.text.style.ResolvedTextDirection
+import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.dp
 import kotlin.math.max
 import kotlin.math.min
@@ -557,6 +559,18 @@
         )
     }
 
+    internal fun getCursorPosition(density: Density): Offset {
+        val offset = offsetMapping.originalToTransformed(value.selection.start)
+        val layoutResult = state?.layoutResult!!.value
+        val cursorRect = layoutResult.getCursorRect(
+            offset.coerceIn(0, layoutResult.layoutInput.text.length)
+        )
+        val x = with(density) {
+            cursorRect.left + DefaultCursorThickness.toPx() / 2
+        }
+        return Offset(x, cursorRect.bottom)
+    }
+
     /**
      * This function get the selected region as a Rectangle region, and pass it to [TextToolbar]
      * to make the FloatingToolbar show up in the proper place. In addition, this function passes
diff --git a/compose/integration-tests/docs-snippets/build.gradle b/compose/integration-tests/docs-snippets/build.gradle
index d41ce68..b6c04ee 100644
--- a/compose/integration-tests/docs-snippets/build.gradle
+++ b/compose/integration-tests/docs-snippets/build.gradle
@@ -44,6 +44,13 @@
     implementation(project(":navigation:navigation-compose"))
     implementation("androidx.activity:activity-compose:1.3.1")
     implementation(project(":lifecycle:lifecycle-viewmodel-compose"))
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation(project(":lifecycle:lifecycle-common-java8"))
+    implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation(project(":paging:paging-compose"))
 
     implementation(libs.kotlinStdlib)
diff --git a/compose/integration-tests/macrobenchmark-target/build.gradle b/compose/integration-tests/macrobenchmark-target/build.gradle
index 623a666a..3b4567f 100644
--- a/compose/integration-tests/macrobenchmark-target/build.gradle
+++ b/compose/integration-tests/macrobenchmark-target/build.gradle
@@ -20,6 +20,12 @@
 
     implementation(libs.kotlinStdlib)
     implementation(project(":activity:activity-compose"))
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
     implementation(project(":compose:foundation:foundation-layout"))
     implementation(project(":compose:material:material"))
     implementation(project(":compose:runtime:runtime"))
diff --git a/compose/integration-tests/material-catalog/build.gradle b/compose/integration-tests/material-catalog/build.gradle
index 49d9f2c..65d3a75 100644
--- a/compose/integration-tests/material-catalog/build.gradle
+++ b/compose/integration-tests/material-catalog/build.gradle
@@ -52,6 +52,12 @@
     implementation project(":compose:material3:material3:integration-tests:material3-catalog")
     implementation "androidx.activity:activity-compose:1.3.1"
     implementation project(":navigation:navigation-compose")
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
     implementation "com.google.accompanist:accompanist-insets:0.18.0"
 }
 
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
index a569c881..db462d1 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
+++ b/compose/runtime/runtime/compose-runtime-benchmark/build.gradle
@@ -52,6 +52,12 @@
     androidTestImplementation(projectOrArtifact(":benchmark:benchmark-junit4"))
     androidTestImplementation("androidx.activity:activity:1.2.0")
     androidTestImplementation(projectOrArtifact(":activity:activity-compose"))
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
 }
 
 androidx {
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
index b012099..c985b20 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt
@@ -689,17 +689,19 @@
             return InvalidationResult.IGNORED // The scope was removed from the composition
         if (!scope.canRecompose)
             return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated
-        if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
-            // The invalidation was redirected to the composer.
-            return InvalidationResult.IMMINENT
-        }
+        synchronized(lock) {
+            if (isComposing && composer.tryImminentInvalidation(scope, instance)) {
+                // The invalidation was redirected to the composer.
+                return InvalidationResult.IMMINENT
+            }
 
-        // invalidations[scope] containing an explicit null means it was invalidated
-        // unconditionally.
-        if (instance == null) {
-            invalidations[scope] = null
-        } else {
-            invalidations.addValue(scope, instance)
+            // invalidations[scope] containing an explicit null means it was invalidated
+            // unconditionally.
+            if (instance == null) {
+                invalidations[scope] = null
+            } else {
+                invalidations.addValue(scope, instance)
+            }
         }
 
         parent.invalidate(this)
diff --git a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
index 25b9fb5..ca50efb 100644
--- a/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/test/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -45,6 +45,7 @@
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.TestCoroutineDispatcher
@@ -3193,6 +3194,41 @@
         thread.interrupt()
     }
 
+    @Test
+    @OptIn(ExperimentalCoroutinesApi::class)
+    fun avoidRaceConditionWhenInvalidating() = compositionTest {
+        var scope: RecomposeScope? = null
+        var count = 0
+        var threadException: Exception? = null
+        val thread = thread {
+            try {
+                while (!Thread.interrupted()) {
+                    scope?.invalidate()
+                    count++
+                }
+            } catch (e: Exception) {
+                threadException = e
+            }
+        }
+
+        compose {
+            scope = currentRecomposeScope
+            Text("Some text")
+            Text("Count $count")
+        }
+
+        repeat(20) {
+            advance(ignorePendingWork = true)
+            delay(1)
+        }
+
+        thread.interrupt()
+        @Suppress("BlockingMethodInNonBlockingContext")
+        thread.join()
+        delay(10)
+        threadException?.let { throw it }
+    }
+
     @Test // b/197064250 and others
     fun canInvalidateDuringApplyChanges() = compositionTest {
         var value by mutableStateOf(0)
diff --git a/compose/test-utils/build.gradle b/compose/test-utils/build.gradle
index e792129..e8f5622 100644
--- a/compose/test-utils/build.gradle
+++ b/compose/test-utils/build.gradle
@@ -44,6 +44,12 @@
         implementation(projectOrArtifact(":compose:ui:ui-unit"))
         implementation(projectOrArtifact(":compose:ui:ui-graphics"))
         implementation(projectOrArtifact(":activity:activity-compose"))
+        // old version of common-java8 conflicts with newer version, because both have
+        // DefaultLifecycleEventObserver.
+        // Outside of androidx this is resolved via constraint added to lifecycle-common,
+        // but it doesn't work in androidx.
+        // See aosp/1804059
+        implementation(projectOrArtifact(":lifecycle:lifecycle-common-java8"))
         implementation(libs.testCore)
         implementation(libs.testRules)
 
diff --git a/compose/ui/ui-tooling/build.gradle b/compose/ui/ui-tooling/build.gradle
index f1e7484..7b39480 100644
--- a/compose/ui/ui-tooling/build.gradle
+++ b/compose/ui/ui-tooling/build.gradle
@@ -102,6 +102,13 @@
 
             androidAndroidTest.dependencies {
                 implementation(project(":compose:ui:ui-test-junit4"))
+                // old version of common-java8 conflicts with newer version, because both have
+                // DefaultLifecycleEventObserver.
+                // Outside of androidx this is resolved via constraint added to lifecycle-common,
+                // but it doesn't work in androidx.
+                // See aosp/1804059
+                implementation(project(":lifecycle:lifecycle-common-java8"))
+                implementation(project(":lifecycle:lifecycle-viewmodel-savedstate"))
 
                 implementation(libs.junit)
                 implementation(libs.testRunner)
diff --git a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
index c26798e..db86c40 100644
--- a/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
+++ b/compose/ui/ui-tooling/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClockTest.kt
@@ -127,6 +127,56 @@
     }
 
     @Test
+    fun getAnimatedPropertiesWithNotSyncedTime() {
+        var rotationAnimation: ComposeAnimation? = null
+        var offsetAnimation: ComposeAnimation? = null
+        var animatedVisibility: Transition<Any>? = null
+
+        composeRule.setContent {
+            rotationAnimation = setUpRotationColorScenario()
+            offsetAnimation = setUpOffsetScenario()
+            animatedVisibility = createAnimationVisibility(1000)
+        }
+        composeRule.waitForIdle()
+        testClock.trackAnimatedVisibility(animatedVisibility!!)
+        composeRule.waitForIdle()
+        val animatedVisibilityComposeAnimation = testClock.trackedAnimatedVisibility.single()
+        testClock.setClockTimes(
+            mapOf(
+                rotationAnimation!! to 500,
+                offsetAnimation!! to 200,
+                animatedVisibilityComposeAnimation to 800
+            )
+        )
+        composeRule.waitForIdle()
+
+        var animatedProperties = testClock.getAnimatedProperties(rotationAnimation!!)
+        val rotation = animatedProperties.single { it.label == "myRotation" }
+        // We're animating from RC1 (0 degrees) to RC3 (360 degrees). There is a transition of
+        // 1000ms defined for the rotation, and we set the clock to 50% of this time.
+        assertEquals(180f, rotation.value as Float, eps)
+
+        animatedProperties = testClock.getAnimatedProperties(offsetAnimation!!)
+        val offset = animatedProperties.single { it.label == "myOffset" }
+        // We're animating from O1 (0) to O2 (100). There is a transition of 800ms defined for
+        // the offset, and we set the clock to 25% of this time.
+        assertEquals(25f, offset.value as Float, eps)
+
+        animatedProperties = testClock.getAnimatedProperties(animatedVisibilityComposeAnimation)
+        val scale = animatedProperties.single { it.label == "box scale" }
+        // We're animating from invisible to visible, which means PreEnter (scale 0.5f) to
+        // Visible (scale 1f). Animation duration is 1000ms, so the current clock time
+        // corresponds to 80% of it.
+        assertEquals(0.9f, scale.value as Float, 0.0001f)
+
+        animatedProperties = testClock.getAnimatedProperties(animatedVisibilityComposeAnimation)
+        val alpha = animatedProperties.single { it.label == "Built-in alpha" }
+        // We're animating from invisible (Built-in alpha 0f) to visible (Built-in alpha 1f),
+        // 1000ms being the animation duration, clock time corresponds to 80% of it.
+        assertEquals(0.8f, alpha.value)
+    }
+
+    @Test
     fun getAnimatedPropertiesReturnsAllDescendantAnimations() {
         var transitionAnimation: ComposeAnimation? = null
 
diff --git a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
index a9932f0..573818c 100644
--- a/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
+++ b/compose/ui/ui-tooling/src/androidMain/kotlin/androidx/compose/ui/tooling/animation/PreviewAnimationClock.kt
@@ -284,22 +284,29 @@
      * via reflection from Android Studio.
      */
     fun setClockTime(animationTimeMs: Long) {
-        val timeNs = TimeUnit.MILLISECONDS.toNanos(animationTimeMs)
-        trackedTransitions.forEach { composeAnimation ->
-            composeAnimation.animationObject.let {
-                val states = transitionStates[it] ?: return@let
-                it.setPlaytimeAfterInitialAndTargetStateEstablished(
-                    states.current,
-                    states.target,
-                    timeNs
-                )
-            }
-        }
-        trackedAnimatedVisibility.forEach { composeAnimation ->
-            composeAnimation.animationObject.let {
-                val (current, target) =
-                    animatedVisibilityStates[it]?.toCurrentTargetPair() ?: return@let
-                it.setPlaytimeAfterInitialAndTargetStateEstablished(current, target, timeNs)
+        setClockTimes((trackedTransitions + trackedAnimatedVisibility)
+            .associateWith { animationTimeMs })
+    }
+
+    /**
+     * Seeks each animation being tracked to the given [animationTimeMillis]. Expected to be called
+     * via reflection from Android Studio.
+     */
+    fun setClockTimes(animationTimeMillis: Map<ComposeAnimation, Long>) {
+        animationTimeMillis.forEach { (composeAnimation, millis) ->
+            val timeNs = TimeUnit.MILLISECONDS.toNanos(millis)
+            if (trackedTransitions.contains(composeAnimation)) {
+                (composeAnimation as TransitionComposeAnimation).animationObject.let {
+                    val states = transitionStates[it] ?: return@let
+                    it.setPlaytimeAfterInitialAndTargetStateEstablished(
+                        states.current, states.target, timeNs)
+                }
+            } else if (trackedAnimatedVisibility.contains(composeAnimation)) {
+                (composeAnimation as AnimatedVisibilityComposeAnimation).animationObject.let {
+                    val (current, target) =
+                        animatedVisibilityStates[it]?.toCurrentTargetPair() ?: return@let
+                    it.setPlaytimeAfterInitialAndTargetStateEstablished(current, target, timeNs)
+                }
             }
         }
         setAnimationsTimeCallback.invoke()
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 2c6747b..27ce9ff 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
@@ -116,7 +116,6 @@
 import org.junit.Assert.assertNotNull
 import org.junit.Assert.assertSame
 import org.junit.Assert.assertTrue
-import org.junit.Assert.fail
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -505,21 +504,6 @@
         validateSquareColors(outerColor = green, innerColor = white, size = 20)
     }
 
-    // Tests that calling measure multiple times on the same Measurable causes an exception
-    @Test
-    fun multipleMeasureCall() {
-        val latch = CountDownLatch(1)
-        activityTestRule.runOnUiThreadIR {
-            activity.setContent {
-                TwoMeasureLayout(50, latch) {
-                    AtLeastSize(50) {
-                    }
-                }
-            }
-        }
-        assertTrue(latch.await(1, TimeUnit.SECONDS))
-    }
-
     @Test
     fun multiChildLayoutTest() {
         val childrenCount = 3
@@ -4019,33 +4003,6 @@
 }
 
 @Composable
-fun TwoMeasureLayout(
-    size: Int,
-    latch: CountDownLatch,
-    modifier: Modifier = Modifier,
-    content: @Composable () -> Unit
-) {
-    Layout(modifier = modifier, content = content) { measurables, _ ->
-        val testConstraints = Constraints()
-        measurables.forEach { it.measure(testConstraints) }
-        val childConstraints = Constraints.fixed(size, size)
-        try {
-            val placeables2 = measurables.map { it.measure(childConstraints) }
-            fail("Measuring twice on the same Measurable should throw an exception")
-            layout(size, size) {
-                placeables2.forEach { child ->
-                    child.placeRelative(0, 0)
-                }
-            }
-        } catch (_: IllegalStateException) {
-            // expected
-            latch.countDown()
-        }
-        layout(0, 0) { }
-    }
-}
-
-@Composable
 fun Wrap(
     modifier: Modifier = Modifier,
     minWidth: Int = 0,
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
index dd0f380..b615ca8 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/Helpers.kt
@@ -50,6 +50,9 @@
             on { snapshotObserver } doAnswer {
                 OwnerSnapshotObserver { it.invoke() }
             }
+            on { forceMeasureTheSubtree(any()) } doAnswer {
+                delegate.forceMeasureTheSubtree(it.arguments[0] as LayoutNode)
+            }
         }
     )
     if (firstMeasureCompleted) {
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
index 9136657..484e2ce 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasureAndLayoutDelegateTest.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.ui.layout
 
+import androidx.compose.ui.node.LayoutNode
 import androidx.compose.ui.node.LayoutNode.LayoutState
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
 import androidx.compose.ui.unit.Constraints
@@ -1230,4 +1231,55 @@
         assertThat(root.first.first.isPlaced).isFalse()
         assertThat(root.first.first.isPlaced).isFalse()
     }
+
+    @Test
+    fun remeasuringNodeSecondTimeWithinTheSameIteration() {
+        lateinit var node1: LayoutNode
+        lateinit var node2: LayoutNode
+        lateinit var node3: LayoutNode
+        lateinit var node4: LayoutNode
+        lateinit var node5: LayoutNode
+        val root = root {
+            size = 100
+            add(node {
+                node1 = this
+                size = 50
+                add(node {
+                    node2 = this
+                    add(node { node3 = this })
+                })
+                add(node { node4 = this })
+            })
+            add(node { node5 = this })
+        }
+
+        val delegate = createDelegate(root)
+
+        delegate.requestRemeasure(root)
+        // we change the root size so now node1 and node5 will be remeasured for the new size
+        root.size = 50
+        // we also want to remeasure node3. as node2 is not scheduled for remeasure node3 will
+        // be remeasured via owner.forceMeasureTheSubtree() logic.
+        delegate.requestRemeasure(node3)
+        // we also want node5 to synchronously request remeasure for already measured node1
+        node5.runDuringMeasure {
+            delegate.requestRemeasure(node1)
+        }
+        node2.toString()
+        node4.toString()
+        // this was crashing and reported as b/208675143
+        assertRemeasured(root) {
+            assertRemeasured(node1, times = 2) {
+                assertNotRemeasured(node2) {
+                    assertRemeasured(node3) {
+                        assertNotRemeasured(node4) {
+                            assertRemeasured(node5) {
+                                delegate.measureAndLayout()
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
new file mode 100644
index 0000000..a21ac3e
--- /dev/null
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/MeasuringPlacingTwiceIsNotAllowedTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.ui.layout
+
+import androidx.compose.ui.AtLeastSize
+import androidx.compose.ui.layout.Placeable.PlacementScope
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Constraints
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class MeasuringPlacingTwiceIsNotAllowedTest {
+
+    @get:Rule
+    val rule = createComposeRule()
+
+    @Test
+    fun measureTwiceInMeasureBlock() {
+        assertException(measureBlock = { measurable, constraints ->
+            measurable.measure(constraints)
+            measurable.measure(constraints)
+        })
+    }
+
+    @Test
+    fun measureTwiceInMeasureBlockWithDifferentConstraints() {
+        assertException(measureBlock = { measurable, _ ->
+            measurable.measure(Constraints.fixed(100, 100))
+            measurable.measure(Constraints.fixed(200, 200))
+        })
+    }
+
+    @Test
+    fun measureTwiceInLayoutBlock() {
+        assertException(layoutBlock = { measurable, constraints ->
+            measurable.measure(constraints)
+            measurable.measure(constraints)
+        })
+    }
+
+    @Test
+    fun measureInBothStages() {
+        assertException(
+            measureBlock = { measurable, constraints ->
+                measurable.measure(constraints)
+            },
+            layoutBlock = { measurable, constraints ->
+                measurable.measure(constraints)
+            }
+        )
+    }
+
+    @Test
+    fun placeTwiceWithTheSamePosition() {
+        assertException(
+            layoutBlock = { measurable, constraints ->
+                measurable.measure(constraints).also {
+                    it.place(0, 0)
+                    it.place(0, 0)
+                }
+            }
+        )
+    }
+
+    @Test
+    fun placeTwiceWithDifferentPositions() {
+        assertException(
+            layoutBlock = { measurable, constraints ->
+                measurable.measure(constraints).also {
+                    it.place(0, 0)
+                    it.place(10, 10)
+                }
+            }
+        )
+    }
+
+    @Test
+    fun placeTwiceWithLayer() {
+        assertException(
+            layoutBlock = { measurable, constraints ->
+                measurable.measure(constraints).also {
+                    it.placeWithLayer(0, 0)
+                    it.placeWithLayer(0, 0)
+                }
+            }
+        )
+    }
+
+    private fun assertException(
+        measureBlock: (Measurable, Constraints) -> Unit = { _, _ -> },
+        layoutBlock: PlacementScope.(Measurable, Constraints) -> Unit = { _, _ -> }
+    ) {
+        var exception: Exception? = null
+        rule.setContent {
+            Layout(content = {
+                AtLeastSize(50)
+            }) { measurables, constraints ->
+                try {
+                    measureBlock(measurables.first(), constraints)
+                } catch (e: Exception) {
+                    exception = e
+                }
+                layout(100, 100) {
+                    try {
+                        layoutBlock(measurables.first(), constraints)
+                    } catch (e: Exception) {
+                        exception = e
+                    }
+                }
+            }
+        }
+        assertThat(exception).isNotNull()
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
index 28bb807..623b031 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/NodesRemeasuredOnceTest.kt
@@ -147,6 +147,84 @@
             assertThat(remeasurements).isEqualTo(2)
         }
     }
+
+    @Test
+    fun remeasuringChildDuringLayoutWithExtraLayer() {
+        val height = mutableStateOf(10)
+        var remeasurements = 0
+
+        rule.setContent {
+            WrapChildMeasureDuringLayout(onMeasured = { actualHeight ->
+                assertThat(actualHeight).isEqualTo(height.value)
+                remeasurements++
+            }) {
+                WrapChild {
+                    Child(height)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(1)
+            height.value = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun remeasuringChildDuringLayout() {
+        val height = mutableStateOf(10)
+        var expectedHeight = 10
+        var remeasurements = 0
+
+        rule.setContent {
+            WrapChildMeasureDuringLayout(onMeasured = { actualHeight ->
+                assertThat(actualHeight).isEqualTo(expectedHeight)
+                remeasurements++
+            }) {
+                Child(height)
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(1)
+            expectedHeight = 20
+            height.value = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(2)
+        }
+    }
+
+    @Test
+    fun remeasuringChildDuringLayoutWithExtraLayerUsingIntrinsics() {
+        val height = mutableStateOf(10)
+        var remeasurements = 0
+
+        rule.setContent {
+            IntrinsicSizeAndMeasureDuringLayout(onMeasured = { actualHeight ->
+                assertThat(actualHeight).isEqualTo(height.value)
+                remeasurements++
+            }) {
+                WrapChild {
+                    Child(height)
+                }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(1)
+            height.value = 20
+        }
+
+        rule.runOnIdle {
+            assertThat(remeasurements).isEqualTo(2)
+        }
+    }
 }
 
 @Composable
@@ -162,6 +240,38 @@
 }
 
 @Composable
+private fun WrapChildMeasureDuringLayout(
+    onMeasured: (Int) -> Unit = {},
+    content: @Composable () -> Unit
+) {
+    Layout(content = content) { measurables, constraints ->
+        layout(constraints.maxWidth, constraints.maxHeight) {
+            val placeable = measurables.first()
+                .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
+            onMeasured(placeable.height)
+            placeable.place(0, 0)
+        }
+    }
+}
+
+@Composable
+private fun IntrinsicSizeAndMeasureDuringLayout(
+    onMeasured: (Int) -> Unit = {},
+    content: @Composable () -> Unit
+) {
+    Layout(content = content) { measurables, constraints ->
+        val width = measurables.first().maxIntrinsicWidth(constraints.maxWidth)
+        val height = measurables.first().maxIntrinsicHeight(constraints.maxHeight)
+        layout(width, height) {
+            val placeable = measurables.first()
+                .measure(constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity))
+            onMeasured(placeable.height)
+            placeable.place(0, 0)
+        }
+    }
+}
+
+@Composable
 private fun NotPlaceChild(height: State<Int>, content: @Composable () -> Unit) {
     Layout(content = content) { measurables, constraints ->
         layout(constraints.maxWidth, height.value) {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
index e7bac4b..90866fc 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/InnerPlaceable.kt
@@ -47,6 +47,10 @@
     override val measureScope get() = layoutNode.measureScope
 
     override fun measure(constraints: Constraints): Placeable = performingMeasure(constraints) {
+        // before rerunning the user's measure block reset previous measuredByParent for children
+        layoutNode._children.forEach {
+            it.measuredByParent = LayoutNode.UsageByParent.NotUsed
+        }
         val measureResult = with(layoutNode.measurePolicy) {
             layoutNode.measureScope.measure(layoutNode.children, constraints)
         }
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 71b3960..665a66e 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
@@ -175,9 +175,6 @@
      */
     internal var layoutState = Ready
 
-    internal val wasMeasuredDuringThisIteration: Boolean
-        get() = requireOwner().measureIteration == outerMeasurablePlaceable.measureIteration
-
     /**
      * A cache of modifiers to be used when setting and reusing previous modifiers.
      */
@@ -853,9 +850,21 @@
      * Place this layout node again on the same position it was placed last time
      */
     internal fun replace() {
-        outerMeasurablePlaceable.replace()
+        try {
+            relayoutWithoutParentInProgress = true
+            outerMeasurablePlaceable.replace()
+        } finally {
+            relayoutWithoutParentInProgress = false
+        }
     }
 
+    /**
+     * Is true during [replace] invocation. Helps to differentiate between the cases when our
+     * parent is measuring us during the measure block, and when we are remeasured individually
+     * because of some change. This could be useful to know if we need to record the placing order.
+     */
+    private var relayoutWithoutParentInProgress = false
+
     internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
 
     /**
@@ -936,7 +945,7 @@
         }
 
         if (parent != null) {
-            if (parent.layoutState == LayingOut) {
+            if (!relayoutWithoutParentInProgress && parent.layoutState == LayingOut) {
                 // the parent is currently placing its children
                 check(placeOrder == NotPlacedPlaceOrder) {
                     "Place was called on a node which was placed already"
@@ -944,8 +953,9 @@
                 placeOrder = parent.nextChildPlaceOrder
                 parent.nextChildPlaceOrder++
             }
-            // if parent is not laying out we were asked to be relaid out without affecting the
-            // parent. this means our placeOrder didn't change since the last time parent placed us
+            // if relayoutWithoutParentInProgress is true we were asked to be relaid out without
+            // affecting the parent. this means our placeOrder didn't change since the last time
+            // parent placed us.
         } else {
             // parent is null for the root node
             placeOrder = 0
@@ -973,6 +983,11 @@
                     child.previousPlaceOrder = child.placeOrder
                     child.placeOrder = NotPlacedPlaceOrder
                     child.alignmentLines.usedDuringParentLayout = false
+                    // before rerunning the user's layout block reset previous measuredByParent
+                    // for children which we measured in the layout block during the last run.
+                    if (child.measuredByParent == UsageByParent.InLayoutBlock) {
+                        child.measuredByParent = UsageByParent.NotUsed
+                    }
                 }
 
                 innerLayoutNodeWrapper.measureResult.placeChildren()
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
index 775bdd3..3dac820 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/MeasureAndLayoutDelegate.kt
@@ -124,15 +124,11 @@
             false
         }
         NeedsRelayout, Ready -> {
-            if (duringMeasureLayout && layoutNode.wasMeasuredDuringThisIteration) {
-                postponedMeasureRequests.add(layoutNode)
-            } else {
-                layoutNode.layoutState = NeedsRemeasure
-                if (layoutNode.isPlaced || layoutNode.canAffectParent) {
-                    val parentLayoutState = layoutNode.parent?.layoutState
-                    if (parentLayoutState != NeedsRemeasure) {
-                        relayoutNodes.add(layoutNode)
-                    }
+            layoutNode.layoutState = NeedsRemeasure
+            if (layoutNode.isPlaced || layoutNode.canAffectParent) {
+                val parentLayoutState = layoutNode.parent?.layoutState
+                if (parentLayoutState != NeedsRemeasure) {
+                    relayoutNodes.add(layoutNode)
                 }
             }
             !duringMeasureLayout
@@ -240,7 +236,6 @@
                 onPositionedDispatcher.onNodePositioned(layoutNode)
                 consistencyChecker?.assertConsistent()
             }
-            measureIteration++
             // execute postponed `onRequestMeasure`
             if (postponedMeasureRequests.isNotEmpty()) {
                 postponedMeasureRequests.fastForEach {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
index c35cec7..4f01beb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OuterMeasurablePlaceable.kt
@@ -44,13 +44,6 @@
     private var lastLayerBlock: (GraphicsLayerScope.() -> Unit)? = null
     private var lastZIndex: Float = 0f
 
-    /**
-     * A local version of [Owner.measureIteration] to ensure that [MeasureBlocks.measure]
-     * is not called multiple times within a measure pass.
-     */
-    var measureIteration = -1L
-        private set
-
     override var parentData: Any? = null
         private set
 
@@ -59,14 +52,25 @@
      */
     override fun measure(constraints: Constraints): Placeable {
         // when we measure the root it is like the virtual parent is currently laying out
-        val parentState = layoutNode.parent?.layoutState ?: LayoutState.LayingOut
-        layoutNode.measuredByParent = when (parentState) {
-            LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
-            LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
-            else -> throw IllegalStateException(
-                "Measurable could be only measured from the parent's measure or layout block." +
-                    "Parents state is $parentState"
-            )
+        val parent = layoutNode.parent
+        if (parent != null) {
+            check(
+                layoutNode.measuredByParent == LayoutNode.UsageByParent.NotUsed ||
+                    @Suppress("DEPRECATION") layoutNode.canMultiMeasure
+            ) {
+                "measure() may not be called multiple times on the same Measurable. Current " +
+                    "state ${layoutNode.measuredByParent}. Parent state ${parent.layoutState}."
+            }
+            layoutNode.measuredByParent = when (parent.layoutState) {
+                LayoutState.Measuring -> LayoutNode.UsageByParent.InMeasureBlock
+                LayoutState.LayingOut -> LayoutNode.UsageByParent.InLayoutBlock
+                else -> throw IllegalStateException(
+                    "Measurable could be only measured from the parent's measure or layout block." +
+                        "Parents state is ${parent.layoutState}"
+                )
+            }
+        } else {
+            layoutNode.measuredByParent = LayoutNode.UsageByParent.NotUsed
         }
         remeasure(constraints)
         return this
@@ -77,16 +81,11 @@
      */
     fun remeasure(constraints: Constraints): Boolean {
         val owner = layoutNode.requireOwner()
-        val iteration = owner.measureIteration
         val parent = layoutNode.parent
         @Suppress("Deprecation")
         layoutNode.canMultiMeasure = layoutNode.canMultiMeasure ||
             (parent != null && parent.canMultiMeasure)
         @Suppress("Deprecation")
-        check(measureIteration != iteration || layoutNode.canMultiMeasure) {
-            "measure() may not be called multiple times on the same Measurable"
-        }
-        measureIteration = owner.measureIteration
         if (layoutNode.layoutState == LayoutState.NeedsRemeasure ||
             measurementConstraints != constraints
         ) {
diff --git a/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
index 6b4847f..1cfc22d 100644
--- a/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
+++ b/emoji2/integration-tests/init-disabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/disabled/EmojiStartupBenchmark.kt
@@ -16,6 +16,7 @@
 
 package androidx.emoji2.integration.macrobenchmark.disabled
 
+import android.os.Build
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -35,7 +36,11 @@
     @Test
     fun emojiCompatInitDisabledStartup() {
         benchmarkRule.measureStartup(
-            compilationMode = CompilationMode.None,
+            compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+                CompilationMode.None()
+            } else {
+                CompilationMode.Full()
+            },
             startupMode = StartupMode.COLD,
             packageName = "androidx.emoji2.integration.macrobenchmark.disabled.target"
         ) {
diff --git a/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt b/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
index 0580f35..aa0807e 100644
--- a/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
+++ b/emoji2/integration-tests/init-enabled-macrobenchmark/src/androidTest/java/androidx/emoji2/integration/macrobenchmark/enabled/EmojiStartupBenchmark.kt
@@ -16,6 +16,7 @@
 
 package androidx.emoji2.integration.macrobenchmark.enabled
 
+import android.os.Build
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
@@ -40,7 +41,11 @@
         // only run this test if the device can configure emoji2
         assumeTrue(hasDiscoverableFontProviderOnDevice())
         benchmarkRule.measureStartup(
-            compilationMode = CompilationMode.None,
+            compilationMode = if (Build.VERSION.SDK_INT >= 24) {
+                CompilationMode.None()
+            } else {
+                CompilationMode.Full()
+            },
             startupMode = StartupMode.COLD,
             packageName = "androidx.emoji2.integration.macrobenchmark.enabled.target"
         ) {
@@ -52,4 +57,4 @@
         val context = InstrumentationRegistry.getInstrumentation().targetContext
         return DefaultEmojiCompatConfig.create(context) != null
     }
-}
\ No newline at end of file
+}
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index fdc614d..de40fab 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -26,7 +26,7 @@
 
 dependencies {
     api(project(":fragment:fragment"))
-    api("androidx.activity:activity-ktx:1.2.3") {
+    api(projectOrArtifact(":activity:activity-ktx")) {
         because "Mirror fragment dependency graph for -ktx artifacts"
     }
     api("androidx.core:core-ktx:1.2.0") {
@@ -35,10 +35,10 @@
     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.1") {
+    api(projectOrArtifact(":lifecycle:lifecycle-livedata-core-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
     api(projectOrArtifact(":savedstate:savedstate-ktx")) {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
@@ -64,12 +64,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
 }
\ No newline at end of file
diff --git a/fragment/fragment-testing/build.gradle b/fragment/fragment-testing/build.gradle
index 93569ee..0177bdf 100644
--- a/fragment/fragment-testing/build.gradle
+++ b/fragment/fragment-testing/build.gradle
@@ -51,12 +51,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
 }
\ No newline at end of file
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 39c0ddd..60abcaf 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -30,9 +30,9 @@
     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.1")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.3.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+    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")
     api(libs.kotlinStdlib)
@@ -76,12 +76,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
-}
\ No newline at end of file
+}
diff --git a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
index 8e99bf8..4c1d460 100644
--- a/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
+++ b/fragment/fragment/src/androidTest/java/androidx/fragment/app/FragmentManagerTest.kt
@@ -310,12 +310,14 @@
 
             val originalWho = fragment1.mWho
 
-            fm.beginTransaction()
-                .add(fragment1, "fragment1")
-                .setReorderingAllowed(true)
-                .addToBackStack("stack1")
-                .commit()
-            fm.popBackStack()
+            withActivity {
+                fm.beginTransaction()
+                    .add(fragment1, "fragment1")
+                    .setReorderingAllowed(true)
+                    .addToBackStack("stack1")
+                    .commit()
+                fm.popBackStack()
+            }
             executePendingTransactions()
 
             assertThat(fragment1.mWho).isNotEqualTo(originalWho)
@@ -326,6 +328,43 @@
     }
 
     @Test
+    fun reAddRemovedBeforeAttached() {
+        with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
+            val fm = withActivity {
+                supportFragmentManager
+            }
+            val fragment1 = StrictFragment()
+
+            val originalWho = fragment1.mWho
+
+            withActivity {
+                fm.beginTransaction()
+                    .add(fragment1, "fragment1")
+                    .setReorderingAllowed(true)
+                    .addToBackStack("stack1")
+                    .commit()
+                fm.popBackStack()
+            }
+            executePendingTransactions()
+
+            assertThat(fragment1.mWho).isNotEqualTo(originalWho)
+            assertThat(fragment1.mFragmentManager).isNull()
+            assertThat(fm.findFragmentByWho(originalWho)).isNull()
+            assertThat(fm.findFragmentByWho(fragment1.mWho)).isNull()
+
+            val afterRemovalWho = fragment1.mWho
+
+            fm.beginTransaction()
+                .add(fragment1, "fragment1")
+                .setReorderingAllowed(true)
+                .commit()
+            executePendingTransactions()
+
+            assertThat(fragment1.mWho).isEqualTo(afterRemovalWho)
+        }
+    }
+
+    @Test
     fun popBackStackImmediate() {
         with(ActivityScenario.launch(FragmentTestActivity::class.java)) {
             val fm = withActivity {
diff --git a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
index 8fcec63..2cd364b 100644
--- a/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
+++ b/fragment/fragment/src/main/java/androidx/fragment/app/Fragment.java
@@ -309,6 +309,13 @@
         abstract void onPreAttached();
     }
 
+    private final OnPreAttachedListener mSavedStateAttachListener = new OnPreAttachedListener() {
+        @Override
+        void onPreAttached() {
+            mSavedStateRegistryController.performAttach();
+        }
+    };
+
     /**
      * {@inheritDoc}
      * <p>
@@ -572,12 +579,9 @@
         // The default factory depends on the SavedStateRegistry so it
         // needs to be reset when the SavedStateRegistry is reset
         mDefaultFactory = null;
-        registerOnPreAttachListener(new OnPreAttachedListener() {
-            @Override
-            void onPreAttached() {
-                mSavedStateRegistryController.performAttach();
-            }
-        });
+        if (!mOnPreAttachedListeners.contains(mSavedStateAttachListener)) {
+            registerOnPreAttachListener(mSavedStateAttachListener);
+        }
     }
 
     /**
diff --git a/glance/glance-appwidget/api/current.txt b/glance/glance-appwidget/api/current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/current.txt
+++ b/glance/glance-appwidget/api/current.txt
@@ -54,7 +54,7 @@
     method @androidx.compose.runtime.Composable public abstract void Content();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
-    method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
   public final class ApplyActionKt {
   }
 
-  public final class LaunchActivityIntentActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
-  }
-
-  public final class LaunchBroadcastActionKt {
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
-    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
-    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
-  }
-
-  public final class LaunchServiceActionKt {
-    method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
-    method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
-    method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
-    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
-  }
-
   public final class RunCallbackActionKt {
     method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
     method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
   }
 
+  public final class SendBroadcastActionKt {
+    method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+  }
+
+  public final class StartActivityIntentActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+  }
+
+  public final class StartServiceActionKt {
+    method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+    method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+    method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+  }
+
   public final class ToggleableKt {
     method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
     property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/api/public_plus_experimental_current.txt b/glance/glance-appwidget/api/public_plus_experimental_current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/public_plus_experimental_current.txt
+++ b/glance/glance-appwidget/api/public_plus_experimental_current.txt
@@ -54,7 +54,7 @@
     method @androidx.compose.runtime.Composable public abstract void Content();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
-    method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
   public final class ApplyActionKt {
   }
 
-  public final class LaunchActivityIntentActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
-  }
-
-  public final class LaunchBroadcastActionKt {
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
-    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
-    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
-  }
-
-  public final class LaunchServiceActionKt {
-    method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
-    method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
-    method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
-    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
-  }
-
   public final class RunCallbackActionKt {
     method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
     method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
   }
 
+  public final class SendBroadcastActionKt {
+    method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+  }
+
+  public final class StartActivityIntentActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+  }
+
+  public final class StartServiceActionKt {
+    method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+    method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+    method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+  }
+
   public final class ToggleableKt {
     method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
     property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/api/restricted_current.txt b/glance/glance-appwidget/api/restricted_current.txt
index 607ed48..acbfe95 100644
--- a/glance/glance-appwidget/api/restricted_current.txt
+++ b/glance/glance-appwidget/api/restricted_current.txt
@@ -54,7 +54,7 @@
     method @androidx.compose.runtime.Composable public abstract void Content();
     method public androidx.glance.appwidget.SizeMode getSizeMode();
     method public androidx.glance.state.GlanceStateDefinition<?>? getStateDefinition();
-    method public suspend Object? onDelete(androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
+    method public suspend Object? onDelete(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     method public final suspend Object? update(android.content.Context context, androidx.glance.GlanceId glanceId, kotlin.coroutines.Continuation<? super kotlin.Unit> p);
     property public androidx.glance.appwidget.SizeMode sizeMode;
     property public androidx.glance.state.GlanceStateDefinition<?>? stateDefinition;
@@ -148,30 +148,30 @@
   public final class ApplyActionKt {
   }
 
-  public final class LaunchActivityIntentActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
-  }
-
-  public final class LaunchBroadcastActionKt {
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(String action, optional android.content.ComponentName? componentName);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.Intent intent);
-    method public static androidx.glance.action.Action actionLaunchBroadcastReceiver(android.content.ComponentName componentName);
-    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionLaunchBroadcastReceiver(Class<T> receiver);
-    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionLaunchBroadcastReceiver();
-  }
-
-  public final class LaunchServiceActionKt {
-    method public static androidx.glance.action.Action actionLaunchService(android.content.Intent intent, optional boolean isForegroundService);
-    method public static androidx.glance.action.Action actionLaunchService(android.content.ComponentName componentName, optional boolean isForegroundService);
-    method public static <T extends android.app.Service> androidx.glance.action.Action actionLaunchService(Class<T> service, optional boolean isForegroundService);
-    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionLaunchService(optional boolean isForegroundService);
-  }
-
   public final class RunCallbackActionKt {
     method public static <T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action actionRunCallback(Class<T> callbackClass, optional androidx.glance.action.ActionParameters parameters);
     method public static inline <reified T extends androidx.glance.appwidget.action.ActionCallback> androidx.glance.action.Action! actionRunCallback(optional androidx.glance.action.ActionParameters parameters);
   }
 
+  public final class SendBroadcastActionKt {
+    method public static androidx.glance.action.Action actionSendBroadcast(String action, optional android.content.ComponentName? componentName);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.Intent intent);
+    method public static androidx.glance.action.Action actionSendBroadcast(android.content.ComponentName componentName);
+    method public static <T extends android.content.BroadcastReceiver> androidx.glance.action.Action actionSendBroadcast(Class<T> receiver);
+    method public static inline <reified T extends android.content.BroadcastReceiver> androidx.glance.action.Action! actionSendBroadcast();
+  }
+
+  public final class StartActivityIntentActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.Intent intent, optional androidx.glance.action.ActionParameters parameters);
+  }
+
+  public final class StartServiceActionKt {
+    method public static androidx.glance.action.Action actionStartService(android.content.Intent intent, optional boolean isForegroundService);
+    method public static androidx.glance.action.Action actionStartService(android.content.ComponentName componentName, optional boolean isForegroundService);
+    method public static <T extends android.app.Service> androidx.glance.action.Action actionStartService(Class<T> service, optional boolean isForegroundService);
+    method public static inline <reified T extends android.app.Service> androidx.glance.action.Action! actionStartService(optional boolean isForegroundService);
+  }
+
   public final class ToggleableKt {
     method public static androidx.glance.action.ActionParameters.Key<java.lang.Boolean> getToggleableStateKey();
     property public static final androidx.glance.action.ActionParameters.Key<java.lang.Boolean> ToggleableStateKey;
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
index 038d797..450ba72 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ActionAppWidget.kt
@@ -39,17 +39,17 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.LocalContext
 import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionLaunchActivity
 import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.action.toParametersKey
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionLaunchActivity
-import androidx.glance.appwidget.action.actionLaunchBroadcastReceiver
-import androidx.glance.appwidget.action.actionLaunchService
 import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.action.actionSendBroadcast
+import androidx.glance.appwidget.action.actionStartActivity
+import androidx.glance.appwidget.action.actionStartService
 import androidx.glance.appwidget.appWidgetBackground
 import androidx.glance.appwidget.cornerRadius
 import androidx.glance.appwidget.state.updateAppWidgetState
@@ -86,9 +86,9 @@
             }
 
             when (currentState<Preferences>()[selectedItemKey] ?: 0) {
-                0 -> LaunchActivityActions()
-                1 -> LaunchServiceActions()
-                2 -> LaunchBroadcastReceiverActions()
+                0 -> StartActivityActions()
+                1 -> StartServiceActions()
+                2 -> SendBroadcastActions()
                 else -> throw IllegalArgumentException("Wrong index selected")
             }
         }
@@ -96,7 +96,7 @@
 }
 
 private val selectedItemKey = intPreferencesKey("selectedItemKey")
-private val launchMessageKey = ActionParameters.Key<String>("launchMessageKey")
+private val startMessageKey = ActionParameters.Key<String>("launchMessageKey")
 
 @Composable
 private fun SelectableActionItem(label: String, index: Int) {
@@ -133,87 +133,87 @@
 }
 
 @Composable
-private fun LaunchActivityActions() {
+private fun StartActivityActions() {
     Button(
         text = "Intent",
-        onClick = actionLaunchActivity(
+        onClick = actionStartActivity(
             Intent(LocalContext.current, ActionDemoActivity::class.java)
         )
     )
     Button(
         text = "Target class",
-        onClick = actionLaunchActivity<ActionDemoActivity>(),
+        onClick = actionStartActivity<ActionDemoActivity>(),
     )
     Button(
         text = "Target class with params",
-        onClick = actionLaunchActivity<ActionDemoActivity>(
+        onClick = actionStartActivity<ActionDemoActivity>(
             actionParametersOf(
-                launchMessageKey to "Launch activity by target class"
+                startMessageKey to "Start activity by target class"
             )
         )
     )
     Button(
         text = "Component name",
-        onClick = actionLaunchActivity(
+        onClick = actionStartActivity(
             ComponentName(LocalContext.current, ActionDemoActivity::class.java)
         )
     )
     Button(
         text = "Component name with params",
-        onClick = actionLaunchActivity(
+        onClick = actionStartActivity(
             ComponentName(LocalContext.current, ActionDemoActivity::class.java),
             actionParametersOf(
-                launchMessageKey to "Launch activity by component name"
+                startMessageKey to "Start activity by component name"
             )
         )
     )
 }
 
 @Composable
-private fun LaunchServiceActions() {
+private fun StartServiceActions() {
     Button(
         text = "Intent",
-        onClick = actionLaunchService(
+        onClick = actionStartService(
             Intent(LocalContext.current, ActionDemoService::class.java)
         )
     )
     Button(
         text = "Target class",
-        onClick = actionLaunchService<ActionDemoService>()
+        onClick = actionStartService<ActionDemoService>()
     )
     Button(
         text = "In foreground",
-        onClick = actionLaunchService<ActionDemoService>(isForegroundService = true)
+        onClick = actionStartService<ActionDemoService>(isForegroundService = true)
     )
     Button(
         text = "Component name",
-        onClick = actionLaunchService(
+        onClick = actionStartService(
             ComponentName(LocalContext.current, ActionDemoService::class.java)
         )
     )
 }
 
 @Composable
-private fun LaunchBroadcastReceiverActions() {
+private fun SendBroadcastActions() {
     Button(
         text = "Intent",
-        onClick = actionLaunchBroadcastReceiver(
+        onClick = actionSendBroadcast(
             Intent(LocalContext.current, ActionAppWidgetReceiver::class.java)
         )
     )
     Button(
         text = "Action",
-        onClick = actionLaunchBroadcastReceiver(
+        onClick = actionSendBroadcast(
             AppWidgetManager.ACTION_APPWIDGET_UPDATE
         )
     )
     Button(
         text = "Target class",
-        onClick = actionLaunchBroadcastReceiver<ActionAppWidgetReceiver>()
+        onClick = actionSendBroadcast<ActionAppWidgetReceiver>()
     )
     Button(
         text = "Component name",
-        onClick = actionLaunchBroadcastReceiver(
+        onClick = actionSendBroadcast(
             ComponentName(LocalContext.current, ActionAppWidgetReceiver::class.java)
         )
     )
@@ -235,7 +235,7 @@
 }
 
 /**
- * Placeholder activity to launch via [actionLaunchActivity]
+ * Placeholder activity to launch via [actionStartActivity]
  */
 class ActionDemoActivity : ComponentActivity() {
 
@@ -246,7 +246,7 @@
                 modifier = Modifier.fillMaxSize(),
                 contentAlignment = androidx.compose.ui.Alignment.Center
             ) {
-                val message = intent.getStringExtra(launchMessageKey.name) ?: "Not found"
+                val message = intent.getStringExtra(startMessageKey.name) ?: "Not found"
                 androidx.compose.material.Text(message)
             }
         }
@@ -255,7 +255,7 @@
 }
 
 /**
- * Placeholder service to launch via [actionLaunchService]
+ * Placeholder service to launch via [actionStartService]
  */
 class ActionDemoService : Service() {
     override fun onBind(intent: Intent?): IBinder? = null
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
index 9599aa9..4c9c6f7 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ResponsiveAppWidget.kt
@@ -28,12 +28,12 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.LocalSize
 import androidx.glance.action.ActionParameters
-import androidx.glance.appwidget.action.ActionCallback
-import androidx.glance.appwidget.action.actionRunCallback
 import androidx.glance.action.actionParametersOf
 import androidx.glance.appwidget.GlanceAppWidget
 import androidx.glance.appwidget.GlanceAppWidgetReceiver
 import androidx.glance.appwidget.SizeMode
+import androidx.glance.appwidget.action.ActionCallback
+import androidx.glance.appwidget.action.actionRunCallback
 import androidx.glance.background
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.Box
diff --git a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
index 16c26a3..5c46dae 100644
--- a/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
+++ b/glance/glance-appwidget/integration-tests/demos/src/main/java/androidx/glance/appwidget/demos/ScrollableAppWidget.kt
@@ -44,8 +44,8 @@
 import androidx.glance.appwidget.SizeMode
 import androidx.glance.appwidget.action.ActionCallback
 import androidx.glance.appwidget.action.ToggleableStateKey
-import androidx.glance.appwidget.action.actionLaunchActivity
 import androidx.glance.appwidget.action.actionRunCallback
+import androidx.glance.appwidget.action.actionStartActivity
 import androidx.glance.appwidget.demos.ScrollableAppWidget.Companion.checkboxKey
 import androidx.glance.appwidget.lazy.LazyColumn
 import androidx.glance.appwidget.lazy.itemsIndexed
@@ -139,7 +139,7 @@
             ) {
                 Button(
                     text = "Activity ${index + 1}",
-                    onClick = actionLaunchActivity(
+                    onClick = actionStartActivity(
                         Intent(
                             LocalContext.current,
                             activityClass
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
index 978172a..d64f0a1 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverScreenshotTest.kt
@@ -28,7 +28,7 @@
 import androidx.glance.Image
 import androidx.glance.ImageProvider
 import androidx.glance.LocalContext
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
 import androidx.glance.appwidget.test.R
 import androidx.glance.appwidget.unit.ColorProvider
 import androidx.glance.background
@@ -210,13 +210,13 @@
                 Row(modifier = GlanceModifier.defaultWeight().fillMaxWidth()) {
                     Button(
                         "Start",
-                        onClick = actionLaunchActivity<Activity>(),
+                        onClick = actionStartActivity<Activity>(),
                         modifier = GlanceModifier.defaultWeight().fillMaxHeight(),
                         style = TextStyle(textAlign = TextAlign.Start)
                     )
                     Button(
                         "End",
-                        onClick = actionLaunchActivity<Activity>(),
+                        onClick = actionStartActivity<Activity>(),
                         modifier = GlanceModifier.defaultWeight().fillMaxHeight(),
                         style = TextStyle(textAlign = TextAlign.End)
                     )
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
index 9aed573..8a868ff 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/GlanceAppWidgetReceiverTest.kt
@@ -47,8 +47,8 @@
 import androidx.glance.LocalContext
 import androidx.glance.LocalSize
 import androidx.glance.action.ActionParameters
-import androidx.glance.action.actionLaunchActivity
 import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.action.toParametersKey
 import androidx.glance.appwidget.action.ActionCallback
@@ -393,7 +393,7 @@
     @Test
     fun createButton() {
         TestGlanceAppWidget.uiDefinition = {
-            Button("Button", onClick = actionLaunchActivity<Activity>(), enabled = false)
+            Button("Button", onClick = actionStartActivity<Activity>(), enabled = false)
         }
 
         mHostRule.startHost()
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
index 23acd1a..84e7151 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/LazyColumnTest.kt
@@ -20,17 +20,17 @@
 import android.os.Build
 import android.view.Gravity
 import android.view.View
-import android.widget.Button
 import android.view.ViewGroup
+import android.widget.Button
 import android.widget.FrameLayout
 import android.widget.ListView
 import android.widget.TextView
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import androidx.glance.Button
 import androidx.core.view.children
+import androidx.glance.Button
 import androidx.glance.GlanceModifier
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.appwidget.lazy.LazyColumn
 import androidx.glance.appwidget.lazy.ReservedItemIdRangeEnd
@@ -323,11 +323,11 @@
                 item {
                     Text(
                         "Text",
-                        modifier = GlanceModifier.clickable(actionLaunchActivity<Activity>())
+                        modifier = GlanceModifier.clickable(actionStartActivity<Activity>())
                     )
                     Button(
                         "Button",
-                        onClick = actionLaunchActivity<Activity>()
+                        onClick = actionStartActivity<Activity>()
                     )
                 }
             }
diff --git a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
index 12ac3f0..89fc5f1 100644
--- a/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
+++ b/glance/glance-appwidget/src/androidAndroidTest/kotlin/androidx/glance/appwidget/TestGlanceAppWidgetReceiver.kt
@@ -16,6 +16,7 @@
 
 package androidx.glance.appwidget
 
+import android.content.Context
 import androidx.compose.runtime.Composable
 import androidx.glance.GlanceId
 import androidx.glance.state.GlanceStateDefinition
@@ -45,7 +46,7 @@
         onDeleteBlock = null
     }
 
-    override suspend fun onDelete(glanceId: GlanceId) {
+    override suspend fun onDelete(context: Context, glanceId: GlanceId) {
         onDeleteBlock?.apply { this(glanceId) }
     }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
index ddcb533..778b197 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/GlanceAppWidget.kt
@@ -43,9 +43,9 @@
 import androidx.glance.LocalSize
 import androidx.glance.LocalState
 import androidx.glance.appwidget.state.getAppWidgetState
-import kotlinx.coroutines.CancellationException
 import androidx.glance.state.GlanceState
 import androidx.glance.state.GlanceStateDefinition
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.async
 import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.coroutineScope
@@ -89,7 +89,7 @@
      *
      * When the method returns, the state associated with the [glanceId] will be deleted.
      */
-    public open suspend fun onDelete(glanceId: GlanceId) { }
+    public open suspend fun onDelete(context: Context, glanceId: GlanceId) {}
 
     /**
      * Triggers the composition of [Content] and sends the result to the [AppWidgetManager].
@@ -109,7 +109,7 @@
     internal suspend fun deleted(context: Context, appWidgetId: Int) {
         val glanceId = AppWidgetId(appWidgetId)
         try {
-            onDelete(glanceId)
+            onDelete(context, glanceId)
         } catch (cancelled: CancellationException) {
             // Nothing to do here
         } catch (t: Throwable) {
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
index 70cc6cd..eeb0379 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/ApplyAction.kt
@@ -28,9 +28,9 @@
 import androidx.core.os.bundleOf
 import androidx.glance.action.Action
 import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.LaunchActivityClassAction
-import androidx.glance.action.LaunchActivityComponentAction
+import androidx.glance.action.StartActivityAction
+import androidx.glance.action.StartActivityClassAction
+import androidx.glance.action.StartActivityComponentAction
 import androidx.glance.action.toMutableParameters
 import androidx.glance.appwidget.GlanceAppWidgetTag
 import androidx.glance.appwidget.TranslationContext
@@ -71,10 +71,10 @@
     editParams: (ActionParameters) -> ActionParameters = { it },
 ): PendingIntent {
     when (action) {
-        is LaunchActivityAction -> {
+        is StartActivityAction -> {
             val params = editParams(action.parameters)
-            val intent = getLaunchActivityIntent(action, translationContext, params)
-            val finalIntent = if (action !is LaunchActivityIntentAction && !params.isEmpty()) {
+            val intent = getStartActivityIntent(action, translationContext, params)
+            val finalIntent = if (action !is StartActivityIntentAction && !params.isEmpty()) {
                 intent.applyTrampolineIntent(
                     translationContext,
                     viewId,
@@ -90,8 +90,8 @@
                 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
             )
         }
-        is LaunchServiceAction -> {
-            val intent = getLaunchServiceIntent(action, translationContext)
+        is StartServiceAction -> {
+            val intent = getServiceIntent(action, translationContext)
             return if (action.isForegroundService &&
                 Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
             ) {
@@ -108,11 +108,11 @@
                 )
             }
         }
-        is LaunchBroadcastReceiverAction -> {
+        is SendBroadcastAction -> {
             return PendingIntent.getBroadcast(
                 translationContext.context,
                 0,
-                getLaunchBroadcastReceiverIntent(action, translationContext),
+                getBroadcastReceiverIntent(action, translationContext),
                 PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
             )
         }
@@ -154,8 +154,8 @@
     @IdRes viewId: Int,
     editParams: (ActionParameters) -> ActionParameters = { it }
 ): Intent = when (action) {
-    is LaunchActivityAction -> {
-        getLaunchActivityIntent(
+    is StartActivityAction -> {
+        getStartActivityIntent(
             action = action,
             translationContext = translationContext,
             params = editParams(action.parameters)
@@ -165,8 +165,8 @@
             type = ActionTrampolineType.ACTIVITY,
         )
     }
-    is LaunchServiceAction -> {
-        getLaunchServiceIntent(
+    is StartServiceAction -> {
+        getServiceIntent(
             action = action,
             translationContext = translationContext
         ).applyTrampolineIntent(
@@ -179,8 +179,8 @@
             },
         )
     }
-    is LaunchBroadcastReceiverAction -> {
-        getLaunchBroadcastReceiverIntent(
+    is SendBroadcastAction -> {
+        getBroadcastReceiverIntent(
             action = action,
             translationContext = translationContext
         ).applyTrampolineIntent(
@@ -223,37 +223,37 @@
         }
     }
 
-private fun getLaunchBroadcastReceiverIntent(
-    action: LaunchBroadcastReceiverAction,
+private fun getBroadcastReceiverIntent(
+    action: SendBroadcastAction,
     translationContext: TranslationContext,
 ): Intent = when (action) {
-    is LaunchBroadcastReceiverComponentAction -> Intent().setComponent(action.componentName)
-    is LaunchBroadcastReceiverClassAction ->
+    is SendBroadcastComponentAction -> Intent().setComponent(action.componentName)
+    is SendBroadcastClassAction ->
         Intent(translationContext.context, action.receiverClass)
-    is LaunchBroadcastReceiverIntentAction -> action.intent
-    is LaunchBroadcastReceiverActionAction ->
+    is SendBroadcastIntentAction -> action.intent
+    is SendBroadcastActionAction ->
         Intent(action.action).setComponent(action.componentName)
 }
 
-private fun getLaunchServiceIntent(
-    action: LaunchServiceAction,
+private fun getServiceIntent(
+    action: StartServiceAction,
     translationContext: TranslationContext,
 ): Intent = when (action) {
-    is LaunchServiceComponentAction -> Intent().setComponent(action.componentName)
-    is LaunchServiceClassAction ->
+    is StartServiceComponentAction -> Intent().setComponent(action.componentName)
+    is StartServiceClassAction ->
         Intent(translationContext.context, action.serviceClass)
-    is LaunchServiceIntentAction -> action.intent
+    is StartServiceIntentAction -> action.intent
 }
 
-private fun getLaunchActivityIntent(
-    action: LaunchActivityAction,
+private fun getStartActivityIntent(
+    action: StartActivityAction,
     translationContext: TranslationContext,
     params: ActionParameters,
 ): Intent {
     val activityIntent = when (action) {
-        is LaunchActivityComponentAction -> Intent().setComponent(action.componentName)
-        is LaunchActivityClassAction -> Intent(translationContext.context, action.activityClass)
-        is LaunchActivityIntentAction -> action.intent
+        is StartActivityComponentAction -> Intent().setComponent(action.componentName)
+        is StartActivityClassAction -> Intent(translationContext.context, action.activityClass)
+        is StartActivityIntentAction -> action.intent
         else -> error("Action type not defined in app widget package: $action")
     }
 
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
similarity index 66%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
index c4a050f..a2edcd0 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchBroadcastAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/SendBroadcastAction.kt
@@ -21,24 +21,24 @@
 import android.content.Intent
 import androidx.glance.action.Action
 
-internal sealed interface LaunchBroadcastReceiverAction : Action
+internal sealed interface SendBroadcastAction : Action
 
-internal class LaunchBroadcastReceiverActionAction(
+internal class SendBroadcastActionAction(
     val action: String,
     val componentName: ComponentName? = null,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
 
-internal class LaunchBroadcastReceiverComponentAction(
+internal class SendBroadcastComponentAction(
     val componentName: ComponentName,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
 
-internal class LaunchBroadcastReceiverClassAction(
+internal class SendBroadcastClassAction(
     val receiverClass: Class<out BroadcastReceiver>,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
 
-internal class LaunchBroadcastReceiverIntentAction(
+internal class SendBroadcastIntentAction(
     val intent: Intent,
-) : LaunchBroadcastReceiverAction
+) : SendBroadcastAction
 
 /**
  * Creates an [Action] that launches the [BroadcastReceiver] specified by the given action.
@@ -46,10 +46,10 @@
  * @param action of the BroadcastReceiver to launch
  * @param componentName optional [ComponentName] of the target BroadcastReceiver
  */
-public fun actionLaunchBroadcastReceiver(
+public fun actionSendBroadcast(
     action: String,
     componentName: ComponentName? = null
-): Action = LaunchBroadcastReceiverActionAction(action, componentName)
+): Action = SendBroadcastActionAction(action, componentName)
 
 /**
  * Creates an [Action] that launches a [BroadcastReceiver] from the given [Intent] when triggered.
@@ -57,28 +57,28 @@
  *
  * @param intent the [Intent] used to launch the [BroadcastReceiver]
  */
-public fun actionLaunchBroadcastReceiver(intent: Intent): Action =
-    LaunchBroadcastReceiverIntentAction(intent)
+public fun actionSendBroadcast(intent: Intent): Action =
+    SendBroadcastIntentAction(intent)
 
 /**
  * Creates an [Action] that launches the [BroadcastReceiver] specified by the given [ComponentName].
  *
  * @param componentName component of the [BroadcastReceiver] to launch
  */
-public fun actionLaunchBroadcastReceiver(componentName: ComponentName): Action =
-    LaunchBroadcastReceiverComponentAction(componentName)
+public fun actionSendBroadcast(componentName: ComponentName): Action =
+    SendBroadcastComponentAction(componentName)
 
 /**
  * Creates an [Action] that launches the specified [BroadcastReceiver] when triggered.
  *
  * @param receiver class of the [BroadcastReceiver] to launch
  */
-public fun <T : BroadcastReceiver> actionLaunchBroadcastReceiver(receiver: Class<T>): Action =
-    LaunchBroadcastReceiverClassAction(receiver)
+public fun <T : BroadcastReceiver> actionSendBroadcast(receiver: Class<T>): Action =
+    SendBroadcastClassAction(receiver)
 
 /**
  * Creates an [Action] that launches the specified [BroadcastReceiver] when triggered.
  */
 @Suppress("MissingNullability") // Shouldn't need to specify @NonNull. b/199284086
-public inline fun <reified T : BroadcastReceiver> actionLaunchBroadcastReceiver(): Action =
-    actionLaunchBroadcastReceiver(T::class.java)
+public inline fun <reified T : BroadcastReceiver> actionSendBroadcast(): Action =
+    actionSendBroadcast(T::class.java)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
similarity index 87%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
index 7e7f48a..1fd2a0f 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartActivityIntentAction.kt
@@ -20,13 +20,13 @@
 import android.content.Intent
 import androidx.glance.action.Action
 import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
+import androidx.glance.action.StartActivityAction
 import androidx.glance.action.actionParametersOf
 
-internal class LaunchActivityIntentAction(
+internal class StartActivityIntentAction(
     val intent: Intent,
     override val parameters: ActionParameters = actionParametersOf()
-) : LaunchActivityAction
+) : StartActivityAction
 
 /**
  * Creates an [Action] that launches an [Activity] from the given [Intent] when triggered. The
@@ -38,7 +38,7 @@
  * @param parameters the parameters associated with the action. Parameter values will be added to
  * the activity intent, keyed by the parameter key name string.
  */
-public fun actionLaunchActivity(
+public fun actionStartActivity(
     intent: Intent,
     parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityIntentAction(intent, parameters)
+): Action = StartActivityIntentAction(intent, parameters)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
similarity index 77%
rename from glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt
rename to glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
index 183e9bf..5b2541d 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/LaunchServiceAction.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/action/StartServiceAction.kt
@@ -21,24 +21,24 @@
 import android.content.Intent
 import androidx.glance.action.Action
 
-internal sealed interface LaunchServiceAction : Action {
+internal sealed interface StartServiceAction : Action {
     val isForegroundService: Boolean
 }
 
-internal class LaunchServiceComponentAction(
+internal class StartServiceComponentAction(
     val componentName: ComponentName,
     override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
 
-internal class LaunchServiceClassAction(
+internal class StartServiceClassAction(
     val serviceClass: Class<out Service>,
     override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
 
-internal class LaunchServiceIntentAction(
+internal class StartServiceIntentAction(
     val intent: Intent,
     override val isForegroundService: Boolean
-) : LaunchServiceAction
+) : StartServiceAction
 
 /**
  * Creates an [Action] that launches a [Service] from the given [Intent] when triggered. The
@@ -49,8 +49,8 @@
  * is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
  * foreground service to be launched differently
  */
-public fun actionLaunchService(intent: Intent, isForegroundService: Boolean = false): Action =
-    LaunchServiceIntentAction(intent, isForegroundService)
+public fun actionStartService(intent: Intent, isForegroundService: Boolean = false): Action =
+    StartServiceIntentAction(intent, isForegroundService)
 
 /**
  * Creates an [Action] that launches the [Service] specified by the given [ComponentName].
@@ -60,10 +60,10 @@
  * is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
  * foreground service to be launched differently
  */
-public fun actionLaunchService(
+public fun actionStartService(
     componentName: ComponentName,
     isForegroundService: Boolean = false
-): Action = LaunchServiceComponentAction(componentName, isForegroundService)
+): Action = StartServiceComponentAction(componentName, isForegroundService)
 
 /**
  * Creates an [Action] that launches the specified [Service] when triggered.
@@ -73,11 +73,11 @@
  * is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
  * foreground service to be launched differently
  */
-public fun <T : Service> actionLaunchService(
+public fun <T : Service> actionStartService(
     service: Class<T>,
     isForegroundService: Boolean = false
 ): Action =
-    LaunchServiceClassAction(service, isForegroundService)
+    StartServiceClassAction(service, isForegroundService)
 
 /**
  * Creates an [Action] that launches the specified [Service] when triggered.
@@ -86,7 +86,8 @@
  * is only used for device versions after [android.os.Build.VERSION_CODES.O] that requires
  * foreground service to be launched differently.
  */
-@Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
-public inline fun <reified T : Service> actionLaunchService(
+@Suppress("MissingNullability")
+/* Shouldn't need to specify @NonNull. b/199284086 */
+public inline fun <reified T : Service> actionStartService(
     isForegroundService: Boolean = false
-): Action = actionLaunchService(T::class.java, isForegroundService)
+): Action = actionStartService(T::class.java, isForegroundService)
diff --git a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
index 4501b4c..f22391fffd 100644
--- a/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
+++ b/glance/glance-appwidget/src/androidMain/kotlin/androidx/glance/appwidget/translators/LazyListTranslator.kt
@@ -58,8 +58,8 @@
         "Glance does not support nested list views."
     }
     // TODO(b/205868100): Remove [FILL_IN_COMPONENT] flag and set target component here when all
-    // click actions on descendants are exclusively [LaunchActivityAction] or exclusively not
-    // [LaunchActivityAction].
+    // click actions on descendants are exclusively [StartActivityAction] or exclusively not
+    // [StartActivityAction].
     setPendingIntentTemplate(
         viewDef.mainViewId,
         PendingIntent.getActivity(
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
index 412938b..349f28f 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/RemoteViewsTranslatorKtTest.kt
@@ -39,7 +39,7 @@
 import androidx.glance.Button
 import androidx.glance.GlanceModifier
 import androidx.glance.Visibility
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.appwidget.FrameLayoutSubject.Companion.assertThat
 import androidx.glance.appwidget.LinearLayoutSubject.Companion.assertThat
@@ -71,9 +71,9 @@
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.annotation.Config
+import org.robolectric.shadows.ShadowLog
 import kotlin.test.assertFailsWith
 import kotlin.test.assertIs
-import org.robolectric.shadows.ShadowLog
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
@@ -694,7 +694,7 @@
         val rv = context.runAndTranslate {
             Button(
                 "Button",
-                onClick = actionLaunchActivity<Activity>(),
+                onClick = actionStartActivity<Activity>(),
                 enabled = true
             )
         }
@@ -710,7 +710,7 @@
         val rv = context.runAndTranslate {
             Button(
                 "Button",
-                onClick = actionLaunchActivity<Activity>(),
+                onClick = actionStartActivity<Activity>(),
                 enabled = false
             )
         }
@@ -846,9 +846,9 @@
             Text(
                 "text1",
                 modifier = GlanceModifier.clickable(
-                    actionLaunchActivity(ComponentName("package", "class"))
+                    actionStartActivity(ComponentName("package", "class"))
                 ).clickable(
-                    actionLaunchActivity(ComponentName("package", "class2"))
+                    actionStartActivity(ComponentName("package", "class2"))
                 )
             )
         }
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
index a154511..5aa780f 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/WidgetLayoutTest.kt
@@ -23,19 +23,19 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
 import androidx.glance.ImageProvider
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
 import androidx.glance.appwidget.lazy.LazyColumn
+import androidx.glance.appwidget.proto.LayoutProto
 import androidx.glance.appwidget.test.R
 import androidx.glance.layout.Alignment
+import androidx.glance.layout.Box
 import androidx.glance.layout.Column
 import androidx.glance.layout.ContentScale
+import androidx.glance.layout.Row
 import androidx.glance.layout.fillMaxHeight
 import androidx.glance.layout.fillMaxSize
 import androidx.glance.layout.width
 import androidx.glance.layout.wrapContentWidth
-import androidx.glance.appwidget.proto.LayoutProto
-import androidx.glance.layout.Box
-import androidx.glance.layout.Row
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -67,7 +67,7 @@
                     onCheckedChange = null,
                     modifier = GlanceModifier.fillMaxSize()
                 )
-                Button(text = "test", onClick = actionLaunchActivity<Activity>())
+                Button(text = "test", onClick = actionStartActivity<Activity>())
                 Image(
                     ImageProvider(R.drawable.oval),
                     "description",
@@ -108,10 +108,10 @@
             Column {
                 CheckBox(
                     checked = true,
-onCheckedChange = null,
+                    onCheckedChange = null,
                     modifier = GlanceModifier.fillMaxSize()
                 )
-                Button(text = "test", onClick = actionLaunchActivity<Activity>())
+                Button(text = "test", onClick = actionStartActivity<Activity>())
             }
         }
         val root2 = runTestingComposition {
@@ -121,7 +121,7 @@
                     onCheckedChange = null,
                     modifier = GlanceModifier.wrapContentWidth().fillMaxHeight()
                 )
-                Button(text = "test", onClick = actionLaunchActivity<Activity>())
+                Button(text = "test", onClick = actionStartActivity<Activity>())
             }
         }
 
@@ -338,7 +338,7 @@
                     onCheckedChange = null,
                     modifier = GlanceModifier.fillMaxSize()
                 )
-                Button(text = "test", onClick = actionLaunchActivity<Activity>())
+                Button(text = "test", onClick = actionStartActivity<Activity>())
             }
         }
         val root2 = runTestingComposition {
@@ -348,7 +348,7 @@
                     onCheckedChange = null,
                     modifier = GlanceModifier.fillMaxSize()
                 )
-                Button(text = "testtesttest", onClick = actionLaunchActivity<Activity>())
+                Button(text = "testtesttest", onClick = actionStartActivity<Activity>())
             }
         }
 
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
index 0284863..acded2e 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchActivityIntentActionTest.kt
@@ -24,12 +24,12 @@
 import androidx.glance.action.clickable
 import androidx.glance.findModifier
 import androidx.test.core.app.ApplicationProvider
-import org.junit.Test
-import kotlin.test.assertIs
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Test
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
+import kotlin.test.assertIs
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
@@ -41,9 +41,9 @@
     fun testLaunchIntent() {
         val intentActionString = "test_action"
         val intent = Intent(context, TestActivity::class.java).setAction(intentActionString)
-        val modifiers = GlanceModifier.clickable(actionLaunchActivity(intent))
+        val modifiers = GlanceModifier.clickable(actionStartActivity(intent))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchActivityIntentAction>(modifier.action)
+        val action = assertIs<StartActivityIntentAction>(modifier.action)
         assertThat(action.intent).isEqualTo(intent)
         assertThat(action.intent.action).isEqualTo(intentActionString)
     }
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
similarity index 79%
rename from glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt
rename to glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
index 79215f4..061d7ef 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchBroadcastReceiverActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/SendBroadcastActionTest.kt
@@ -34,25 +34,25 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
-class LaunchBroadcastReceiverActionTest {
+class SendBroadcastActionTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
     @Test
     fun testLaunchClass() {
         val modifiers =
-            GlanceModifier.clickable(actionLaunchBroadcastReceiver<TestBroadcastReceiver>())
+            GlanceModifier.clickable(actionSendBroadcast<TestBroadcastReceiver>())
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchBroadcastReceiverClassAction>(modifier.action)
+        val action = assertIs<SendBroadcastClassAction>(modifier.action)
         assertThat(action.receiverClass).isEqualTo(TestBroadcastReceiver::class.java)
     }
 
     @Test
     fun testLaunchAction() {
         val intentActionString = "test_action"
-        val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(intentActionString))
+        val modifiers = GlanceModifier.clickable(actionSendBroadcast(intentActionString))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchBroadcastReceiverActionAction>(modifier.action)
+        val action = assertIs<SendBroadcastActionAction>(modifier.action)
         assertThat(action.action).isEqualTo(intentActionString)
         assertThat(action.componentName).isNull()
     }
@@ -65,13 +65,13 @@
             "androidx.glance.appwidget.action.TestBroadcastReceiver"
         )
         val modifiers = GlanceModifier.clickable(
-            actionLaunchBroadcastReceiver(
+            actionSendBroadcast(
                 intentActionString,
                 componentName
             )
         )
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchBroadcastReceiverActionAction>(modifier.action)
+        val action = assertIs<SendBroadcastActionAction>(modifier.action)
         assertThat(action.action).isEqualTo(intentActionString)
         assertThat(action.componentName).isEqualTo(componentName)
     }
@@ -81,9 +81,9 @@
         val intentActionString = "test_action"
         val intent =
             Intent(context, TestBroadcastReceiver::class.java).setAction(intentActionString)
-        val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(intent))
+        val modifiers = GlanceModifier.clickable(actionSendBroadcast(intent))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchBroadcastReceiverIntentAction>(modifier.action)
+        val action = assertIs<SendBroadcastIntentAction>(modifier.action)
         assertThat(action.intent).isEqualTo(intent)
         assertThat(action.intent.action).isEqualTo(intentActionString)
     }
@@ -94,9 +94,9 @@
             "androidx.glance.appwidget.action",
             "androidx.glance.appwidget.action.TestBroadcastReceiver"
         )
-        val modifiers = GlanceModifier.clickable(actionLaunchBroadcastReceiver(componentName))
+        val modifiers = GlanceModifier.clickable(actionSendBroadcast(componentName))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchBroadcastReceiverComponentAction>(modifier.action)
+        val action = assertIs<SendBroadcastComponentAction>(modifier.action)
         assertThat(action.componentName).isEqualTo(componentName)
     }
 }
diff --git a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
similarity index 83%
rename from glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt
rename to glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
index 5dbffd7..431e9f0 100644
--- a/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/LaunchServiceActionTest.kt
+++ b/glance/glance-appwidget/src/test/kotlin/androidx/glance/appwidget/action/StartServiceActionTest.kt
@@ -35,15 +35,15 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @RunWith(RobolectricTestRunner::class)
-class LaunchServiceActionTest {
+class StartServiceActionTest {
 
     private val context = ApplicationProvider.getApplicationContext<Context>()
 
     @Test
     fun testLaunchClass() {
-        val modifiers = GlanceModifier.clickable(actionLaunchService<TestService>())
+        val modifiers = GlanceModifier.clickable(actionStartService<TestService>())
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchServiceClassAction>(modifier.action)
+        val action = assertIs<StartServiceClassAction>(modifier.action)
         assertThat(action.serviceClass).isEqualTo(TestService::class.java)
         assertThat(action.isForegroundService).isEqualTo(false)
     }
@@ -51,12 +51,12 @@
     @Test
     fun testLaunchClassWithForeground() {
         val modifiers = GlanceModifier.clickable(
-            actionLaunchService<TestService>(
+            actionStartService<TestService>(
                 isForegroundService = true
             )
         )
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchServiceClassAction>(modifier.action)
+        val action = assertIs<StartServiceClassAction>(modifier.action)
         assertThat(action.serviceClass).isEqualTo(TestService::class.java)
         assertThat(action.isForegroundService).isEqualTo(true)
     }
@@ -65,9 +65,9 @@
     fun testLaunchIntent() {
         val intentActionString = "test_action"
         val intent = Intent(context, TestService::class.java).setAction(intentActionString)
-        val modifiers = GlanceModifier.clickable(actionLaunchService(intent))
+        val modifiers = GlanceModifier.clickable(actionStartService(intent))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchServiceIntentAction>(modifier.action)
+        val action = assertIs<StartServiceIntentAction>(modifier.action)
         assertThat(action.intent).isEqualTo(intent)
         assertThat(action.intent.action).isEqualTo(intentActionString)
         assertThat(action.isForegroundService).isEqualTo(false)
@@ -79,9 +79,9 @@
             "androidx.glance.appwidget.action",
             "androidx.glance.appwidget.action.TestService"
         )
-        val modifiers = GlanceModifier.clickable(actionLaunchService(componentName))
+        val modifiers = GlanceModifier.clickable(actionStartService(componentName))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchServiceComponentAction>(modifier.action)
+        val action = assertIs<StartServiceComponentAction>(modifier.action)
         assertThat(action.componentName).isEqualTo(componentName)
         assertThat(action.isForegroundService).isEqualTo(false)
     }
diff --git a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
index 28ff657..e336057 100644
--- a/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
+++ b/glance/glance-wear-tiles/src/androidMain/kotlin/androidx/glance/wear/tiles/WearCompositionTranslator.kt
@@ -33,9 +33,9 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.VisibilityModifier
 import androidx.glance.action.ActionModifier
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.LaunchActivityClassAction
-import androidx.glance.action.LaunchActivityComponentAction
+import androidx.glance.action.StartActivityAction
+import androidx.glance.action.StartActivityClassAction
+import androidx.glance.action.StartActivityComponentAction
 import androidx.glance.findModifier
 import androidx.glance.layout.Alignment
 import androidx.glance.layout.ContentScale
@@ -164,21 +164,21 @@
 }
 
 // TODO: handle parameters
-private fun LaunchActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
+private fun StartActivityAction.toProto(context: Context): ActionBuilders.LaunchAction =
     ActionBuilders.LaunchAction.Builder()
         .setAndroidActivity(
             ActionBuilders.AndroidActivity.Builder()
                 .setPackageName(
                     when (this) {
-                        is LaunchActivityComponentAction -> componentName.packageName
-                        is LaunchActivityClassAction -> context.packageName
+                        is StartActivityComponentAction -> componentName.packageName
+                        is StartActivityClassAction -> context.packageName
                         else -> error("Action type not defined in wear package: $this")
                     }
                 )
                 .setClassName(
                     when (this) {
-                        is LaunchActivityComponentAction -> componentName.className
-                        is LaunchActivityClassAction -> activityClass.name
+                        is StartActivityComponentAction -> componentName.className
+                        is StartActivityClassAction -> activityClass.name
                         else -> error("Action type not defined in wear package: $this")
                     }
                 )
@@ -190,7 +190,7 @@
     val builder = ModifiersBuilders.Clickable.Builder()
 
     when (val action = this.action) {
-        is LaunchActivityAction -> {
+        is StartActivityAction -> {
             builder.setOnClick(action.toProto(context))
         } else -> {
             Log.e(GlanceWearTileTag, "Unknown Action $this, skipped")
diff --git a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
index a31eab3..eec788c 100644
--- a/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
+++ b/glance/glance-wear-tiles/src/test/kotlin/androidx/glance/wear/tiles/WearCompositionTranslatorTest.kt
@@ -29,7 +29,7 @@
 import androidx.glance.GlanceModifier
 import androidx.glance.Image
 import androidx.glance.ImageProvider
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.actionStartActivity
 import androidx.glance.action.clickable
 import androidx.glance.background
 import androidx.glance.layout.Alignment
@@ -500,7 +500,7 @@
     fun canInflateLaunchAction() = fakeCoroutineScope.runBlockingTest {
         val content = runAndTranslate {
             Text(
-                modifier = GlanceModifier.clickable(actionLaunchActivity(TestActivity::class.java)),
+                modifier = GlanceModifier.clickable(actionStartActivity(TestActivity::class.java)),
                 text = "Hello World"
             )
         }.layout
@@ -533,7 +533,7 @@
             )
             Button(
                 "Hello World",
-                onClick = actionLaunchActivity(TestActivity::class.java),
+                onClick = actionStartActivity(TestActivity::class.java),
                 modifier = GlanceModifier.padding(1.dp),
                 style = style
             )
diff --git a/glance/glance/api/current.txt b/glance/glance/api/current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/current.txt
+++ b/glance/glance/api/current.txt
@@ -121,12 +121,6 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
-  public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
-  }
-
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -138,6 +132,12 @@
     method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
   }
 
+  public final class StartActivityActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/api/public_plus_experimental_current.txt b/glance/glance/api/public_plus_experimental_current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/public_plus_experimental_current.txt
+++ b/glance/glance/api/public_plus_experimental_current.txt
@@ -121,12 +121,6 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
-  public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
-  }
-
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -138,6 +132,12 @@
     method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
   }
 
+  public final class StartActivityActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/api/restricted_current.txt b/glance/glance/api/restricted_current.txt
index b4a5861..8893409 100644
--- a/glance/glance/api/restricted_current.txt
+++ b/glance/glance/api/restricted_current.txt
@@ -121,12 +121,6 @@
     method public static <T> androidx.glance.action.ActionParameters.Key<T> toParametersKey(androidx.datastore.preferences.core.Preferences.Key<T>);
   }
 
-  public final class LaunchActivityActionKt {
-    method public static androidx.glance.action.Action actionLaunchActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
-    method public static <T extends android.app.Activity> androidx.glance.action.Action actionLaunchActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
-    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionLaunchActivity(optional androidx.glance.action.ActionParameters parameters);
-  }
-
   public final class MutableActionParameters extends androidx.glance.action.ActionParameters {
     method public java.util.Map<androidx.glance.action.ActionParameters.Key<?>,java.lang.Object> asMap();
     method public void clear();
@@ -138,6 +132,12 @@
     method public operator <T> T? set(androidx.glance.action.ActionParameters.Key<T> key, T? value);
   }
 
+  public final class StartActivityActionKt {
+    method public static androidx.glance.action.Action actionStartActivity(android.content.ComponentName componentName, optional androidx.glance.action.ActionParameters parameters);
+    method public static <T extends android.app.Activity> androidx.glance.action.Action actionStartActivity(Class<T> activity, optional androidx.glance.action.ActionParameters parameters);
+    method public static inline <reified T extends android.app.Activity> androidx.glance.action.Action! actionStartActivity(optional androidx.glance.action.ActionParameters parameters);
+  }
+
 }
 
 package androidx.glance.layout {
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
index f363143..6348d86 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/Action.kt
@@ -22,7 +22,7 @@
 
 /**
  * An Action defines the actions a user can take. Implementations specify what operation will be
- * performed in response to the action, eg. [actionLaunchActivity] creates an Action that launches
+ * performed in response to the action, eg. [actionStartActivity] creates an Action that launches
  * the specified [Activity].
  */
 public interface Action
diff --git a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt b/glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
similarity index 79%
rename from glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
rename to glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
index ff1e90b..657da3bc 100644
--- a/glance/glance/src/androidMain/kotlin/androidx/glance/action/LaunchActivityAction.kt
+++ b/glance/glance/src/androidMain/kotlin/androidx/glance/action/StartActivityAction.kt
@@ -17,28 +17,28 @@
 package androidx.glance.action
 
 import android.app.Activity
-import androidx.annotation.RestrictTo
 import android.content.ComponentName
+import androidx.annotation.RestrictTo
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public interface LaunchActivityAction : Action {
+public interface StartActivityAction : Action {
     abstract val parameters: ActionParameters
 }
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class LaunchActivityComponentAction(
+public class StartActivityComponentAction(
     public val componentName: ComponentName,
     public override val parameters: ActionParameters
-) : LaunchActivityAction
+) : StartActivityAction
 
 /** @suppress */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class LaunchActivityClassAction(
+public class StartActivityClassAction(
     public val activityClass: Class<out Activity>,
     public override val parameters: ActionParameters
-) : LaunchActivityAction
+) : StartActivityAction
 
 /**
  * Creates an [Action] that launches the [Activity] specified by the given [ComponentName].
@@ -47,10 +47,10 @@
  * @param parameters the parameters associated with the action. Parameter values will be added to
  * the activity intent, keyed by the parameter key name string.
  */
-public fun actionLaunchActivity(
+public fun actionStartActivity(
     componentName: ComponentName,
     parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityComponentAction(componentName, parameters)
+): Action = StartActivityComponentAction(componentName, parameters)
 
 /**
  * Creates an [Action] that launches the specified [Activity] when triggered.
@@ -59,18 +59,19 @@
  * @param parameters the parameters associated with the action. Parameter values will be added to
  * the activity intent, keyed by the parameter key name string.
  */
-public fun <T : Activity> actionLaunchActivity(
+public fun <T : Activity> actionStartActivity(
     activity: Class<T>,
     parameters: ActionParameters = actionParametersOf()
-): Action = LaunchActivityClassAction(activity, parameters)
+): Action = StartActivityClassAction(activity, parameters)
 
-@Suppress("MissingNullability") /* Shouldn't need to specify @NonNull. b/199284086 */
+@Suppress("MissingNullability")
+/* Shouldn't need to specify @NonNull. b/199284086 */
 /**
  * Creates an [Action] that launches the specified [Activity] when triggered.
  *
  * @param parameters the parameters associated with the action. Parameter values will be added to
  * the activity intent, keyed by the parameter key name string.
  */
-public inline fun <reified T : Activity> actionLaunchActivity(
+public inline fun <reified T : Activity> actionStartActivity(
     parameters: ActionParameters = actionParametersOf()
-): Action = actionLaunchActivity(T::class.java, parameters)
+): Action = actionStartActivity(T::class.java, parameters)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
index 83909e2..e94dd92 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/ButtonTest.kt
@@ -19,9 +19,9 @@
 import androidx.compose.ui.unit.sp
 import androidx.glance.action.ActionModifier
 import androidx.glance.action.ActionParameters
-import androidx.glance.action.LaunchActivityAction
-import androidx.glance.action.actionLaunchActivity
+import androidx.glance.action.StartActivityAction
 import androidx.glance.action.actionParametersOf
+import androidx.glance.action.actionStartActivity
 import androidx.glance.layout.fillMaxSize
 import androidx.glance.layout.runTestingComposition
 import androidx.glance.text.TextStyle
@@ -51,7 +51,7 @@
 
         val root = runTestingComposition {
             Button(
-                text = "button", onClick = actionLaunchActivity<Activity>(
+                text = "button", onClick = actionStartActivity<Activity>(
                     actionParametersOf(stringKey to string, intKey to int)
                 ), enabled = true
             )
@@ -61,7 +61,7 @@
         val child = assertIs<EmittableButton>(root.children[0])
         assertThat(child.text).isEqualTo("button")
         val action =
-            assertIs<LaunchActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
+            assertIs<StartActivityAction>(child.modifier.findModifier<ActionModifier>()?.action)
         assertThat(child.enabled).isTrue()
         assertThat(action.parameters.asMap()).hasSize(2)
         assertThat(action.parameters[stringKey]).isEqualTo(string)
@@ -71,7 +71,7 @@
     @Test
     fun createDisabledButton() = fakeCoroutineScope.runBlockingTest {
         val root = runTestingComposition {
-            Button(text = "button", onClick = actionLaunchActivity<Activity>(), enabled = false)
+            Button(text = "button", onClick = actionStartActivity<Activity>(), enabled = false)
         }
 
         assertThat(root.children).hasSize(1)
@@ -86,7 +86,7 @@
         val root = runTestingComposition {
             Button(
                 text = "button",
-                onClick = actionLaunchActivity<Activity>(),
+                onClick = actionStartActivity<Activity>(),
                 modifier = GlanceModifier.fillMaxSize(),
                 maxLines = 3,
                 style = TextStyle(fontSize = 12.sp)
diff --git a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
index 9543516..78b6bfb 100644
--- a/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
+++ b/glance/glance/src/test/kotlin/androidx/glance/action/ActionTest.kt
@@ -45,19 +45,19 @@
     }
 
     @Test
-    fun testLaunchActivity() {
-        val modifiers = GlanceModifier.clickable(actionLaunchActivity(TestActivity::class.java))
+    fun testStartActivity() {
+        val modifiers = GlanceModifier.clickable(actionStartActivity(TestActivity::class.java))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        assertIs<LaunchActivityClassAction>(modifier.action)
+        assertIs<StartActivityClassAction>(modifier.action)
     }
 
     @Test
     fun testLaunchFromComponent() = fakeCoroutineScope.runBlockingTest {
         val c = ComponentName("androidx.glance.action", "androidx.glance.action.TestActivity")
 
-        val modifiers = GlanceModifier.clickable(actionLaunchActivity(c))
+        val modifiers = GlanceModifier.clickable(actionStartActivity(c))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchActivityComponentAction>(modifier.action)
+        val action = assertIs<StartActivityComponentAction>(modifier.action)
         val component = assertNotNull(action.componentName)
 
         assertThat(component).isEqualTo(c)
@@ -67,9 +67,9 @@
     fun testLaunchFromComponentWithContext() = fakeCoroutineScope.runBlockingTest {
         val c = ComponentName(context, "androidx.glance.action.TestActivity")
 
-        val modifiers = GlanceModifier.clickable(actionLaunchActivity(c))
+        val modifiers = GlanceModifier.clickable(actionStartActivity(c))
         val modifier = checkNotNull(modifiers.findModifier<ActionModifier>())
-        val action = assertIs<LaunchActivityComponentAction>(modifier.action)
+        val action = assertIs<StartActivityComponentAction>(modifier.action)
         val component = assertNotNull(action.componentName)
 
         assertThat(component).isEqualTo(c)
diff --git a/lifecycle/lifecycle-viewmodel-compose/build.gradle b/lifecycle/lifecycle-viewmodel-compose/build.gradle
index 1285491..2932a3a 100644
--- a/lifecycle/lifecycle-viewmodel-compose/build.gradle
+++ b/lifecycle/lifecycle-viewmodel-compose/build.gradle
@@ -29,7 +29,7 @@
 dependencies {
     kotlinPlugin(projectOrArtifact(":compose:compiler:compiler"))
 
-    api "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0"
+    api projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx")
     api("androidx.compose.runtime:runtime:1.0.1")
     api "androidx.compose.ui:ui:1.0.1"
 
@@ -43,6 +43,13 @@
     androidTestImplementation(libs.truth)
     androidTestImplementation "androidx.fragment:fragment:1.3.0"
     androidTestImplementation "androidx.appcompat:appcompat:1.3.0"
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
+    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate")
     androidTestImplementation projectOrArtifact(":activity:activity-compose")
 
     samples(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples"))
@@ -59,12 +66,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
 }
\ No newline at end of file
diff --git a/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java b/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
index 41be4d5..14b0d3c 100644
--- a/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
+++ b/media2/media2-session/src/main/java/androidx/media2/session/MediaLibraryServiceLegacyStub.java
@@ -144,8 +144,7 @@
                 }
                 LibraryParams params = MediaUtils.convertToLibraryParams(
                         mLibrarySessionImpl.getContext(), option);
-                mLibrarySessionImpl.getCallback().onSubscribe(mLibrarySessionImpl.getInstance(),
-                        controller, id, params);
+                mLibrarySessionImpl.onSubscribeOnExecutor(controller, id, params);
             }
         });
     }
@@ -168,8 +167,7 @@
                     }
                     return;
                 }
-                mLibrarySessionImpl.getCallback().onUnsubscribe(mLibrarySessionImpl.getInstance(),
-                                controller, id);
+                mLibrarySessionImpl.onUnsubscribeOnExecutor(controller, id);
             }
         });
     }
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index a864da0..82a3f77 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -31,10 +31,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.savedstate:savedstate:1.0.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-savedstate"))
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
 
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index eadea17..ba33048e 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -30,12 +30,18 @@
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.compose.foundation:foundation-layout:1.0.1")
-    api("androidx.activity:activity-compose:1.3.1")
+    api(projectOrArtifact(":activity:activity-compose"))
     api("androidx.compose.animation:animation:1.0.1")
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0")
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-compose"))
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    implementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-dynamic-features-fragment/build.gradle b/navigation/navigation-dynamic-features-fragment/build.gradle
index a7a7afa..48c5b1f 100644
--- a/navigation/navigation-dynamic-features-fragment/build.gradle
+++ b/navigation/navigation-dynamic-features-fragment/build.gradle
@@ -73,12 +73,10 @@
 
 // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
 // targeted to 1.6
-if (project.hasProperty("androidx.useMaxDepVersions")){
-    tasks.withType(KotlinCompile).configureEach {
-        kotlinOptions {
-            freeCompilerArgs += [
-                    "-Xjvm-default=enable",
-            ]
-        }
+tasks.withType(KotlinCompile).configureEach {
+    kotlinOptions {
+        freeCompilerArgs += [
+                "-Xjvm-default=enable",
+        ]
     }
 }
\ No newline at end of file
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 8108470..8d49434 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -24,7 +24,7 @@
 }
 
 dependencies {
-    api("androidx.fragment:fragment-ktx:1.4.0")
+    api(projectOrArtifact(":fragment:fragment-ktx"))
     api(project(":navigation:navigation-runtime"))
     api(projectOrArtifact(":slidingpanelayout:slidingpanelayout"))
     api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 4d5e19f..f52b59c 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -26,13 +26,13 @@
 
 dependencies {
     api(project(":navigation:navigation-common"))
-    api("androidx.activity:activity-ktx:1.2.3")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.3.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1")
+    api(projectOrArtifact(":activity:activity-ktx"))
+    api(projectOrArtifact(":lifecycle:lifecycle-runtime-ktx"))
+    api(projectOrArtifact(":lifecycle:lifecycle-viewmodel-ktx"))
     api("androidx.annotation:annotation-experimental:1.1.0")
 
     api(libs.kotlinStdlib)
-    androidTestImplementation("androidx.lifecycle:lifecycle-runtime-testing:2.3.1")
+    androidTestImplementation(projectOrArtifact(":lifecycle:lifecycle-runtime-testing"))
     androidTestImplementation(project(":internal-testutils-navigation"))
     androidTestImplementation(project(":internal-testutils-runtime"))
     androidTestImplementation(libs.testExtJunit)
@@ -69,8 +69,6 @@
         freeCompilerArgs += ["-Xopt-in=kotlin.RequiresOptIn"]
         // needed only while https://youtrack.jetbrains.com/issue/KT-47000 isn't resolved which is
         // targeted to 1.6
-        if (project.hasProperty("androidx.useMaxDepVersions")) {
-            freeCompilerArgs += ["-Xjvm-default=enable"]
-        }
+        freeCompilerArgs += ["-Xjvm-default=enable"]
     }
 }
\ No newline at end of file
diff --git a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
index 90a699a..4bf9938 100644
--- a/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
+++ b/playground-common/playground-plugin/src/main/kotlin/androidx/playground/PlaygroundExtension.kt
@@ -168,6 +168,8 @@
         if (name == ":compose:test-utils") return true
         if (name == ":compose:lint:common-test") return true
         if (name == ":test:screenshot:screenshot") return true
+        if (name == ":lifecycle:lifecycle-common") return true
+        if (name == ":lifecycle:lifecycle-common-java8") return true
         return false
     }
 }
\ No newline at end of file
diff --git a/playground-common/playground.properties b/playground-common/playground.properties
index 1c4f9a7..ec2d6c4 100644
--- a/playground-common/playground.properties
+++ b/playground-common/playground.properties
@@ -25,7 +25,7 @@
 kotlin.code.style=official
 # Disable docs
 androidx.enableDocumentation=false
-androidx.playground.snapshotBuildId=7967568
+androidx.playground.snapshotBuildId=7990267
 androidx.playground.metalavaBuildId=7856580
 androidx.playground.dokkaBuildId=7472101
 androidx.studio.type=playground
diff --git a/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java b/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
index df0b2b0..dcddf49 100644
--- a/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
+++ b/preference/preference/src/main/java/androidx/preference/PreferenceInflater.java
@@ -49,7 +49,7 @@
     private PreferenceManager mPreferenceManager;
     private String[] mDefaultPackages;
 
-    PreferenceInflater(@NonNull Context context, PreferenceManager preferenceManager) {
+    public PreferenceInflater(@NonNull Context context, PreferenceManager preferenceManager) {
         mContext = context;
         init(preferenceManager);
     }
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java b/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
index 3054ad4..c2dc700 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/IdentityCredentialStore.java
@@ -211,6 +211,10 @@
     /**
      * Creates a new credential.
      *
+     * <p>Note that the credential is not persisted until calling
+     * {@link WritableIdentityCredential#personalize(PersonalizationData)} on the returned
+     * {@link WritableIdentityCredential} object.
+     *
      * @param credentialName The name used to identify the credential.
      * @param docType        The document type for the credential.
      * @return A @{link WritableIdentityCredential} that can be used to create a new credential.
diff --git a/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java b/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
index 2b6aab0..7b2e394 100644
--- a/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
+++ b/security/security-identity-credential/src/main/java/androidx/security/identity/WritableIdentityCredential.java
@@ -25,8 +25,11 @@
 /**
  * Class used to personalize a new identity credential.
  *
- * <p>Credentials cannot be updated or modified after creation; any changes require deletion and
- * re-creation.
+ * <p>Note that the credential is not persisted until calling
+ * {@link #personalize(PersonalizationData)}.
+ *
+ * <p>Once persisted, the PII in a credential can be updated using
+ * {@link IdentityCredential#update(PersonalizationData)}.
  *
  * Use {@link IdentityCredentialStore#createCredential(String, String)} to create a new credential.
  */
@@ -62,6 +65,9 @@
      * authority doesn't care about the nature of the security hardware. If called, however, this
      * method must be called before {@link #personalize(PersonalizationData)}.
      *
+     * <p>Note that the credential is not persisted until calling
+     * {@link #personalize(PersonalizationData)}.
+     *
      * @param challenge is a non-empty byte array whose contents should be unique, fresh and
      *                  provided by the issuing authority. The value provided is embedded in the
      *                  attestation extension and enables the issuing authority to verify that the
@@ -74,6 +80,8 @@
     /**
      * Stores all of the data in the credential, with the specified access control profiles.
      *
+     * <p>The credential is persisted only after this method returns successfully.
+     *
      * <p>This method returns a COSE_Sign1 data structure signed by the CredentialKey with payload
      * set to {@code ProofOfProvisioning} as defined below.
      *
diff --git a/settings.gradle b/settings.gradle
index 9577478..afb72af 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -536,7 +536,7 @@
 includeProject(":lifecycle:lifecycle-compiler", "lifecycle/lifecycle-compiler", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-extensions", "lifecycle/lifecycle-extensions", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-livedata", "lifecycle/lifecycle-livedata", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-livedata-core", "lifecycle/lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-livedata-core", "lifecycle/lifecycle-livedata-core", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-livedata-core-ktx", "lifecycle/lifecycle-livedata-core-ktx", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-livedata-core-ktx-lint", "lifecycle/lifecycle-livedata-core-ktx-lint", [BuildType.MAIN, BuildType.FLAN])
 includeProject(":lifecycle:lifecycle-livedata-core-truth", "lifecycle/lifecycle-livedata-core-truth", [BuildType.MAIN, BuildType.FLAN])
@@ -549,12 +549,12 @@
 includeProject(":lifecycle:lifecycle-runtime-ktx-lint", "lifecycle/lifecycle-runtime-ktx-lint", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-runtime-testing", "lifecycle/lifecycle-runtime-testing", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-service", "lifecycle/lifecycle-service", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-viewmodel", "lifecycle/lifecycle-viewmodel", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-compose", "lifecycle/lifecycle-viewmodel-compose", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-compose:lifecycle-viewmodel-compose-samples", "lifecycle/lifecycle-viewmodel-compose/samples", [BuildType.COMPOSE])
 includeProject(":lifecycle:lifecycle-viewmodel-compose:integration-tests:lifecycle-viewmodel-demos", "lifecycle/lifecycle-viewmodel-compose/integration-tests/lifecycle-viewmodel-demos", [BuildType.COMPOSE])
-includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN])
-includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR])
+includeProject(":lifecycle:lifecycle-viewmodel-ktx", "lifecycle/lifecycle-viewmodel-ktx", [BuildType.MAIN, BuildType.FLAN, BuildType.COMPOSE])
+includeProject(":lifecycle:lifecycle-viewmodel-savedstate", "lifecycle/lifecycle-viewmodel-savedstate", [BuildType.MAIN, BuildType.FLAN, BuildType.WEAR, BuildType.COMPOSE])
 includeProject(":lint-checks", "lint-checks")
 includeProject(":lint-checks:integration-tests", "lint-checks/integration-tests")
 includeProject(":lint-demos:lint-demo-appcompat", "lint-demos/lint-demo-appcompat", [BuildType.MAIN])
diff --git a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
index 42a98af..d287c1e 100644
--- a/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
+++ b/testutils/testutils-macrobenchmark/src/main/java/androidx/testutils/MacrobenchUtils.kt
@@ -19,6 +19,7 @@
 import android.content.Intent
 import android.os.Build
 import androidx.annotation.RequiresApi
+import androidx.benchmark.macro.BaselineProfileMode
 import androidx.benchmark.macro.CompilationMode
 import androidx.benchmark.macro.StartupMode
 import androidx.benchmark.macro.StartupTimingLegacyMetric
@@ -34,12 +35,16 @@
  */
 val BASIC_COMPILATION_MODES = if (Build.VERSION.SDK_INT < 24) {
     // other modes aren't supported
-    listOf(CompilationMode.None)
+    listOf(CompilationMode.Full())
 } else {
     listOf(
-        CompilationMode.None,
+        CompilationMode.None(),
         CompilationMode.Interpreted,
-        CompilationMode.SpeedProfile()
+        CompilationMode.Partial(
+            baselineProfileMode = BaselineProfileMode.Disable,
+            warmupIterations = 3
+        ),
+        CompilationMode.Full()
     )
 }
 
@@ -49,8 +54,9 @@
  * Baseline profiles are only supported from Nougat (API 24),
  * currently through Android 11 (API 30)
  */
-val COMPILATION_MODES = if (Build.VERSION.SDK_INT in 24..30) {
-    listOf(CompilationMode.BaselineProfile)
+@Suppress("ConvertTwoComparisonsToRangeCheck") // lint doesn't understand range checks
+val COMPILATION_MODES = if (Build.VERSION.SDK_INT >= 24 && Build.VERSION.SDK_INT <= 30) {
+    listOf(CompilationMode.Partial())
 } else {
     emptyList()
 } + BASIC_COMPILATION_MODES
diff --git a/tracing/tracing-ktx/api/1.1.0-beta03.txt b/tracing/tracing-ktx/api/1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class TraceKt {
+    method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+    method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+  }
+
+}
+
diff --git a/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt b/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/public_plus_experimental_1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class TraceKt {
+    method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+    method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+  }
+
+}
+
diff --git a/tracing/tracing-ktx/api/res-1.1.0-beta03.txt b/tracing/tracing-ktx/api/res-1.1.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing-ktx/api/res-1.1.0-beta03.txt
diff --git a/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt b/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt
new file mode 100644
index 0000000..e77d248
--- /dev/null
+++ b/tracing/tracing-ktx/api/restricted_1.1.0-beta03.txt
@@ -0,0 +1,10 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class TraceKt {
+    method public static inline <T> T! trace(String label, kotlin.jvm.functions.Function0<? extends T> block);
+    method public static suspend inline <T> Object? traceAsync(String methodName, int cookie, kotlin.jvm.functions.Function1<? super kotlin.coroutines.Continuation<? super T>,?> block, kotlin.coroutines.Continuation<? super T> p);
+  }
+
+}
+
diff --git a/tracing/tracing/api/1.1.0-beta03.txt b/tracing/tracing/api/1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class Trace {
+    method public static void beginAsyncSection(String, int);
+    method public static void beginSection(String);
+    method public static void endAsyncSection(String, int);
+    method public static void endSection();
+    method public static void forceEnableAppTracing();
+    method public static boolean isEnabled();
+    method public static void setCounter(String, int);
+  }
+
+}
+
diff --git a/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt b/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/public_plus_experimental_1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class Trace {
+    method public static void beginAsyncSection(String, int);
+    method public static void beginSection(String);
+    method public static void endAsyncSection(String, int);
+    method public static void endSection();
+    method public static void forceEnableAppTracing();
+    method public static boolean isEnabled();
+    method public static void setCounter(String, int);
+  }
+
+}
+
diff --git a/tracing/tracing/api/res-1.1.0-beta03.txt b/tracing/tracing/api/res-1.1.0-beta03.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tracing/tracing/api/res-1.1.0-beta03.txt
diff --git a/tracing/tracing/api/restricted_1.1.0-beta03.txt b/tracing/tracing/api/restricted_1.1.0-beta03.txt
new file mode 100644
index 0000000..c883da2
--- /dev/null
+++ b/tracing/tracing/api/restricted_1.1.0-beta03.txt
@@ -0,0 +1,15 @@
+// Signature format: 4.0
+package androidx.tracing {
+
+  public final class Trace {
+    method public static void beginAsyncSection(String, int);
+    method public static void beginSection(String);
+    method public static void endAsyncSection(String, int);
+    method public static void endSection();
+    method public static void forceEnableAppTracing();
+    method public static boolean isEnabled();
+    method public static void setCounter(String, int);
+  }
+
+}
+
diff --git a/wear/compose/integration-tests/navigation/build.gradle b/wear/compose/integration-tests/navigation/build.gradle
index 9c93c22..b2f8d46 100644
--- a/wear/compose/integration-tests/navigation/build.gradle
+++ b/wear/compose/integration-tests/navigation/build.gradle
@@ -58,4 +58,10 @@
     implementation(project(":wear:compose:compose-foundation-samples"))
     implementation(project(":wear:compose:compose-material-samples"))
     implementation(project(':wear:compose:compose-navigation'))
+    // old version of common-java8 conflicts with newer version, because both have
+    // DefaultLifecycleEventObserver.
+    // Outside of androidx this is resolved via constraint added to lifecycle-common,
+    // but it doesn't work in androidx.
+    // See aosp/1804059
+    androidTestImplementation projectOrArtifact(":lifecycle:lifecycle-common-java8")
 }
\ No newline at end of file
diff --git a/wear/watchface/watchface-client/api/current.txt b/wear/watchface/watchface-client/api/current.txt
index 210b4a0..9afd83d 100644
--- a/wear/watchface/watchface-client/api/current.txt
+++ b/wear/watchface/watchface-client/api/current.txt
@@ -196,8 +196,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
   }
 
diff --git a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
index a4e489e..b947824 100644
--- a/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-client/api/public_plus_experimental_current.txt
@@ -199,8 +199,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
   }
 
diff --git a/wear/watchface/watchface-client/api/restricted_current.txt b/wear/watchface/watchface-client/api/restricted_current.txt
index 8160792..bf20972 100644
--- a/wear/watchface/watchface-client/api/restricted_current.txt
+++ b/wear/watchface/watchface-client/api/restricted_current.txt
@@ -196,8 +196,8 @@
   }
 
   public interface WatchFaceMetadataClient extends java.lang.AutoCloseable {
-    method public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap();
-    method public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema();
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public java.util.Map<java.lang.Integer,androidx.wear.watchface.client.ComplicationSlotMetadata> getComplicationSlotMetadataMap() throws android.os.RemoteException;
+    method @kotlin.jvm.Throws(exceptionClasses=RemoteException::class) public androidx.wear.watchface.style.UserStyleSchema getUserStyleSchema() throws android.os.RemoteException;
     field public static final androidx.wear.watchface.client.WatchFaceMetadataClient.Companion Companion;
   }
 
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
index 5bf74e2..7252e3f 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/WatchFaceMetadataClient.kt
@@ -163,12 +163,14 @@
     /**
      * Returns the watch face's [UserStyleSchema].
      */
+    @Throws(RemoteException::class)
     public fun getUserStyleSchema(): UserStyleSchema
 
     /**
      * Returns a map of [androidx.wear.watchface.ComplicationSlot] ID to [ComplicationSlotMetadata]
      * for each slot in the watch face's [androidx.wear.watchface.ComplicationSlotsManager].
      */
+    @Throws(RemoteException::class)
     public fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata>
 }
 
@@ -246,70 +248,61 @@
     }
 
     override fun getUserStyleSchema(): UserStyleSchema {
-        requireNotClosed()
-        try {
-            return if (service.apiVersion >= 3) {
-                UserStyleSchema(service.getUserStyleSchema(GetUserStyleSchemaParams(watchFaceName)))
-            } else {
-                headlessClient.userStyleSchema
-            }
-        } catch (e: RemoteException) {
-            throw RuntimeException(e)
+        return if (service.apiVersion >= 3) {
+            UserStyleSchema(service.getUserStyleSchema(GetUserStyleSchemaParams(watchFaceName)))
+        } else {
+            headlessClient.userStyleSchema
         }
     }
 
     override fun getComplicationSlotMetadataMap(): Map<Int, ComplicationSlotMetadata> {
         requireNotClosed()
-        try {
-            return if (service.apiVersion >= 3) {
-                val wireFormat = service.getComplicationSlotMetadata(
-                    GetComplicationSlotMetadataParams(watchFaceName)
-                )
-                wireFormat.associateBy(
-                    { it.id },
-                    {
-                        val perSlotBounds = HashMap<ComplicationType, RectF>()
-                        for (i in it.complicationBoundsType.indices) {
-                            perSlotBounds[
-                                ComplicationType.fromWireType(it.complicationBoundsType[i])
-                            ] = it.complicationBounds[i]
-                        }
-                        ComplicationSlotMetadata(
-                            ComplicationSlotBounds(perSlotBounds),
-                            it.boundsType,
-                            it.supportedTypes.map { ComplicationType.fromWireType(it) },
-                            DefaultComplicationDataSourcePolicy(
-                                it.defaultDataSourcesToTry ?: emptyList(),
-                                it.fallbackSystemDataSource,
-                                ComplicationType.fromWireType(
-                                    it.primaryDataSourceDefaultType
-                                ),
-                                ComplicationType.fromWireType(
-                                    it.secondaryDataSourceDefaultType
-                                ),
-                                ComplicationType.fromWireType(it.defaultDataSourceType)
-                            ),
-                            it.isInitiallyEnabled,
-                            it.isFixedComplicationDataSource,
-                            it.complicationConfigExtras
-                        )
+        return if (service.apiVersion >= 3) {
+            val wireFormat = service.getComplicationSlotMetadata(
+                GetComplicationSlotMetadataParams(watchFaceName)
+            )
+            wireFormat.associateBy(
+                { it.id },
+                {
+                    val perSlotBounds = HashMap<ComplicationType, RectF>()
+                    for (i in it.complicationBoundsType.indices) {
+                        perSlotBounds[
+                            ComplicationType.fromWireType(it.complicationBoundsType[i])
+                        ] = it.complicationBounds[i]
                     }
-                )
-            } else {
-                headlessClient.complicationSlotsState.mapValues {
                     ComplicationSlotMetadata(
-                        null,
-                        it.value.boundsType,
-                        it.value.supportedTypes,
-                        it.value.defaultDataSourcePolicy,
-                        it.value.isInitiallyEnabled,
-                        it.value.fixedComplicationDataSource,
-                        it.value.complicationConfigExtras
+                        ComplicationSlotBounds(perSlotBounds),
+                        it.boundsType,
+                        it.supportedTypes.map { ComplicationType.fromWireType(it) },
+                        DefaultComplicationDataSourcePolicy(
+                            it.defaultDataSourcesToTry ?: emptyList(),
+                            it.fallbackSystemDataSource,
+                            ComplicationType.fromWireType(
+                                it.primaryDataSourceDefaultType
+                            ),
+                            ComplicationType.fromWireType(
+                                it.secondaryDataSourceDefaultType
+                            ),
+                            ComplicationType.fromWireType(it.defaultDataSourceType)
+                        ),
+                        it.isInitiallyEnabled,
+                        it.isFixedComplicationDataSource,
+                        it.complicationConfigExtras
                     )
                 }
+            )
+        } else {
+            headlessClient.complicationSlotsState.mapValues {
+                ComplicationSlotMetadata(
+                    null,
+                    it.value.boundsType,
+                    it.value.supportedTypes,
+                    it.value.defaultDataSourcePolicy,
+                    it.value.isInitiallyEnabled,
+                    it.value.fixedComplicationDataSource,
+                    it.value.complicationConfigExtras
+                )
             }
-        } catch (e: RemoteException) {
-            throw RuntimeException(e)
         }
     }
 
diff --git a/work/work-runtime/build.gradle b/work/work-runtime/build.gradle
index a6fe6da..6db8f53 100644
--- a/work/work-runtime/build.gradle
+++ b/work/work-runtime/build.gradle
@@ -23,6 +23,7 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("kotlin-android")
+    id("com.google.devtools.ksp")
 }
 
 android {
@@ -51,7 +52,7 @@
 
 dependencies {
     implementation("androidx.core:core:1.6.0")
-    annotationProcessor("androidx.room:room-compiler:2.4.0-rc01")
+    ksp("androidx.room:room-compiler:2.4.0-rc01")
     implementation("androidx.room:room-runtime:2.4.0-rc01")
     androidTestImplementation("androidx.room:room-testing:2.4.0-rc01")
     implementation("androidx.sqlite:sqlite:2.1.0")
@@ -80,6 +81,11 @@
 
 packageInspector(project, project(":work:work-inspection"))
 
+// KSP does not support argument per flavor so we set it to global instead.
+ksp {
+    arg("room.schemaLocation","$projectDir/src/schemas".toString())
+}
+
 androidx {
     name = "Android WorkManager Runtime"
     publish = Publish.SNAPSHOT_AND_RELEASE
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
index a593404..39c636d 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabaseMigrationTest.java
@@ -19,24 +19,18 @@
 import static android.content.Context.MODE_PRIVATE;
 import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL;
 
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_11_12;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_7_8;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_8_9;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_1;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_12;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_4;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_5;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_6;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_7;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_8;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_9;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_1;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_10;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_11;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_12;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_2;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_3;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_4;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_5;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_6;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_7;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_8;
+import static androidx.work.impl.WorkDatabaseVersions.VERSION_9;
 import static androidx.work.impl.utils.IdGenerator.NEXT_ALARM_MANAGER_ID_KEY;
 import static androidx.work.impl.utils.IdGenerator.NEXT_JOB_SCHEDULER_ID_KEY;
 import static androidx.work.impl.utils.IdGenerator.PREFERENCE_FILE_KEY;
@@ -62,9 +56,17 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 import androidx.test.filters.MediumTest;
 import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.work.impl.Migration_11_12;
+import androidx.work.impl.Migration_1_2;
+import androidx.work.impl.Migration_3_4;
+import androidx.work.impl.Migration_4_5;
+import androidx.work.impl.Migration_6_7;
+import androidx.work.impl.Migration_7_8;
+import androidx.work.impl.Migration_8_9;
+import androidx.work.impl.RescheduleMigration;
 import androidx.work.impl.WorkDatabase;
-import androidx.work.impl.WorkDatabaseMigrations;
 import androidx.work.impl.WorkManagerImpl;
+import androidx.work.impl.WorkMigration9To10;
 import androidx.work.impl.model.WorkSpec;
 import androidx.work.impl.model.WorkTypeConverters;
 import androidx.work.worker.TestWorker;
@@ -76,6 +78,7 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.UUID;
 
 @RunWith(AndroidJUnit4.class)
@@ -117,7 +120,8 @@
     @Rule
     public MigrationTestHelper mMigrationTestHelper = new MigrationTestHelper(
             InstrumentationRegistry.getInstrumentation(),
-            WorkDatabase.class.getCanonicalName(),
+            WorkDatabase.class,
+            new ArrayList<>(),
             new FrameworkSQLiteOpenHelperFactory());
 
     @Before
@@ -165,7 +169,7 @@
                 TEST_DATABASE,
                 VERSION_2,
                 VALIDATE_DROPPED_TABLES,
-                WorkDatabaseMigrations.MIGRATION_1_2);
+                Migration_1_2.INSTANCE);
 
         Cursor tagCursor = database.query("SELECT * FROM worktag");
         assertThat(tagCursor.getCount(), is(prepopulatedWorkSpecIds.length));
@@ -209,8 +213,8 @@
     public void testMigrationVersion2To3() throws IOException {
         SupportSQLiteDatabase database =
                 mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_2);
-        WorkDatabaseMigrations.RescheduleMigration migration2To3 =
-                new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_2, VERSION_3);
+        RescheduleMigration migration2To3 =
+                new RescheduleMigration(mContext, VERSION_2, VERSION_3);
 
         database = mMigrationTestHelper.runMigrationsAndValidate(
                 TEST_DATABASE,
@@ -246,7 +250,7 @@
                 TEST_DATABASE,
                 VERSION_4,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_3_4);
+                Migration_3_4.INSTANCE);
 
         Cursor cursor = database.query("SELECT * from workspec");
         assertThat(cursor.getCount(), is(2));
@@ -277,7 +281,7 @@
                 TEST_DATABASE,
                 VERSION_5,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_4_5);
+                Migration_4_5.INSTANCE);
         assertThat(checkExists(database, TABLE_WORKSPEC), is(true));
         assertThat(
                 checkColumnExists(database, TABLE_WORKSPEC, TRIGGER_CONTENT_UPDATE_DELAY),
@@ -293,8 +297,7 @@
     public void testMigrationVersion5To6() throws IOException {
         SupportSQLiteDatabase database =
                 mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_5);
-        WorkDatabaseMigrations.RescheduleMigration migration5To6 =
-                new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_5, VERSION_6);
+        RescheduleMigration migration5To6 = new RescheduleMigration(mContext, VERSION_5, VERSION_6);
 
         database = mMigrationTestHelper.runMigrationsAndValidate(
                 TEST_DATABASE,
@@ -317,7 +320,7 @@
                 TEST_DATABASE,
                 VERSION_7,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_6_7);
+                Migration_6_7.INSTANCE);
         assertThat(checkExists(database, TABLE_WORKPROGRESS), is(true));
         database.close();
     }
@@ -331,7 +334,7 @@
                 TEST_DATABASE,
                 VERSION_8,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_7_8);
+                Migration_7_8.INSTANCE);
 
         assertThat(checkIndexExists(database, INDEX_PERIOD_START_TIME, TABLE_WORKSPEC), is(true));
         database.close();
@@ -346,7 +349,7 @@
                 TEST_DATABASE,
                 VERSION_9,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_8_9);
+                Migration_8_9.INSTANCE);
 
         assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_RUN_IN_FOREGROUND),
                 is(true));
@@ -378,7 +381,7 @@
                 TEST_DATABASE,
                 VERSION_10,
                 VALIDATE_DROPPED_TABLES,
-                new WorkDatabaseMigrations.WorkMigration9To10(mContext));
+                new WorkMigration9To10(mContext));
 
         assertThat(checkExists(database, TABLE_PREFERENCE), is(true));
         String query = "SELECT * FROM `Preference` where `key`=@key";
@@ -411,8 +414,8 @@
     public void testMigrationVersion10To11() throws IOException {
         SupportSQLiteDatabase database =
                 mMigrationTestHelper.createDatabase(TEST_DATABASE, VERSION_10);
-        WorkDatabaseMigrations.RescheduleMigration migration10To11 =
-                new WorkDatabaseMigrations.RescheduleMigration(mContext, VERSION_10, VERSION_11);
+        RescheduleMigration migration10To11 =
+                new RescheduleMigration(mContext, VERSION_10, VERSION_11);
         database = mMigrationTestHelper.runMigrationsAndValidate(
                 TEST_DATABASE,
                 VERSION_11,
@@ -448,7 +451,7 @@
                 TEST_DATABASE,
                 VERSION_12,
                 VALIDATE_DROPPED_TABLES,
-                MIGRATION_11_12);
+                Migration_11_12.INSTANCE);
 
         assertThat(checkColumnExists(database, TABLE_WORKSPEC, COLUMN_OUT_OF_QUOTA_POLICY),
                 is(true));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
index cd70bd7..b02375a 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/WorkDatabasePathHelperTest.kt
@@ -24,9 +24,9 @@
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
-import androidx.work.impl.WorkDatabasePathHelper
 import androidx.work.impl.WorkDatabase
-import androidx.work.impl.WorkDatabaseMigrations.VERSION_9
+import androidx.work.impl.WorkDatabasePathHelper
+import androidx.work.impl.WorkDatabaseVersions.VERSION_9
 import org.hamcrest.CoreMatchers.`is`
 import org.hamcrest.MatcherAssert.assertThat
 import org.junit.Before
@@ -37,13 +37,12 @@
 
 @RunWith(AndroidJUnit4::class)
 @LargeTest
-@Suppress("DEPRECATION")
-// TODO: (b/189268580) Update this test to use the new constructors in MigrationTestHelper.
 class WorkDatabasePathHelperTest {
     @get:Rule
     val migrationTestHelper = MigrationTestHelper(
         InstrumentationRegistry.getInstrumentation(),
-        WorkDatabase::class.java.canonicalName,
+        WorkDatabase::class.java,
+        emptyList(),
         FrameworkSQLiteOpenHelperFactory()
     )
 
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
index 5f3abfc..69fab95 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerImplTest.java
@@ -1614,7 +1614,8 @@
     public void testGenerateCleanupCallback_deletesOldFinishedWork() {
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setInitialState(SUCCEEDED)
-                .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+                .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+                        TimeUnit.MILLISECONDS)
                 .build();
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setPeriodStartTime(Long.MAX_VALUE, TimeUnit.MILLISECONDS)
@@ -1625,7 +1626,7 @@
 
         SupportSQLiteOpenHelper openHelper = mDatabase.getOpenHelper();
         SupportSQLiteDatabase db = openHelper.getWritableDatabase();
-        WorkDatabase.generateCleanupCallback().onOpen(db);
+        CleanupCallback.INSTANCE.onOpen(db);
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getWorkSpec(work1.getStringId()), is(nullValue()));
@@ -1637,15 +1638,18 @@
     public void testGenerateCleanupCallback_doesNotDeleteOldFinishedWorkWithActiveDependents() {
         OneTimeWorkRequest work0 = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setInitialState(SUCCEEDED)
-                .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+                .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+                        TimeUnit.MILLISECONDS)
                 .build();
         OneTimeWorkRequest work1 = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setInitialState(SUCCEEDED)
-                .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+                .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+                        TimeUnit.MILLISECONDS)
                 .build();
         OneTimeWorkRequest work2 = new OneTimeWorkRequest.Builder(TestWorker.class)
                 .setInitialState(ENQUEUED)
-                .setPeriodStartTime(WorkDatabase.getPruneDate() - 1L, TimeUnit.MILLISECONDS)
+                .setPeriodStartTime(CleanupCallback.INSTANCE.getPruneDate() - 1L,
+                        TimeUnit.MILLISECONDS)
                 .build();
 
         insertWorkSpecAndTags(work0);
@@ -1658,7 +1662,7 @@
 
         SupportSQLiteOpenHelper openHelper = mDatabase.getOpenHelper();
         SupportSQLiteDatabase db = openHelper.getWritableDatabase();
-        WorkDatabase.generateCleanupCallback().onOpen(db);
+        CleanupCallback.INSTANCE.onOpen(db);
 
         WorkSpecDao workSpecDao = mDatabase.workSpecDao();
         assertThat(workSpecDao.getWorkSpec(work0.getStringId()), is(nullValue()));
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
index e1cdbc1..fc84c9b 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/WorkManagerInitializationTest.kt
@@ -16,53 +16,49 @@
 
 package androidx.work.impl
 
-import android.content.Context
+import android.content.ContextWrapper
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SdkSuppress
 import androidx.test.filters.SmallTest
 import androidx.work.Configuration
-import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import androidx.work.impl.utils.SynchronousExecutor
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
 import org.junit.Assert.assertNotNull
-import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mockito.`when`
-import org.mockito.Mockito.mock
-import java.util.concurrent.Executor
 
 @RunWith(AndroidJUnit4::class)
 class WorkManagerInitializationTest {
-    private lateinit var mContext: Context
-    private lateinit var mConfiguration: Configuration
-    private lateinit var mExecutor: Executor
-    private lateinit var mTaskExecutor: TaskExecutor
-
-    @Before
-    fun setUp() {
-        mContext = mock(Context::class.java)
-        `when`(mContext.applicationContext).thenReturn(mContext)
-        mExecutor = mock(Executor::class.java)
-        mConfiguration = Configuration.Builder()
-            .setExecutor(mExecutor)
-            .setTaskExecutor(mExecutor)
-            .build()
-        mTaskExecutor = mock(TaskExecutor::class.java)
-    }
+    private val executor = SynchronousExecutor()
+    private val configuration = Configuration.Builder()
+        .setExecutor(executor)
+        .setTaskExecutor(executor)
+        .build()
+    private val taskExecutor = InstantWorkTaskExecutor()
 
     @Test(expected = IllegalStateException::class)
     @SmallTest
     @SdkSuppress(minSdkVersion = 24)
     fun directBootTest() {
-        `when`(mContext.isDeviceProtectedStorage).thenReturn(true)
-        WorkManagerImpl(mContext, mConfiguration, mTaskExecutor, true)
+        val context = DeviceProtectedStoreContext(true)
+        WorkManagerImpl(context, configuration, taskExecutor, true)
     }
 
     @Test
     @SmallTest
     @SdkSuppress(minSdkVersion = 24)
     fun credentialBackedStorageTest() {
-        `when`(mContext.isDeviceProtectedStorage).thenReturn(false)
-        val workManager = WorkManagerImpl(mContext, mConfiguration, mTaskExecutor, true)
+        val context = DeviceProtectedStoreContext(false)
+        val workManager = WorkManagerImpl(context, configuration, taskExecutor, true)
         assertNotNull(workManager)
     }
 }
+
+private class DeviceProtectedStoreContext(
+    val deviceProtectedStorage: Boolean
+) : ContextWrapper(ApplicationProvider.getApplicationContext()) {
+    override fun isDeviceProtectedStorage() = deviceProtectedStorage
+
+    override fun getApplicationContext() = this
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java
deleted file mode 100644
index ed651cd..0000000
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.nullValue;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.hamcrest.Matchers.empty;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-import androidx.work.impl.constraints.controllers.ConstraintController;
-import androidx.work.impl.model.WorkSpec;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-public class WorkConstraintsTrackerTest {
-    private static final List<String> TEST_WORKSPEC_IDS = new ArrayList<>();
-    static {
-        TEST_WORKSPEC_IDS.add("A");
-        TEST_WORKSPEC_IDS.add("B");
-        TEST_WORKSPEC_IDS.add("C");
-    }
-
-    private WorkConstraintsCallback mCallback = new WorkConstraintsCallback() {
-        @Override
-        public void onAllConstraintsMet(@NonNull List<String> workSpecIds) {
-            mUnconstrainedWorkSpecIds = workSpecIds;
-        }
-
-        @Override
-        public void onAllConstraintsNotMet(@NonNull List<String> workSpecIds) {
-            mConstrainedWorkSpecIds = workSpecIds;
-        }
-    };
-
-    private ConstraintController mMockController = mock(ConstraintController.class);
-    private List<String> mUnconstrainedWorkSpecIds;
-    private List<String> mConstrainedWorkSpecIds;
-    private WorkConstraintsTrackerImpl mWorkConstraintsTracker;
-
-    @Before
-    public void setUp() {
-        ConstraintController[] controllers = new ConstraintController[] {mMockController};
-        mWorkConstraintsTracker = new WorkConstraintsTrackerImpl(mCallback, controllers);
-    }
-
-    @SuppressWarnings("unchecked")
-    @Test
-    public void testReplace() {
-        List<WorkSpec> emptyList = Collections.emptyList();
-
-        ArgumentCaptor<ConstraintController.OnConstraintUpdatedCallback> captor =
-                ArgumentCaptor.forClass(ConstraintController.OnConstraintUpdatedCallback.class);
-
-        mWorkConstraintsTracker.replace(emptyList);
-        verify(mMockController).replace(emptyList);
-        verify(mMockController, times(2)).setCallback(captor.capture());
-        assertThat(captor.getAllValues().size(), is(2));
-        assertThat(captor.getAllValues().get(0), is(nullValue()));
-        assertThat(captor.getAllValues().get(1),
-                is((ConstraintController.OnConstraintUpdatedCallback) mWorkConstraintsTracker));
-    }
-
-    @Test
-    public void testReset() {
-        mWorkConstraintsTracker.reset();
-        verify(mMockController).reset();
-    }
-
-    @Test
-    public void testOnConstraintMet_controllerInvoked() {
-        mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
-        for (String id : TEST_WORKSPEC_IDS) {
-            verify(mMockController).isWorkSpecConstrained(id);
-        }
-    }
-
-    @Test
-    public void testOnConstraintMet_allConstraintsMet() {
-        when(mMockController.isWorkSpecConstrained(any(String.class))).thenReturn(false);
-        mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
-        assertThat(mUnconstrainedWorkSpecIds, is(TEST_WORKSPEC_IDS));
-    }
-
-    @Test
-    public void testOnConstraintMet_allConstraintsMet_subList() {
-        when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(0))).thenReturn(true);
-        when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(1))).thenReturn(false);
-        when(mMockController.isWorkSpecConstrained(TEST_WORKSPEC_IDS.get(2))).thenReturn(false);
-        mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
-        assertThat(mUnconstrainedWorkSpecIds,
-                containsInAnyOrder(TEST_WORKSPEC_IDS.get(1), TEST_WORKSPEC_IDS.get(2)));
-    }
-
-    @Test
-    public void testOnConstraintMet_allConstraintsNotMet() {
-        when(mMockController.isWorkSpecConstrained(any(String.class))).thenReturn(true);
-        mWorkConstraintsTracker.onConstraintMet(TEST_WORKSPEC_IDS);
-        assertThat(mUnconstrainedWorkSpecIds, is(empty()));
-    }
-
-    @Test
-    public void testOnConstraintNotMet() {
-        mWorkConstraintsTracker.onConstraintNotMet(TEST_WORKSPEC_IDS);
-        assertThat(mConstrainedWorkSpecIds, is(TEST_WORKSPEC_IDS));
-    }
-}
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
new file mode 100644
index 0000000..540925e
--- /dev/null
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/WorkConstraintsTrackerTest.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import androidx.work.impl.constraints.controllers.ConstraintController
+import androidx.work.impl.constraints.trackers.ConstraintTracker
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.utils.taskexecutor.InstantWorkTaskExecutor
+import androidx.work.impl.utils.taskexecutor.TaskExecutor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WorkConstraintsTrackerTest {
+
+    private val capturingCallback = CapturingWorkConstraintsCallback()
+
+    @Test
+    fun testReplace() {
+        val tracker = TestConstraintTracker(true)
+        val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+        workConstraintsTracker.replace(TEST_WORKSPECS.subList(0, 2))
+        val (unconstrained1, _) = capturingCallback.consumeCurrent()
+        assertThat(unconstrained1).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
+        workConstraintsTracker.replace(TEST_WORKSPECS.subList(1, 3))
+        val (unconstrained2, _) = capturingCallback.consumeCurrent()
+        assertThat(unconstrained2).containsExactly(TEST_WORKSPEC_IDS[1], TEST_WORKSPEC_IDS[2])
+    }
+
+    @Test
+    fun testReset() {
+        val tracker = TestConstraintTracker(true)
+        val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+        workConstraintsTracker.replace(TEST_WORKSPECS)
+        assertThat(tracker.isTracking).isTrue()
+        workConstraintsTracker.reset()
+        assertThat(tracker.isTracking).isFalse()
+    }
+
+    @Test
+    fun testOnConstraintMet_allConstraintsMet() {
+        val tracker = TestConstraintTracker()
+        val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker)
+        workConstraintsTracker.replace(TEST_WORKSPECS)
+        val (_, constrained) = capturingCallback.consumeCurrent()
+        assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
+        tracker.setState(true)
+        val (unconstrained, _) = capturingCallback.consumeCurrent()
+        assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
+    }
+
+    @Test
+    fun testOnConstraintMet_allConstraintsMet_subList() {
+        val tracker1 = TestConstraintTracker()
+        val tracker2 = TestConstraintTracker()
+        val controller1 = TestConstraintController(tracker1, TEST_WORKSPEC_IDS.subList(0, 2))
+        val controller2 = TestConstraintController(tracker2, TEST_WORKSPEC_IDS.subList(2, 3))
+        val workConstraintsTracker = WorkConstraintsTrackerImpl(
+            capturingCallback,
+            arrayOf(controller1, controller2)
+        )
+        workConstraintsTracker.replace(TEST_WORKSPECS)
+        capturingCallback.consumeCurrent()
+        tracker1.setState(true)
+        val (unconstrained, _) = capturingCallback.consumeCurrent()
+        assertThat(unconstrained).containsExactly(TEST_WORKSPEC_IDS[0], TEST_WORKSPEC_IDS[1])
+    }
+
+    @Test
+    fun testOnConstraintMet_allConstraintsNotMet() {
+        val tracker1 = TestConstraintTracker()
+        val tracker2 = TestConstraintTracker()
+        val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
+        workConstraintsTracker.replace(TEST_WORKSPECS)
+        capturingCallback.consumeCurrent()
+        tracker1.setState(true)
+        val (unconstrained, _) = capturingCallback.consumeCurrent()
+        // only one constraint is resolved, so unconstrained is empty list
+        assertThat(unconstrained).isEqualTo(emptyList<String>())
+    }
+
+    @Test
+    fun testOnConstraintNotMet() {
+        val tracker1 = TestConstraintTracker(true)
+        val tracker2 = TestConstraintTracker(true)
+        val workConstraintsTracker = WorkConstraintsTracker(capturingCallback, tracker1, tracker2)
+        workConstraintsTracker.replace(TEST_WORKSPECS)
+        val (unconstrained, _) = capturingCallback.consumeCurrent()
+        assertThat(unconstrained).isEqualTo(TEST_WORKSPEC_IDS)
+        tracker1.setState(false)
+        val (_, constrained) = capturingCallback.consumeCurrent()
+        assertThat(constrained).isEqualTo(TEST_WORKSPEC_IDS)
+    }
+}
+
+private val TEST_WORKSPECS = listOf(
+    WorkSpec("A", "Worker1"),
+    WorkSpec("B", "Worker2"),
+    WorkSpec("C", "Worker3"),
+)
+private val TEST_WORKSPEC_IDS = TEST_WORKSPECS.map { it.id }
+
+private fun WorkConstraintsTracker(
+    callback: WorkConstraintsCallback,
+    vararg trackers: ConstraintTracker<Boolean>
+): WorkConstraintsTrackerImpl {
+    val controllers = trackers.map { TestConstraintController(it) }
+    return WorkConstraintsTrackerImpl(callback, controllers.toTypedArray())
+}
+
+private class TestConstraintTracker(
+    val initialState: Boolean = false,
+    context: Context = ApplicationProvider.getApplicationContext(),
+    taskExecutor: TaskExecutor = InstantWorkTaskExecutor(),
+) : ConstraintTracker<Boolean>(context, taskExecutor) {
+    var isTracking = false
+    override fun getInitialState() = initialState
+
+    override fun startTracking() {
+        isTracking = true
+    }
+
+    override fun stopTracking() {
+        isTracking = false
+    }
+}
+
+private class TestConstraintController(
+    tracker: ConstraintTracker<Boolean>,
+    private val constrainedIds: List<String> = TEST_WORKSPEC_IDS
+) : ConstraintController<Boolean>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec) = workSpec.id in constrainedIds
+    override fun isConstrained(value: Boolean) = !value
+}
+
+private class CapturingWorkConstraintsCallback(
+    var unconstrainedWorkSpecIds: List<String>? = null,
+    var constrainedWorkSpecIds: List<String>? = null,
+) : WorkConstraintsCallback {
+    override fun onAllConstraintsMet(workSpecIds: List<String>) {
+        unconstrainedWorkSpecIds = workSpecIds
+    }
+
+    override fun onAllConstraintsNotMet(workSpecIds: List<String>) {
+        constrainedWorkSpecIds = workSpecIds
+    }
+
+    fun consumeCurrent(): Pair<List<String>?, List<String>?> {
+        val result = unconstrainedWorkSpecIds to constrainedWorkSpecIds
+        unconstrainedWorkSpecIds = null
+        constrainedWorkSpecIds = null
+        return result
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
index 82367fe..ffaa451 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/constraints/controllers/ConstraintControllerTest.java
@@ -250,12 +250,12 @@
         }
 
         @Override
-        boolean hasConstraint(@NonNull WorkSpec workSpec) {
+        public boolean hasConstraint(@NonNull WorkSpec workSpec) {
             return workSpec.constraints.requiresDeviceIdle();
         }
 
         @Override
-        boolean isConstrained(@NonNull Boolean isDeviceIdle) {
+        public boolean isConstrained(@NonNull Boolean isDeviceIdle) {
             return !isDeviceIdle;
         }
 
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java
deleted file mode 100644
index 5073e66..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.java
+++ /dev/null
@@ -1,233 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl;
-
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_3_4;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_4_5;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_6_7;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_7_8;
-import static androidx.work.impl.WorkDatabaseMigrations.MIGRATION_8_9;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_10;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_11;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_2;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_3;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_5;
-import static androidx.work.impl.WorkDatabaseMigrations.VERSION_6;
-import static androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES;
-
-import android.content.Context;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.room.Database;
-import androidx.room.Room;
-import androidx.room.RoomDatabase;
-import androidx.room.TypeConverters;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory;
-import androidx.work.Data;
-import androidx.work.impl.model.Dependency;
-import androidx.work.impl.model.DependencyDao;
-import androidx.work.impl.model.Preference;
-import androidx.work.impl.model.PreferenceDao;
-import androidx.work.impl.model.RawWorkInfoDao;
-import androidx.work.impl.model.SystemIdInfo;
-import androidx.work.impl.model.SystemIdInfoDao;
-import androidx.work.impl.model.WorkName;
-import androidx.work.impl.model.WorkNameDao;
-import androidx.work.impl.model.WorkProgress;
-import androidx.work.impl.model.WorkProgressDao;
-import androidx.work.impl.model.WorkSpec;
-import androidx.work.impl.model.WorkSpecDao;
-import androidx.work.impl.model.WorkTag;
-import androidx.work.impl.model.WorkTagDao;
-import androidx.work.impl.model.WorkTypeConverters;
-
-import java.util.concurrent.Executor;
-import java.util.concurrent.TimeUnit;
-
-/**
- * A Room database for keeping track of work states.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@Database(entities = {
-        Dependency.class,
-        WorkSpec.class,
-        WorkTag.class,
-        SystemIdInfo.class,
-        WorkName.class,
-        WorkProgress.class,
-        Preference.class},
-        version = 12)
-@TypeConverters(value = {Data.class, WorkTypeConverters.class})
-public abstract class WorkDatabase extends RoomDatabase {
-    // Delete rows in the workspec table that...
-    private static final String PRUNE_SQL_FORMAT_PREFIX = "DELETE FROM workspec WHERE "
-            // are completed...
-            + "state IN " + COMPLETED_STATES + " AND "
-            // and the minimum retention time has expired...
-            + "(period_start_time + minimum_retention_duration) < ";
-    // and all dependents are completed.
-    private static final String PRUNE_SQL_FORMAT_SUFFIX = " AND "
-            + "(SELECT COUNT(*)=0 FROM dependency WHERE "
-            + "    prerequisite_id=id AND "
-            + "    work_spec_id NOT IN "
-            + "        (SELECT id FROM workspec WHERE state IN " + COMPLETED_STATES + "))";
-
-    private static final long PRUNE_THRESHOLD_MILLIS = TimeUnit.DAYS.toMillis(1);
-
-    /**
-     * Creates an instance of the WorkDatabase.
-     *
-     * @param context         A context (this method will use the application context from it)
-     * @param queryExecutor   An {@link Executor} that will be used to execute all async Room
-     *                        queries.
-     * @param useTestDatabase {@code true} to generate an in-memory database that allows main thread
-     *                        access
-     * @return The created WorkDatabase
-     */
-    @NonNull
-    public static WorkDatabase create(
-            @NonNull final Context context,
-            @NonNull Executor queryExecutor,
-            boolean useTestDatabase) {
-        RoomDatabase.Builder<WorkDatabase> builder;
-        if (useTestDatabase) {
-            builder = Room.inMemoryDatabaseBuilder(context, WorkDatabase.class)
-                    .allowMainThreadQueries();
-        } else {
-            String name = WorkDatabasePathHelper.getWorkDatabaseName();
-            builder = Room.databaseBuilder(context, WorkDatabase.class, name);
-            builder.openHelperFactory(new SupportSQLiteOpenHelper.Factory() {
-                @NonNull
-                @Override
-                public SupportSQLiteOpenHelper create(
-                        @NonNull SupportSQLiteOpenHelper.Configuration configuration) {
-                    SupportSQLiteOpenHelper.Configuration.Builder configBuilder =
-                            SupportSQLiteOpenHelper.Configuration.builder(context);
-                    configBuilder.name(configuration.name)
-                            .callback(configuration.callback)
-                            .noBackupDirectory(true);
-                    FrameworkSQLiteOpenHelperFactory factory =
-                            new FrameworkSQLiteOpenHelperFactory();
-                    return factory.create(configBuilder.build());
-                }
-            });
-        }
-
-        return builder.setQueryExecutor(queryExecutor)
-                .addCallback(generateCleanupCallback())
-                .addMigrations(WorkDatabaseMigrations.MIGRATION_1_2)
-                .addMigrations(
-                        new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_2,
-                                VERSION_3))
-                .addMigrations(MIGRATION_3_4)
-                .addMigrations(MIGRATION_4_5)
-                .addMigrations(
-                        new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_5,
-                                VERSION_6))
-                .addMigrations(MIGRATION_6_7)
-                .addMigrations(MIGRATION_7_8)
-                .addMigrations(MIGRATION_8_9)
-                .addMigrations(new WorkDatabaseMigrations.WorkMigration9To10(context))
-                .addMigrations(
-                        new WorkDatabaseMigrations.RescheduleMigration(context, VERSION_10,
-                                VERSION_11))
-                .addMigrations(WorkDatabaseMigrations.MIGRATION_11_12)
-                .fallbackToDestructiveMigration()
-                .build();
-    }
-
-    static Callback generateCleanupCallback() {
-        return new Callback() {
-            @Override
-            public void onOpen(@NonNull SupportSQLiteDatabase db) {
-                super.onOpen(db);
-                db.beginTransaction();
-                try {
-                    // Prune everything that is completed, has an expired retention time, and has no
-                    // active dependents:
-                    db.execSQL(getPruneSQL());
-                    db.setTransactionSuccessful();
-                } finally {
-                    db.endTransaction();
-                }
-            }
-        };
-    }
-
-    @SuppressWarnings("WeakerAccess") /* synthetic access */
-    @NonNull
-    static String getPruneSQL() {
-        return PRUNE_SQL_FORMAT_PREFIX + getPruneDate() + PRUNE_SQL_FORMAT_SUFFIX;
-    }
-
-    static long getPruneDate() {
-        return System.currentTimeMillis() - PRUNE_THRESHOLD_MILLIS;
-    }
-
-    /**
-     * @return The Data Access Object for {@link WorkSpec}s.
-     */
-    @NonNull
-    public abstract WorkSpecDao workSpecDao();
-
-    /**
-     * @return The Data Access Object for {@link Dependency}s.
-     */
-    @NonNull
-    public abstract DependencyDao dependencyDao();
-
-    /**
-     * @return The Data Access Object for {@link WorkTag}s.
-     */
-    @NonNull
-    public abstract WorkTagDao workTagDao();
-
-    /**
-     * @return The Data Access Object for {@link SystemIdInfo}s.
-     */
-    @NonNull
-    public abstract SystemIdInfoDao systemIdInfoDao();
-
-    /**
-     * @return The Data Access Object for {@link WorkName}s.
-     */
-    @NonNull
-    public abstract WorkNameDao workNameDao();
-
-    /**
-     * @return The Data Access Object for {@link WorkProgress}.
-     */
-    @NonNull
-    public abstract WorkProgressDao workProgressDao();
-
-    /**
-     * @return The Data Access Object for {@link Preference}.
-     */
-    @NonNull
-    public abstract PreferenceDao preferenceDao();
-
-    /**
-     * @return The Data Access Object which can be used to execute raw queries.
-     */
-    @NonNull
-    public abstract RawWorkInfoDao rawWorkInfoDao();
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
new file mode 100644
index 0000000..e2e3c89
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabase.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl
+
+import android.content.Context
+import androidx.annotation.RestrictTo
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import androidx.room.TypeConverters
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
+import androidx.work.Data
+import androidx.work.impl.WorkDatabaseVersions.VERSION_10
+import androidx.work.impl.WorkDatabaseVersions.VERSION_11
+import androidx.work.impl.WorkDatabaseVersions.VERSION_2
+import androidx.work.impl.WorkDatabaseVersions.VERSION_3
+import androidx.work.impl.WorkDatabaseVersions.VERSION_5
+import androidx.work.impl.WorkDatabaseVersions.VERSION_6
+import androidx.work.impl.model.Dependency
+import androidx.work.impl.model.DependencyDao
+import androidx.work.impl.model.Preference
+import androidx.work.impl.model.PreferenceDao
+import androidx.work.impl.model.RawWorkInfoDao
+import androidx.work.impl.model.SystemIdInfo
+import androidx.work.impl.model.SystemIdInfoDao
+import androidx.work.impl.model.WorkName
+import androidx.work.impl.model.WorkNameDao
+import androidx.work.impl.model.WorkProgress
+import androidx.work.impl.model.WorkProgressDao
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.model.WorkSpecDao
+import androidx.work.impl.model.WorkTag
+import androidx.work.impl.model.WorkTagDao
+import androidx.work.impl.model.WorkTypeConverters
+import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES
+import java.util.concurrent.Executor
+import java.util.concurrent.TimeUnit
+
+/**
+ * A Room database for keeping track of work states.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Database(
+    entities = [Dependency::class, WorkSpec::class, WorkTag::class, SystemIdInfo::class,
+        WorkName::class, WorkProgress::class, Preference::class],
+    version = 12
+)
+@TypeConverters(value = [Data::class, WorkTypeConverters::class])
+abstract class WorkDatabase : RoomDatabase() {
+    /**
+     * @return The Data Access Object for [WorkSpec]s.
+     */
+    abstract fun workSpecDao(): WorkSpecDao
+
+    /**
+     * @return The Data Access Object for [Dependency]s.
+     */
+    abstract fun dependencyDao(): DependencyDao
+
+    /**
+     * @return The Data Access Object for [WorkTag]s.
+     */
+    abstract fun workTagDao(): WorkTagDao
+
+    /**
+     * @return The Data Access Object for [SystemIdInfo]s.
+     */
+    abstract fun systemIdInfoDao(): SystemIdInfoDao
+
+    /**
+     * @return The Data Access Object for [WorkName]s.
+     */
+    abstract fun workNameDao(): WorkNameDao
+
+    /**
+     * @return The Data Access Object for [WorkProgress].
+     */
+    abstract fun workProgressDao(): WorkProgressDao
+
+    /**
+     * @return The Data Access Object for [Preference].
+     */
+    abstract fun preferenceDao(): PreferenceDao
+
+    /**
+     * @return The Data Access Object which can be used to execute raw queries.
+     */
+    abstract fun rawWorkInfoDao(): RawWorkInfoDao
+
+    companion object {
+        /**
+         * Creates an instance of the WorkDatabase.
+         *
+         * @param context         A context (this method will use the application context from it)
+         * @param queryExecutor   An [Executor] that will be used to execute all async Room
+         * queries.
+         * @param useTestDatabase `true` to generate an in-memory database that allows main thread
+         * access
+         * @return The created WorkDatabase
+         */
+        @JvmStatic
+        fun create(
+            context: Context,
+            queryExecutor: Executor,
+            useTestDatabase: Boolean
+        ): WorkDatabase {
+            val builder = if (useTestDatabase) {
+                Room.inMemoryDatabaseBuilder(context, WorkDatabase::class.java)
+                    .allowMainThreadQueries()
+            } else {
+                Room.databaseBuilder(context, WorkDatabase::class.java, WORK_DATABASE_NAME)
+                    .openHelperFactory { configuration ->
+                        val configBuilder = SupportSQLiteOpenHelper.Configuration.builder(context)
+                        configBuilder.name(configuration.name)
+                            .callback(configuration.callback)
+                            .noBackupDirectory(true)
+                        FrameworkSQLiteOpenHelperFactory().create(configBuilder.build())
+                    }
+            }
+            return builder.setQueryExecutor(queryExecutor)
+                .addCallback(CleanupCallback)
+                .addMigrations(Migration_1_2)
+                .addMigrations(RescheduleMigration(context, VERSION_2, VERSION_3))
+                .addMigrations(Migration_3_4)
+                .addMigrations(Migration_4_5)
+                .addMigrations(RescheduleMigration(context, VERSION_5, VERSION_6))
+                .addMigrations(Migration_6_7)
+                .addMigrations(Migration_7_8)
+                .addMigrations(Migration_8_9)
+                .addMigrations(WorkMigration9To10(context))
+                .addMigrations(RescheduleMigration(context, VERSION_10, VERSION_11))
+                .addMigrations(Migration_11_12)
+                .fallbackToDestructiveMigration()
+                .build()
+        }
+    }
+}
+
+// Delete rows in the workspec table that...
+private const val PRUNE_SQL_FORMAT_PREFIX =
+    // are completed...
+    "DELETE FROM workspec WHERE state IN $COMPLETED_STATES AND " +
+        // and the minimum retention time has expired...
+        "(period_start_time + minimum_retention_duration) < "
+
+// and all dependents are completed.
+private const val PRUNE_SQL_FORMAT_SUFFIX = " AND " +
+    "(SELECT COUNT(*)=0 FROM dependency WHERE " +
+    "    prerequisite_id=id AND " +
+    "    work_spec_id NOT IN " +
+    "        (SELECT id FROM workspec WHERE state IN $COMPLETED_STATES))"
+private val PRUNE_THRESHOLD_MILLIS = TimeUnit.DAYS.toMillis(1)
+
+internal object CleanupCallback : RoomDatabase.Callback() {
+    private val pruneSQL: String
+        get() = "$PRUNE_SQL_FORMAT_PREFIX$pruneDate$PRUNE_SQL_FORMAT_SUFFIX"
+
+    val pruneDate: Long
+        get() = System.currentTimeMillis() - PRUNE_THRESHOLD_MILLIS
+
+    override fun onOpen(db: SupportSQLiteDatabase) {
+        super.onOpen(db)
+        db.beginTransaction()
+        try {
+            // Prune everything that is completed, has an expired retention time, and has no
+            // active dependents:
+            db.execSQL(pruneSQL)
+            db.setTransactionSuccessful()
+        } finally {
+            db.endTransaction()
+        }
+    }
+}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
deleted file mode 100644
index 7a1d045..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * 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.
- */
-
-package androidx.work.impl;
-
-import static android.content.Context.MODE_PRIVATE;
-
-import static androidx.work.impl.utils.PreferenceUtils.KEY_RESCHEDULE_NEEDED;
-import static androidx.work.impl.utils.PreferenceUtils.PREFERENCES_FILE_NAME;
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RestrictTo;
-import androidx.room.migration.Migration;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.work.impl.model.Preference;
-import androidx.work.impl.model.WorkSpec;
-import androidx.work.impl.model.WorkTypeConverters;
-import androidx.work.impl.utils.IdGenerator;
-import androidx.work.impl.utils.PreferenceUtils;
-
-/**
- * Migration helpers for {@link androidx.work.impl.WorkDatabase}.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkDatabaseMigrations {
-
-    private WorkDatabaseMigrations() {
-        // does nothing
-    }
-
-    // Known WorkDatabase versions
-    public static final int VERSION_1 = 1;
-    public static final int VERSION_2 = 2;
-    public static final int VERSION_3 = 3;
-    public static final int VERSION_4 = 4;
-    public static final int VERSION_5 = 5;
-    public static final int VERSION_6 = 6;
-    public static final int VERSION_7 = 7;
-    public static final int VERSION_8 = 8;
-    public static final int VERSION_9 = 9;
-    public static final int VERSION_10 = 10;
-    public static final int VERSION_11 = 11;
-    public static final int VERSION_12 = 12;
-
-    private static final String CREATE_SYSTEM_ID_INFO =
-            "CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`"
-                    + " INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)"
-                    + " REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )";
-
-    private static final String MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO =
-            "INSERT INTO SystemIdInfo(work_spec_id, system_id) "
-                    + "SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo";
-
-    private static final String PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT =
-            "UPDATE workspec SET schedule_requested_at=0"
-                    + " WHERE state NOT IN " + WorkTypeConverters.StateIds.COMPLETED_STATES
-                    + " AND schedule_requested_at=" + WorkSpec.SCHEDULE_NOT_REQUESTED_YET
-                    + " AND interval_duration<>0";
-
-    private static final String REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo";
-
-    private static final String WORKSPEC_ADD_TRIGGER_UPDATE_DELAY =
-            "ALTER TABLE workspec ADD COLUMN `trigger_content_update_delay` INTEGER NOT NULL "
-                    + "DEFAULT -1";
-
-    private static final String WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY =
-            "ALTER TABLE workspec ADD COLUMN `trigger_max_content_delay` INTEGER NOT NULL DEFAULT"
-                    + " -1";
-
-    private static final String CREATE_WORK_PROGRESS =
-            "CREATE TABLE IF NOT EXISTS `WorkProgress` (`work_spec_id` TEXT NOT NULL, `progress`"
-                    + " BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`) "
-                    + "REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )";
-
-    private static final String CREATE_INDEX_PERIOD_START_TIME =
-            "CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `workspec` "
-                    + "(`period_start_time`)";
-
-    private static final String CREATE_RUN_IN_FOREGROUND =
-            "ALTER TABLE workspec ADD COLUMN `run_in_foreground` INTEGER NOT NULL DEFAULT 0";
-
-    public static final String INSERT_PREFERENCE =
-            "INSERT OR REPLACE INTO `Preference`"
-                    + " (`key`, `long_value`) VALUES"
-                    + " (@key, @long_value)";
-
-    private static final String CREATE_PREFERENCE =
-            "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
-                    + "PRIMARY KEY(`key`))";
-
-    private static final String CREATE_OUT_OF_QUOTA_POLICY =
-            "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0";
-
-    /**
-     * Removes the {@code alarmInfo} table and substitutes it for a more general
-     * {@code SystemIdInfo} table.
-     * Adds implicit work tags for all work (a tag with the worker class name).
-     */
-    @NonNull
-    public static Migration MIGRATION_1_2 = new Migration(VERSION_1, VERSION_2) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_SYSTEM_ID_INFO);
-            database.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO);
-            database.execSQL(REMOVE_ALARM_INFO);
-            database.execSQL("INSERT OR IGNORE INTO worktag(tag, work_spec_id) "
-                    + "SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec");
-        }
-    };
-
-    /**
-     * A {@link WorkDatabase} migration that reschedules all eligible Workers.
-     */
-    public static class RescheduleMigration extends Migration {
-        final Context mContext;
-
-        public RescheduleMigration(@NonNull Context context, int startVersion, int endVersion) {
-            super(startVersion, endVersion);
-            mContext = context;
-        }
-
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            if (endVersion >= VERSION_10) {
-                database.execSQL(INSERT_PREFERENCE, new Object[]{KEY_RESCHEDULE_NEEDED, 1});
-            } else {
-                SharedPreferences preferences =
-                        mContext.getSharedPreferences(PREFERENCES_FILE_NAME, MODE_PRIVATE);
-
-                // Mutate the shared preferences directly, and eventually they will get
-                // migrated to the data store post v10.
-                preferences.edit()
-                        .putBoolean(KEY_RESCHEDULE_NEEDED, true)
-                        .apply();
-            }
-        }
-    }
-
-    /**
-     * Marks {@code SCHEDULE_REQUESTED_AT} to something other than
-     * {@code SCHEDULE_NOT_REQUESTED_AT}.
-     */
-    @NonNull
-    public static Migration MIGRATION_3_4 = new Migration(VERSION_3, VERSION_4) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
-                database.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT);
-            }
-        }
-    };
-
-    /**
-     * Adds the {@code ContentUri} delays to the WorkSpec table.
-     */
-    @NonNull
-    public static Migration MIGRATION_4_5 = new Migration(VERSION_4, VERSION_5) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(WORKSPEC_ADD_TRIGGER_UPDATE_DELAY);
-            database.execSQL(WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY);
-        }
-    };
-
-    /**
-     * Adds {@link androidx.work.impl.model.WorkProgress}.
-     */
-    @NonNull
-    public static Migration MIGRATION_6_7 = new Migration(VERSION_6, VERSION_7) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_WORK_PROGRESS);
-        }
-    };
-
-    /**
-     * Adds an index on period_start_time in {@link WorkSpec}.
-     */
-    @NonNull
-    public static Migration MIGRATION_7_8 = new Migration(VERSION_7, VERSION_8) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_INDEX_PERIOD_START_TIME);
-        }
-    };
-
-    /**
-     * Adds a notification_provider to the {@link WorkSpec}.
-     */
-    @NonNull
-    public static Migration MIGRATION_8_9 = new Migration(VERSION_8, VERSION_9) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_RUN_IN_FOREGROUND);
-        }
-    };
-
-    /**
-     * Adds the {@link Preference} table.
-     */
-    public static class WorkMigration9To10 extends Migration {
-        final Context mContext;
-
-        public WorkMigration9To10(@NonNull Context context) {
-            super(VERSION_9, VERSION_10);
-            mContext = context;
-        }
-
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_PREFERENCE);
-            PreferenceUtils.migrateLegacyPreferences(mContext, database);
-            IdGenerator.migrateLegacyIdGenerator(mContext, database);
-        }
-    }
-
-    /**
-     * Adds a notification_provider to the {@link WorkSpec}.
-     */
-    @NonNull
-    public static Migration MIGRATION_11_12 = new Migration(VERSION_11, VERSION_12) {
-        @Override
-        public void migrate(@NonNull SupportSQLiteDatabase database) {
-            database.execSQL(CREATE_OUT_OF_QUOTA_POLICY);
-        }
-    };
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
new file mode 100644
index 0000000..17fbb18
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabaseMigrations.kt
@@ -0,0 +1,208 @@
+/*
+ * 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.
+ */
+package androidx.work.impl
+
+import android.content.Context
+import android.os.Build
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.work.impl.WorkDatabaseVersions.VERSION_1
+import androidx.work.impl.WorkDatabaseVersions.VERSION_10
+import androidx.work.impl.WorkDatabaseVersions.VERSION_11
+import androidx.work.impl.WorkDatabaseVersions.VERSION_12
+import androidx.work.impl.WorkDatabaseVersions.VERSION_2
+import androidx.work.impl.WorkDatabaseVersions.VERSION_3
+import androidx.work.impl.WorkDatabaseVersions.VERSION_4
+import androidx.work.impl.WorkDatabaseVersions.VERSION_5
+import androidx.work.impl.WorkDatabaseVersions.VERSION_6
+import androidx.work.impl.WorkDatabaseVersions.VERSION_7
+import androidx.work.impl.WorkDatabaseVersions.VERSION_8
+import androidx.work.impl.WorkDatabaseVersions.VERSION_9
+import androidx.work.impl.model.WorkSpec
+import androidx.work.impl.model.WorkTypeConverters.StateIds.COMPLETED_STATES
+import androidx.work.impl.utils.IdGenerator
+import androidx.work.impl.utils.PreferenceUtils
+
+/**
+ * Migration helpers for [androidx.work.impl.WorkDatabase].
+ */
+internal object WorkDatabaseVersions {
+    // Known WorkDatabase versions
+    const val VERSION_1 = 1
+    const val VERSION_2 = 2
+    const val VERSION_3 = 3
+    const val VERSION_4 = 4
+    const val VERSION_5 = 5
+    const val VERSION_6 = 6
+    const val VERSION_7 = 7
+    const val VERSION_8 = 8
+    const val VERSION_9 = 9
+    const val VERSION_10 = 10
+    const val VERSION_11 = 11
+    const val VERSION_12 = 12
+}
+
+private const val CREATE_SYSTEM_ID_INFO =
+    """
+    CREATE TABLE IF NOT EXISTS `SystemIdInfo` (`work_spec_id` TEXT NOT NULL, `system_id`
+    INTEGER NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)
+    REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )
+    """
+private const val MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO =
+    """
+    INSERT INTO SystemIdInfo(work_spec_id, system_id)
+    SELECT work_spec_id, alarm_id AS system_id FROM alarmInfo
+    """
+private const val PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT =
+    """
+    UPDATE workspec SET schedule_requested_at=0
+    WHERE state NOT IN $COMPLETED_STATES
+        AND schedule_requested_at=${WorkSpec.SCHEDULE_NOT_REQUESTED_YET}
+        AND interval_duration<>0
+    """
+private const val REMOVE_ALARM_INFO = "DROP TABLE IF EXISTS alarmInfo"
+private const val WORKSPEC_ADD_TRIGGER_UPDATE_DELAY =
+    "ALTER TABLE workspec ADD COLUMN `trigger_content_update_delay` INTEGER NOT NULL DEFAULT -1"
+private const val WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY =
+    "ALTER TABLE workspec ADD COLUMN `trigger_max_content_delay` INTEGER NOT NULL DEFAULT -1"
+private const val CREATE_WORK_PROGRESS =
+    """
+    CREATE TABLE IF NOT EXISTS `WorkProgress` (`work_spec_id` TEXT NOT NULL, `progress`
+    BLOB NOT NULL, PRIMARY KEY(`work_spec_id`), FOREIGN KEY(`work_spec_id`)
+    REFERENCES `WorkSpec`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )
+    """
+private const val CREATE_INDEX_PERIOD_START_TIME =
+    """
+    CREATE INDEX IF NOT EXISTS `index_WorkSpec_period_start_time` ON `workspec`(`period_start_time`)
+    """
+private const val CREATE_RUN_IN_FOREGROUND =
+    "ALTER TABLE workspec ADD COLUMN `run_in_foreground` INTEGER NOT NULL DEFAULT 0"
+private const val CREATE_OUT_OF_QUOTA_POLICY =
+    "ALTER TABLE workspec ADD COLUMN `out_of_quota_policy` INTEGER NOT NULL DEFAULT 0"
+
+/**
+ * Removes the `alarmInfo` table and substitutes it for a more general
+ * `SystemIdInfo` table.
+ * Adds implicit work tags for all work (a tag with the worker class name).
+ */
+object Migration_1_2 : Migration(VERSION_1, VERSION_2) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(CREATE_SYSTEM_ID_INFO)
+        database.execSQL(MIGRATE_ALARM_INFO_TO_SYSTEM_ID_INFO)
+        database.execSQL(REMOVE_ALARM_INFO)
+        database.execSQL(
+            """
+                INSERT OR IGNORE INTO worktag(tag, work_spec_id)
+                SELECT worker_class_name AS tag, id AS work_spec_id FROM workspec
+                """
+        )
+    }
+}
+
+/**
+ * Marks `SCHEDULE_REQUESTED_AT` to something other than
+ * `SCHEDULE_NOT_REQUESTED_AT`.
+ */
+object Migration_3_4 : Migration(VERSION_3, VERSION_4) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        if (Build.VERSION.SDK_INT >= WorkManagerImpl.MIN_JOB_SCHEDULER_API_LEVEL) {
+            database.execSQL(PERIODIC_WORK_SET_SCHEDULE_REQUESTED_AT)
+        }
+    }
+}
+
+/**
+ * Adds the `ContentUri` delays to the WorkSpec table.
+ */
+object Migration_4_5 : Migration(VERSION_4, VERSION_5) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(WORKSPEC_ADD_TRIGGER_UPDATE_DELAY)
+        database.execSQL(WORKSPEC_ADD_TRIGGER_MAX_CONTENT_DELAY)
+    }
+}
+
+/**
+ * Adds [androidx.work.impl.model.WorkProgress].
+ */
+object Migration_6_7 : Migration(VERSION_6, VERSION_7) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(CREATE_WORK_PROGRESS)
+    }
+}
+
+/**
+ * Adds an index on period_start_time in [WorkSpec].
+ */
+object Migration_7_8 : Migration(VERSION_7, VERSION_8) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(CREATE_INDEX_PERIOD_START_TIME)
+    }
+}
+
+/**
+ * Adds a notification_provider to the [WorkSpec].
+ */
+object Migration_8_9 : Migration(VERSION_8, VERSION_9) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(CREATE_RUN_IN_FOREGROUND)
+    }
+}
+
+/**
+ * Adds a notification_provider to the [WorkSpec].
+ */
+object Migration_11_12 : Migration(VERSION_11, VERSION_12) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(CREATE_OUT_OF_QUOTA_POLICY)
+    }
+}
+
+/**
+ * A [WorkDatabase] migration that reschedules all eligible Workers.
+ */
+class RescheduleMigration(val mContext: Context, startVersion: Int, endVersion: Int) :
+    Migration(startVersion, endVersion) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        if (endVersion >= VERSION_10) {
+            database.execSQL(
+                PreferenceUtils.INSERT_PREFERENCE,
+                arrayOf<Any>(PreferenceUtils.KEY_RESCHEDULE_NEEDED, 1)
+            )
+        } else {
+            val preferences = mContext.getSharedPreferences(
+                PreferenceUtils.PREFERENCES_FILE_NAME,
+                Context.MODE_PRIVATE
+            )
+
+            // Mutate the shared preferences directly, and eventually they will get
+            // migrated to the data store post v10.
+            preferences.edit()
+                .putBoolean(PreferenceUtils.KEY_RESCHEDULE_NEEDED, true)
+                .apply()
+        }
+    }
+}
+
+/**
+ * Adds the [androidx.work.impl.model.Preference] table.
+ */
+internal class WorkMigration9To10(private val context: Context) : Migration(VERSION_9, VERSION_10) {
+    override fun migrate(database: SupportSQLiteDatabase) {
+        database.execSQL(PreferenceUtils.CREATE_PREFERENCE)
+        PreferenceUtils.migrateLegacyPreferences(context, database)
+        IdGenerator.migrateLegacyIdGenerator(context, database)
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java
deleted file mode 100644
index 2c419c1..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.java
+++ /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.
- */
-
-package androidx.work.impl;
-
-import android.content.Context;
-import android.os.Build;
-
-import androidx.annotation.DoNotInline;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.annotation.RestrictTo;
-import androidx.annotation.VisibleForTesting;
-import androidx.work.Logger;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Keeps track of {@link WorkDatabase} paths.
- *
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class WorkDatabasePathHelper {
-    private WorkDatabasePathHelper() {
-    }
-
-    private static final String TAG = Logger.tagWithPrefix("WrkDbPathHelper");
-
-    private static final String WORK_DATABASE_NAME = "androidx.work.workdb";
-
-    // Supporting files for a SQLite database
-    private static final String[] DATABASE_EXTRA_FILES = new String[]{"-journal", "-shm", "-wal"};
-
-    /**
-     * @return The name of the database.
-     */
-    @NonNull
-    public static String getWorkDatabaseName() {
-        return WORK_DATABASE_NAME;
-    }
-
-    /**
-     * Migrates {@link WorkDatabase} to the no-backup directory.
-     *
-     * @param context The application context.
-     */
-    public static void migrateDatabase(@NonNull Context context) {
-        File defaultDatabasePath = getDefaultDatabasePath(context);
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && defaultDatabasePath.exists()) {
-            Logger.get().debug(TAG, "Migrating WorkDatabase to the no-backup directory");
-            Map<File, File> paths = migrationPaths(context);
-            for (File source : paths.keySet()) {
-                File destination = paths.get(source);
-                if (source.exists() && destination != null) {
-                    if (destination.exists()) {
-                        String message = "Over-writing contents of " + destination;
-                        Logger.get().warning(TAG, message);
-                    }
-                    boolean renamed = source.renameTo(destination);
-                    String message;
-                    if (renamed) {
-                        message = "Migrated " + source + "to " + destination;
-                    } else {
-                        message = "Renaming " + source + " to " + destination + " failed";
-                    }
-                    Logger.get().debug(TAG, message);
-                }
-            }
-        }
-    }
-
-    /**
-     * Returns a {@link Map} of all paths which need to be migrated to the no-backup directory.
-     *
-     * @param context The application {@link Context}
-     * @return a {@link Map} of paths to be migrated from source -> destination
-     */
-    @NonNull
-    @VisibleForTesting
-    public static Map<File, File> migrationPaths(@NonNull Context context) {
-        Map<File, File> paths = new HashMap<>();
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-            File databasePath = getDefaultDatabasePath(context);
-            File migratedPath = getDatabasePath(context);
-            paths.put(databasePath, migratedPath);
-            for (String extra : DATABASE_EXTRA_FILES) {
-                File source = new File(databasePath.getPath() + extra);
-                File destination = new File(migratedPath.getPath() + extra);
-                paths.put(source, destination);
-            }
-        }
-        return paths;
-    }
-
-    /**
-     * @param context The application {@link Context}
-     * @return The database path before migration to the no-backup directory.
-     */
-    @NonNull
-    @VisibleForTesting
-    public static File getDefaultDatabasePath(@NonNull Context context) {
-        return context.getDatabasePath(WORK_DATABASE_NAME);
-    }
-
-    /**
-     * @param context The application {@link Context}
-     * @return The the migrated database path.
-     */
-    @NonNull
-    @VisibleForTesting
-    public static File getDatabasePath(@NonNull Context context) {
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
-            // No notion of a backup directory exists.
-            return getDefaultDatabasePath(context);
-        } else {
-            return getNoBackupPath(context, WORK_DATABASE_NAME);
-        }
-    }
-
-    /**
-     * Return the path for a {@link File} path in the {@link Context#getNoBackupFilesDir()}
-     * identified by the {@link String} fragment.
-     *
-     * @param context  The application {@link Context}
-     * @param filePath The {@link String} file path
-     * @return the {@link File}
-     */
-    @RequiresApi(23)
-    private static File getNoBackupPath(@NonNull Context context, @NonNull String filePath) {
-        return new File(Api21Impl.getNoBackupFilesDir(context), filePath);
-    }
-
-    @RequiresApi(21)
-    static class Api21Impl {
-        private Api21Impl() {
-            // This class is not instantiable.
-        }
-
-        @DoNotInline
-        static File getNoBackupFilesDir(Context context) {
-            return context.getNoBackupFilesDir();
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt
new file mode 100644
index 0000000..6ab27fa
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/WorkDatabasePathHelper.kt
@@ -0,0 +1,126 @@
+/*
+ * 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.work.impl
+
+import android.content.Context
+import android.os.Build
+import androidx.annotation.DoNotInline
+import androidx.annotation.RequiresApi
+import androidx.annotation.RestrictTo
+import androidx.work.Logger
+import java.io.File
+
+private val TAG = Logger.tagWithPrefix("WrkDbPathHelper")
+
+/**
+ * @return The name of the database.
+ */
+internal const val WORK_DATABASE_NAME = "androidx.work.workdb"
+
+// Supporting files for a SQLite database
+private val DATABASE_EXTRA_FILES = arrayOf("-journal", "-shm", "-wal")
+
+/**
+ * Keeps track of {@link WorkDatabase} paths.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+object WorkDatabasePathHelper {
+    /**
+     * Migrates [WorkDatabase] to the no-backup directory.
+     *
+     * @param context The application context.
+     */
+    @JvmStatic
+    fun migrateDatabase(context: Context) {
+        val defaultDatabasePath = getDefaultDatabasePath(context)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && defaultDatabasePath.exists()) {
+            Logger.get().debug(TAG, "Migrating WorkDatabase to the no-backup directory")
+            migrationPaths(context).forEach { (source, destination) ->
+                if (source.exists()) {
+                    if (destination.exists()) {
+                        Logger.get().warning(TAG, "Over-writing contents of $destination")
+                    }
+                    val renamed = source.renameTo(destination)
+                    val message = if (renamed) {
+                        "Migrated ${source}to $destination"
+                    } else {
+                        "Renaming $source to $destination failed"
+                    }
+                    Logger.get().debug(TAG, message)
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns a [Map] of all paths which need to be migrated to the no-backup directory.
+     *
+     * @param context The application [Context]
+     * @return a [Map] of paths to be migrated from source -> destination
+     */
+    fun migrationPaths(context: Context): Map<File, File> {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            val databasePath = getDefaultDatabasePath(context)
+            val migratedPath = getDatabasePath(context)
+            val map = DATABASE_EXTRA_FILES.associate { extra ->
+                File(databasePath.path + extra) to File(migratedPath.path + extra)
+            }
+            map + (databasePath to migratedPath)
+        } else emptyMap()
+    }
+
+    /**
+     * @param context The application [Context]
+     * @return The database path before migration to the no-backup directory.
+     */
+    fun getDefaultDatabasePath(context: Context): File {
+        return context.getDatabasePath(WORK_DATABASE_NAME)
+    }
+
+    /**
+     * @param context The application [Context]
+     * @return The the migrated database path.
+     */
+    fun getDatabasePath(context: Context): File {
+        return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+            // No notion of a backup directory exists.
+            getDefaultDatabasePath(context)
+        } else {
+            getNoBackupPath(context)
+        }
+    }
+
+    /**
+     * Return the path for a [File] path in the [Context.getNoBackupFilesDir]
+     * identified by the [String] fragment.
+     *
+     * @param context  The application [Context]
+     * @return the [File]
+     */
+    @RequiresApi(23)
+    private fun getNoBackupPath(context: Context): File {
+        return File(Api21Impl.getNoBackupFilesDir(context), WORK_DATABASE_NAME)
+    }
+}
+
+@RequiresApi(21)
+internal object Api21Impl {
+    @DoNotInline
+    fun getNoBackupFilesDir(context: Context): File {
+        return context.noBackupFilesDir
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
index b5b769b..3928d63 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/WorkConstraintsTracker.kt
@@ -86,13 +86,13 @@
     override fun replace(workSpecs: Iterable<WorkSpec>) {
         synchronized(lock) {
             for (controller in constraintControllers) {
-                controller.setCallback(null)
+                controller.callback = null
             }
             for (controller in constraintControllers) {
                 controller.replace(workSpecs)
             }
             for (controller in constraintControllers) {
-                controller.setCallback(this)
+                controller.callback = this
             }
         }
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
deleted file mode 100644
index 7ff030b..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryChargingController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.BatteryChargingTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for battery charging events.
- */
-
-public class BatteryChargingController extends ConstraintController<Boolean> {
-    public BatteryChargingController(@NonNull BatteryChargingTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.requiresCharging();
-    }
-
-    @Override
-    boolean isConstrained(@NonNull Boolean isBatteryCharging) {
-        return !isBatteryCharging;
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
deleted file mode 100644
index ff2190e..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/BatteryNotLowController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.BatteryNotLowTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for battery not low events.
- */
-
-public class BatteryNotLowController extends ConstraintController<Boolean> {
-    public BatteryNotLowController(@NonNull BatteryNotLowTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.requiresBatteryNotLow();
-    }
-
-    @Override
-    boolean isConstrained(@NonNull Boolean isBatteryNotLow) {
-        return !isBatteryNotLow;
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java
deleted file mode 100644
index b330c34..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.work.impl.constraints.ConstraintListener;
-import androidx.work.impl.constraints.trackers.ConstraintTracker;
-import androidx.work.impl.model.WorkSpec;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * A controller for a particular constraint.
- *
- * @param <T> the constraint data type managed by this controller.
- */
-
-public abstract class ConstraintController<T> implements ConstraintListener<T> {
-
-    /**
-     * A callback for when a constraint changes.
-     */
-    public interface OnConstraintUpdatedCallback {
-
-        /**
-         * Called when a constraint is met.
-         *
-         * @param workSpecIds A list of {@link WorkSpec} IDs that may have become eligible to run
-         */
-        void onConstraintMet(@NonNull List<String> workSpecIds);
-
-        /**
-         * Called when a constraint is not met.
-         *
-         * @param workSpecIds A list of {@link WorkSpec} IDs that have become ineligible to run
-         */
-        void onConstraintNotMet(@NonNull List<String> workSpecIds);
-    }
-
-    private final List<String> mMatchingWorkSpecIds = new ArrayList<>();
-
-    private T mCurrentValue;
-    private ConstraintTracker<T> mTracker;
-    private OnConstraintUpdatedCallback mCallback;
-
-    ConstraintController(ConstraintTracker<T> tracker) {
-        mTracker = tracker;
-    }
-
-    /**
-     * Sets the callback to inform when constraints change.  This callback is also triggered the
-     * first time it is set.
-     *
-     * @param callback The callback to inform about constraint met/unmet states
-     */
-    public void setCallback(@Nullable OnConstraintUpdatedCallback callback) {
-        if (mCallback != callback) {
-            mCallback = callback;
-            updateCallback(mCallback, mCurrentValue);
-        }
-    }
-
-    abstract boolean hasConstraint(@NonNull WorkSpec workSpec);
-
-    abstract boolean isConstrained(@NonNull T currentValue);
-
-    /**
-     * Replaces the list of {@link WorkSpec}s to monitor constraints for.
-     *
-     * @param workSpecs A list of {@link WorkSpec}s to monitor constraints for
-     */
-    public void replace(@NonNull Iterable<WorkSpec> workSpecs) {
-        mMatchingWorkSpecIds.clear();
-
-        for (WorkSpec workSpec : workSpecs) {
-            if (hasConstraint(workSpec)) {
-                mMatchingWorkSpecIds.add(workSpec.id);
-            }
-        }
-
-        if (mMatchingWorkSpecIds.isEmpty()) {
-            mTracker.removeListener(this);
-        } else {
-            mTracker.addListener(this);
-        }
-        updateCallback(mCallback, mCurrentValue);
-    }
-
-    /**
-     * Clears all tracked {@link WorkSpec}s.
-     */
-    public void reset() {
-        if (!mMatchingWorkSpecIds.isEmpty()) {
-            mMatchingWorkSpecIds.clear();
-            mTracker.removeListener(this);
-        }
-    }
-
-    /**
-     * Determines if a particular {@link WorkSpec} is constrained. It is constrained if it is
-     * tracked by this controller, and the controller constraint was set, but not satisfied.
-     *
-     * @param workSpecId The ID of the {@link WorkSpec} to check if it is constrained.
-     * @return {@code true} if the {@link WorkSpec} is considered constrained
-     */
-    public boolean isWorkSpecConstrained(@NonNull String workSpecId) {
-        return mCurrentValue != null && isConstrained(mCurrentValue)
-                && mMatchingWorkSpecIds.contains(workSpecId);
-    }
-
-    private void updateCallback(
-            @Nullable OnConstraintUpdatedCallback callback,
-            T currentValue) {
-
-        // We pass copies of references (callback, currentValue) to updateCallback because public
-        // APIs on ConstraintController may be called from any thread, and onConstraintChanged() is
-        // called from the main thread.
-        if (mMatchingWorkSpecIds.isEmpty() || callback == null) {
-            return;
-        }
-
-        if (currentValue == null || isConstrained(currentValue)) {
-            callback.onConstraintNotMet(mMatchingWorkSpecIds);
-        } else {
-            callback.onConstraintMet(mMatchingWorkSpecIds);
-        }
-    }
-
-    @Override
-    public void onConstraintChanged(T newValue) {
-        mCurrentValue = newValue;
-        updateCallback(mCallback, mCurrentValue);
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt
new file mode 100644
index 0000000..5081fcf
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ConstraintController.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.work.impl.constraints.controllers
+
+import androidx.work.impl.constraints.ConstraintListener
+import androidx.work.impl.constraints.trackers.ConstraintTracker
+import androidx.work.impl.model.WorkSpec
+
+/**
+ * A controller for a particular constraint.
+ *
+ * @param <T> the constraint data type managed by this controller.
+ */
+abstract class ConstraintController<T> internal constructor(
+    private val tracker: ConstraintTracker<T>
+) : ConstraintListener<T> {
+    /**
+     * A callback for when a constraint changes.
+     */
+    interface OnConstraintUpdatedCallback {
+        /**
+         * Called when a constraint is met.
+         *
+         * @param workSpecIds A list of [WorkSpec] IDs that may have become eligible to run
+         */
+        fun onConstraintMet(workSpecIds: List<String>)
+
+        /**
+         * Called when a constraint is not met.
+         *
+         * @param workSpecIds A list of [WorkSpec] IDs that have become ineligible to run
+         */
+        fun onConstraintNotMet(workSpecIds: List<String>)
+    }
+
+    private val matchingWorkSpecIds = mutableListOf<String>()
+    private var currentValue: T? = null
+
+    /**
+     * Sets the callback to inform when constraints change.  This callback is also triggered the
+     * first time it is set.
+     */
+    var callback: OnConstraintUpdatedCallback? = null
+        set(value) {
+            if (field !== value) {
+                field = value
+                updateCallback(value, currentValue)
+            }
+        }
+
+    abstract fun hasConstraint(workSpec: WorkSpec): Boolean
+    abstract fun isConstrained(value: T): Boolean
+
+    /**
+     * Replaces the list of [WorkSpec]s to monitor constraints for.
+     *
+     * @param workSpecs A list of [WorkSpec]s to monitor constraints for
+     */
+    fun replace(workSpecs: Iterable<WorkSpec>) {
+        matchingWorkSpecIds.clear()
+        workSpecs.mapNotNullTo(matchingWorkSpecIds) {
+            if (hasConstraint(it)) it.id else null
+        }
+
+        if (matchingWorkSpecIds.isEmpty()) {
+            tracker.removeListener(this)
+        } else {
+            tracker.addListener(this)
+        }
+        updateCallback(callback, currentValue)
+    }
+
+    /**
+     * Clears all tracked [WorkSpec]s.
+     */
+    fun reset() {
+        if (matchingWorkSpecIds.isNotEmpty()) {
+            matchingWorkSpecIds.clear()
+            tracker.removeListener(this)
+        }
+    }
+
+    /**
+     * Determines if a particular [WorkSpec] is constrained. It is constrained if it is
+     * tracked by this controller, and the controller constraint was set, but not satisfied.
+     *
+     * @param workSpecId The ID of the [WorkSpec] to check if it is constrained.
+     * @return `true` if the [WorkSpec] is considered constrained
+     */
+    fun isWorkSpecConstrained(workSpecId: String): Boolean {
+        // TODO: unify `null` treatment here and in updateCallback, because
+        // here it is considered as not constrained and but in updateCallback as constrained.
+        val value = currentValue
+        return (value != null && isConstrained(value) && workSpecId in matchingWorkSpecIds)
+    }
+
+    private fun updateCallback(callback: OnConstraintUpdatedCallback?, currentValue: T?) {
+        // We pass copies of references (callback, currentValue) to updateCallback because public
+        // APIs on ConstraintController may be called from any thread, and onConstraintChanged() is
+        // called from the main thread.
+        if (matchingWorkSpecIds.isEmpty() || callback == null) {
+            return
+        }
+        if (currentValue == null || isConstrained(currentValue)) {
+            callback.onConstraintNotMet(matchingWorkSpecIds)
+        } else {
+            callback.onConstraintMet(matchingWorkSpecIds)
+        }
+    }
+
+    override fun onConstraintChanged(newValue: T) {
+        currentValue = newValue
+        updateCallback(callback, currentValue)
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
new file mode 100644
index 0000000..c72820c
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/ContraintControllers.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.work.impl.constraints.controllers
+
+import android.os.Build
+import androidx.work.Logger
+import androidx.work.NetworkType
+import androidx.work.NetworkType.TEMPORARILY_UNMETERED
+import androidx.work.NetworkType.UNMETERED
+import androidx.work.impl.constraints.NetworkState
+import androidx.work.impl.constraints.trackers.BatteryChargingTracker
+import androidx.work.impl.constraints.trackers.BatteryNotLowTracker
+import androidx.work.impl.constraints.trackers.NetworkStateTracker
+import androidx.work.impl.constraints.trackers.StorageNotLowTracker
+import androidx.work.impl.model.WorkSpec
+
+/**
+ * A [ConstraintController] for battery charging events.
+ */
+class BatteryChargingController(tracker: BatteryChargingTracker) :
+    ConstraintController<Boolean>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresCharging()
+
+    override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for battery not low events.
+ */
+class BatteryNotLowController(tracker: BatteryNotLowTracker) :
+    ConstraintController<Boolean>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresBatteryNotLow()
+
+    override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is unmetered.
+ */
+class NetworkUnmeteredController(tracker: NetworkStateTracker) :
+    ConstraintController<NetworkState>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec): Boolean {
+        val requiredNetworkType = workSpec.constraints.requiredNetworkType
+        return requiredNetworkType == UNMETERED ||
+            (Build.VERSION.SDK_INT >= 30 && requiredNetworkType == TEMPORARILY_UNMETERED)
+    }
+
+    override fun isConstrained(value: NetworkState) = !value.isConnected || value.isMetered
+}
+
+/**
+ * A [ConstraintController] for storage not low events.
+ */
+class StorageNotLowController(tracker: StorageNotLowTracker) :
+    ConstraintController<Boolean>(tracker) {
+
+    override fun hasConstraint(workSpec: WorkSpec) = workSpec.constraints.requiresStorageNotLow()
+
+    override fun isConstrained(value: Boolean) = !value
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is not roaming.
+ */
+class NetworkNotRoamingController(tracker: NetworkStateTracker) :
+    ConstraintController<NetworkState>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec): Boolean {
+        return workSpec.constraints.requiredNetworkType == NetworkType.NOT_ROAMING
+    }
+
+    /**
+     * Check for not-roaming constraint on API 24+, when JobInfo#NETWORK_TYPE_NOT_ROAMING was added,
+     * to be consistent with JobScheduler functionality.
+     */
+    override fun isConstrained(value: NetworkState): Boolean {
+        return if (Build.VERSION.SDK_INT < 24) {
+            Logger.get().debug(
+                TAG, "Not-roaming network constraint is not supported before API 24, " +
+                    "only checking for connected state."
+            )
+            !value.isConnected
+        } else !value.isConnected || !value.isNotRoaming
+    }
+
+    companion object {
+        private val TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr")
+    }
+}
+
+/**
+ * A [ConstraintController] for monitoring that any usable network connection is available.
+ *
+ *
+ * For API 26 and above, usable means that the [NetworkState] is validated, i.e.
+ * it has a working internet connection.
+ *
+ *
+ * For API 25 and below, usable simply means that [NetworkState] is connected.
+ */
+class NetworkConnectedController(tracker: NetworkStateTracker) :
+    ConstraintController<NetworkState>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec) =
+        workSpec.constraints.requiredNetworkType == NetworkType.CONNECTED
+
+    override fun isConstrained(value: NetworkState) =
+        if (Build.VERSION.SDK_INT >= 26) {
+            !value.isConnected || !value.isValidated
+        } else {
+            !value.isConnected
+        }
+}
+
+/**
+ * A [ConstraintController] for monitoring that the network connection is metered.
+ */
+class NetworkMeteredController(tracker: NetworkStateTracker) :
+    ConstraintController<NetworkState>(tracker) {
+    override fun hasConstraint(workSpec: WorkSpec) =
+        workSpec.constraints.requiredNetworkType == NetworkType.METERED
+
+    /**
+     * Check for metered constraint on API 26+, when JobInfo#NETWORK_METERED was added, to
+     * be consistent with JobScheduler functionality.
+     */
+    override fun isConstrained(value: NetworkState): Boolean {
+        return if (Build.VERSION.SDK_INT < 26) {
+            Logger.get().debug(
+                TAG, "Metered network constraint is not supported before API 26, " +
+                    "only checking for connected state."
+            )
+            !value.isConnected
+        } else !value.isConnected || !value.isMetered
+    }
+
+    companion object {
+        private val TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr")
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
deleted file mode 100644
index 099953b9..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkConnectedController.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.CONNECTED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that any usable network connection is available.
- * <p>
- * For API 26 and above, usable means that the {@link NetworkState} is validated, i.e.
- * it has a working internet connection.
- * <p>
- * For API 25 and below, usable simply means that {@link NetworkState} is connected.
- */
-
-public class NetworkConnectedController extends ConstraintController<NetworkState> {
-    public NetworkConnectedController(@NonNull NetworkStateTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.getRequiredNetworkType() == CONNECTED;
-    }
-
-    @Override
-    boolean isConstrained(@NonNull NetworkState state) {
-        if (Build.VERSION.SDK_INT >= 26) {
-            return !state.isConnected() || !state.isValidated();
-        } else {
-            return !state.isConnected();
-        }
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
deleted file mode 100644
index 99a5519..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkMeteredController.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.METERED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is metered.
- */
-
-public class NetworkMeteredController extends ConstraintController<NetworkState> {
-    private static final String TAG = Logger.tagWithPrefix("NetworkMeteredCtrlr");
-
-    public NetworkMeteredController(@NonNull NetworkStateTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.getRequiredNetworkType() == METERED;
-    }
-
-    /**
-     * Check for metered constraint on API 26+, when JobInfo#NETWORK_METERED was added, to
-     * be consistent with JobScheduler functionality.
-     */
-    @Override
-    boolean isConstrained(@NonNull NetworkState state) {
-        if (Build.VERSION.SDK_INT < 26) {
-            Logger.get().debug(TAG, "Metered network constraint is not supported before API 26, "
-                    + "only checking for connected state.");
-            return !state.isConnected();
-        }
-        return !state.isConnected() || !state.isMetered();
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
deleted file mode 100644
index fb13514..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkNotRoamingController.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.NOT_ROAMING;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.Logger;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is not roaming.
- */
-
-public class NetworkNotRoamingController extends ConstraintController<NetworkState> {
-    private static final String TAG = Logger.tagWithPrefix("NetworkNotRoamingCtrlr");
-
-    public NetworkNotRoamingController(@NonNull NetworkStateTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.getRequiredNetworkType() == NOT_ROAMING;
-    }
-
-    /**
-     * Check for not-roaming constraint on API 24+, when JobInfo#NETWORK_TYPE_NOT_ROAMING was added,
-     * to be consistent with JobScheduler functionality.
-     */
-    @Override
-    boolean isConstrained(@NonNull NetworkState state) {
-        if (Build.VERSION.SDK_INT < 24) {
-            Logger.get().debug(
-                    TAG,
-                    "Not-roaming network constraint is not supported before API 24, "
-                            + "only checking for connected state.");
-            return !state.isConnected();
-        }
-        return !state.isConnected() || !state.isNotRoaming();
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
deleted file mode 100644
index 24b4573..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/NetworkUnmeteredController.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Copyright 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.work.impl.constraints.controllers;
-
-import static androidx.work.NetworkType.TEMPORARILY_UNMETERED;
-import static androidx.work.NetworkType.UNMETERED;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.NetworkState;
-import androidx.work.impl.constraints.trackers.NetworkStateTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for monitoring that the network connection is unmetered.
- */
-
-public class NetworkUnmeteredController extends ConstraintController<NetworkState> {
-    public NetworkUnmeteredController(@NonNull NetworkStateTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.getRequiredNetworkType() == UNMETERED
-                || (Build.VERSION.SDK_INT >= 30
-                && workSpec.constraints.getRequiredNetworkType() == TEMPORARILY_UNMETERED);
-    }
-
-    @Override
-    boolean isConstrained(@NonNull NetworkState state) {
-        return !state.isConnected() || state.isMetered();
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
deleted file mode 100644
index dc8c18b..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/controllers/StorageNotLowController.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package androidx.work.impl.constraints.controllers;
-
-import androidx.annotation.NonNull;
-import androidx.work.impl.constraints.trackers.StorageNotLowTracker;
-import androidx.work.impl.model.WorkSpec;
-
-/**
- * A {@link ConstraintController} for storage not low events.
- */
-
-public class StorageNotLowController extends ConstraintController<Boolean> {
-    public StorageNotLowController(@NonNull StorageNotLowTracker tracker) {
-        super(tracker);
-    }
-
-    @Override
-    boolean hasConstraint(@NonNull WorkSpec workSpec) {
-        return workSpec.constraints.requiresStorageNotLow();
-    }
-
-    @Override
-    boolean isConstrained(@NonNull Boolean isStorageNotLow) {
-        return !isStorageNotLow;
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
index 7d64637..935d3ab 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/constraints/trackers/ConstraintTracker.java
@@ -48,7 +48,7 @@
     // Synthetic access
     T mCurrentState;
 
-    ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
+    protected ConstraintTracker(@NonNull Context context, @NonNull TaskExecutor taskExecutor) {
         mAppContext = context.getApplicationContext();
         mTaskExecutor = taskExecutor;
     }
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
deleted file mode 100644
index f60ac02..0000000
--- a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.java
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * 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.
- */
-
-package androidx.work.impl.model;
-
-import static androidx.work.BackoffPolicy.EXPONENTIAL;
-import static androidx.work.BackoffPolicy.LINEAR;
-import static androidx.work.WorkInfo.State.BLOCKED;
-import static androidx.work.WorkInfo.State.CANCELLED;
-import static androidx.work.WorkInfo.State.ENQUEUED;
-import static androidx.work.WorkInfo.State.FAILED;
-import static androidx.work.WorkInfo.State.RUNNING;
-import static androidx.work.WorkInfo.State.SUCCEEDED;
-
-import android.net.Uri;
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.room.TypeConverter;
-import androidx.work.BackoffPolicy;
-import androidx.work.ContentUriTriggers;
-import androidx.work.NetworkType;
-import androidx.work.OutOfQuotaPolicy;
-import androidx.work.WorkInfo;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-
-/**
- * TypeConverters for WorkManager enums and classes.
- */
-
-public class WorkTypeConverters {
-
-    /**
-     * Integer identifiers that map to {@link WorkInfo.State}.
-     */
-    public interface StateIds {
-        int ENQUEUED = 0;
-        int RUNNING = 1;
-        int SUCCEEDED = 2;
-        int FAILED = 3;
-        int BLOCKED = 4;
-        int CANCELLED = 5;
-
-        String COMPLETED_STATES = "(" + SUCCEEDED + ", " + FAILED + ", " + CANCELLED + ")";
-    }
-
-    /**
-     * Integer identifiers that map to {@link BackoffPolicy}.
-     */
-    public interface BackoffPolicyIds {
-        int EXPONENTIAL = 0;
-        int LINEAR = 1;
-    }
-
-    /**
-     * Integer identifiers that map to {@link NetworkType}.
-     */
-    public interface NetworkTypeIds {
-        int NOT_REQUIRED = 0;
-        int CONNECTED = 1;
-        int UNMETERED = 2;
-        int NOT_ROAMING = 3;
-        int METERED = 4;
-        int TEMPORARILY_UNMETERED = 5;
-    }
-
-    /**
-     * Integer identifiers that map to {@link OutOfQuotaPolicy}.
-     */
-    public interface OutOfPolicyIds {
-        int RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0;
-        int DROP_WORK_REQUEST = 1;
-    }
-
-    /**
-     * TypeConverter for a State to an int.
-     *
-     * @param state The input State
-     * @return The associated int constant
-     */
-    @TypeConverter
-    public static int stateToInt(WorkInfo.State state) {
-        switch (state) {
-            case ENQUEUED:
-                return StateIds.ENQUEUED;
-
-            case RUNNING:
-                return StateIds.RUNNING;
-
-            case SUCCEEDED:
-                return StateIds.SUCCEEDED;
-
-            case FAILED:
-                return StateIds.FAILED;
-
-            case BLOCKED:
-                return StateIds.BLOCKED;
-
-            case CANCELLED:
-                return StateIds.CANCELLED;
-
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + state + " to int");
-        }
-    }
-
-    /**
-     * TypeConverter for an int to a State.
-     *
-     * @param value The input integer
-     * @return The associated State enum value
-     */
-    @TypeConverter
-    public static WorkInfo.State intToState(int value) {
-        switch (value) {
-            case StateIds.ENQUEUED:
-                return ENQUEUED;
-
-            case StateIds.RUNNING:
-                return RUNNING;
-
-            case StateIds.SUCCEEDED:
-                return SUCCEEDED;
-
-            case StateIds.FAILED:
-                return FAILED;
-
-            case StateIds.BLOCKED:
-                return BLOCKED;
-
-            case StateIds.CANCELLED:
-                return CANCELLED;
-
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + value + " to State");
-        }
-    }
-
-    /**
-     * TypeConverter for a BackoffPolicy to an int.
-     *
-     * @param backoffPolicy The input BackoffPolicy
-     * @return The associated int constant
-     */
-    @TypeConverter
-    public static int backoffPolicyToInt(BackoffPolicy backoffPolicy) {
-        switch (backoffPolicy) {
-            case EXPONENTIAL:
-                return BackoffPolicyIds.EXPONENTIAL;
-
-            case LINEAR:
-                return BackoffPolicyIds.LINEAR;
-
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + backoffPolicy + " to int");
-        }
-    }
-
-    /**
-     * TypeConverter for an int to a BackoffPolicy.
-     *
-     * @param value The input integer
-     * @return The associated BackoffPolicy enum value
-     */
-    @TypeConverter
-    public static BackoffPolicy intToBackoffPolicy(int value) {
-        switch (value) {
-            case BackoffPolicyIds.EXPONENTIAL:
-                return EXPONENTIAL;
-
-            case BackoffPolicyIds.LINEAR:
-                return LINEAR;
-
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + value + " to BackoffPolicy");
-        }
-    }
-
-    /**
-     * TypeConverter for a NetworkType to an int.
-     *
-     * @param networkType The input NetworkType
-     * @return The associated int constant
-     */
-    @TypeConverter
-    public static int networkTypeToInt(NetworkType networkType) {
-        switch (networkType) {
-            case NOT_REQUIRED:
-                return NetworkTypeIds.NOT_REQUIRED;
-
-            case CONNECTED:
-                return NetworkTypeIds.CONNECTED;
-
-            case UNMETERED:
-                return NetworkTypeIds.UNMETERED;
-
-            case NOT_ROAMING:
-                return NetworkTypeIds.NOT_ROAMING;
-
-            case METERED:
-                return NetworkTypeIds.METERED;
-
-            default:
-                if (Build.VERSION.SDK_INT >= 30
-                        && networkType == NetworkType.TEMPORARILY_UNMETERED) {
-                    return NetworkTypeIds.TEMPORARILY_UNMETERED;
-                }
-                throw new IllegalArgumentException(
-                        "Could not convert " + networkType + " to int");
-
-        }
-    }
-
-    /**
-     * TypeConverter for an int to a NetworkType.
-     *
-     * @param value The input integer
-     * @return The associated NetworkType enum value
-     */
-    @TypeConverter
-    public static NetworkType intToNetworkType(int value) {
-        switch (value) {
-            case NetworkTypeIds.NOT_REQUIRED:
-                return NetworkType.NOT_REQUIRED;
-
-            case NetworkTypeIds.CONNECTED:
-                return NetworkType.CONNECTED;
-
-            case NetworkTypeIds.UNMETERED:
-                return NetworkType.UNMETERED;
-
-            case NetworkTypeIds.NOT_ROAMING:
-                return NetworkType.NOT_ROAMING;
-
-            case NetworkTypeIds.METERED:
-                return NetworkType.METERED;
-
-            default:
-                if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
-                    return NetworkType.TEMPORARILY_UNMETERED;
-                }
-                throw new IllegalArgumentException(
-                        "Could not convert " + value + " to NetworkType");
-        }
-    }
-
-    /**
-     * Converts a {@link OutOfQuotaPolicy} to an int.
-     *
-     * @param policy The {@link OutOfQuotaPolicy} policy being used
-     * @return the corresponding int representation.
-     */
-    @TypeConverter
-    public static int outOfQuotaPolicyToInt(@NonNull OutOfQuotaPolicy policy) {
-        switch (policy) {
-            case RUN_AS_NON_EXPEDITED_WORK_REQUEST:
-                return OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
-            case DROP_WORK_REQUEST:
-                return OutOfPolicyIds.DROP_WORK_REQUEST;
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + policy + " to int");
-        }
-    }
-
-    /**
-     * Converter from an int to a {@link OutOfQuotaPolicy}.
-     *
-     * @param value The input integer
-     * @return An {@link OutOfQuotaPolicy}
-     */
-    @TypeConverter
-    @NonNull
-    public static OutOfQuotaPolicy intToOutOfQuotaPolicy(int value) {
-        switch (value) {
-            case OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST:
-                return OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST;
-            case OutOfPolicyIds.DROP_WORK_REQUEST:
-                return OutOfQuotaPolicy.DROP_WORK_REQUEST;
-            default:
-                throw new IllegalArgumentException(
-                        "Could not convert " + value + " to OutOfQuotaPolicy");
-        }
-    }
-
-    /**
-     * Converts a list of {@link ContentUriTriggers.Trigger}s to byte array representation
-     * @param triggers the list of {@link ContentUriTriggers.Trigger}s to convert
-     * @return corresponding byte array representation
-     */
-    @TypeConverter
-    @SuppressWarnings("CatchAndPrintStackTrace")
-    public static byte[] contentUriTriggersToByteArray(ContentUriTriggers triggers) {
-        if (triggers.size() == 0) {
-            return null;
-        }
-        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-        ObjectOutputStream objectOutputStream = null;
-        try {
-            objectOutputStream = new ObjectOutputStream(outputStream);
-            objectOutputStream.writeInt(triggers.size());
-            for (ContentUriTriggers.Trigger trigger : triggers.getTriggers()) {
-                objectOutputStream.writeUTF(trigger.getUri().toString());
-                objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants());
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-        } finally {
-            if (objectOutputStream != null) {
-                try {
-                    objectOutputStream.close();
-                } catch (IOException e) {
-                    e.printStackTrace();
-                }
-            }
-            try {
-                outputStream.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return outputStream.toByteArray();
-    }
-
-    /**
-     * Converts a byte array to list of {@link ContentUriTriggers.Trigger}s
-     * @param bytes byte array representation to convert
-     * @return list of {@link ContentUriTriggers.Trigger}s
-     */
-    @TypeConverter
-    @SuppressWarnings("CatchAndPrintStackTrace")
-    public static ContentUriTriggers byteArrayToContentUriTriggers(byte[] bytes) {
-        ContentUriTriggers triggers = new ContentUriTriggers();
-        if (bytes == null) {
-            // bytes will be null if there are no Content Uri Triggers
-            return triggers;
-        }
-        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
-        ObjectInputStream objectInputStream = null;
-        try {
-            objectInputStream = new ObjectInputStream(inputStream);
-            for (int i = objectInputStream.readInt(); i > 0; i--) {
-                Uri uri = Uri.parse(objectInputStream.readUTF());
-                boolean triggersForDescendants = objectInputStream.readBoolean();
-                triggers.add(uri, triggersForDescendants);
-            }
-        } catch (IOException e) {
-            e.printStackTrace();
-        } finally {
-            if (objectInputStream != null) {
-                try {
-                    objectInputStream.close();
-                } catch (IOException e) {
-                    e.printStackTrace();
-                }
-            }
-            try {
-                inputStream.close();
-            } catch (IOException e) {
-                e.printStackTrace();
-            }
-        }
-        return triggers;
-    }
-
-    private WorkTypeConverters() {
-    }
-}
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt
new file mode 100644
index 0000000..2d4ab79
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/model/WorkTypeConverters.kt
@@ -0,0 +1,282 @@
+/*
+ * 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.
+ */
+package androidx.work.impl.model
+
+import android.net.Uri
+import android.os.Build
+import androidx.room.TypeConverter
+import androidx.work.BackoffPolicy
+import androidx.work.ContentUriTriggers
+import androidx.work.NetworkType
+import androidx.work.OutOfQuotaPolicy
+import androidx.work.WorkInfo
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.ObjectInputStream
+import java.io.ObjectOutputStream
+import java.lang.IllegalArgumentException
+
+/**
+ * TypeConverters for WorkManager enums and classes.
+ */
+object WorkTypeConverters {
+    /**
+     * Integer identifiers that map to [WorkInfo.State].
+     */
+    object StateIds {
+        const val ENQUEUED = 0
+        const val RUNNING = 1
+        const val SUCCEEDED = 2
+        const val FAILED = 3
+        const val BLOCKED = 4
+        const val CANCELLED = 5
+        const val COMPLETED_STATES = "($SUCCEEDED, $FAILED, $CANCELLED)"
+    }
+
+    /**
+     * Integer identifiers that map to [BackoffPolicy].
+     */
+    private object BackoffPolicyIds {
+        const val EXPONENTIAL = 0
+        const val LINEAR = 1
+    }
+
+    /**
+     * Integer identifiers that map to [NetworkType].
+     */
+    private object NetworkTypeIds {
+        const val NOT_REQUIRED = 0
+        const val CONNECTED = 1
+        const val UNMETERED = 2
+        const val NOT_ROAMING = 3
+        const val METERED = 4
+        const val TEMPORARILY_UNMETERED = 5
+    }
+
+    /**
+     * Integer identifiers that map to [OutOfQuotaPolicy].
+     */
+    private object OutOfPolicyIds {
+        const val RUN_AS_NON_EXPEDITED_WORK_REQUEST = 0
+        const val DROP_WORK_REQUEST = 1
+    }
+
+    /**
+     * TypeConverter for a State to an int.
+     *
+     * @param state The input State
+     * @return The associated int constant
+     */
+    @JvmStatic
+    @TypeConverter
+    fun stateToInt(state: WorkInfo.State): Int {
+        return when (state) {
+            WorkInfo.State.ENQUEUED -> StateIds.ENQUEUED
+            WorkInfo.State.RUNNING -> StateIds.RUNNING
+            WorkInfo.State.SUCCEEDED -> StateIds.SUCCEEDED
+            WorkInfo.State.FAILED -> StateIds.FAILED
+            WorkInfo.State.BLOCKED -> StateIds.BLOCKED
+            WorkInfo.State.CANCELLED -> StateIds.CANCELLED
+        }
+    }
+
+    /**
+     * TypeConverter for an int to a State.
+     *
+     * @param value The input integer
+     * @return The associated State enum value
+     */
+    @JvmStatic
+    @TypeConverter
+    fun intToState(value: Int): WorkInfo.State {
+        return when (value) {
+            StateIds.ENQUEUED -> WorkInfo.State.ENQUEUED
+            StateIds.RUNNING -> WorkInfo.State.RUNNING
+            StateIds.SUCCEEDED -> WorkInfo.State.SUCCEEDED
+            StateIds.FAILED -> WorkInfo.State.FAILED
+            StateIds.BLOCKED -> WorkInfo.State.BLOCKED
+            StateIds.CANCELLED -> WorkInfo.State.CANCELLED
+            else -> throw IllegalArgumentException("Could not convert $value to State")
+        }
+    }
+
+    /**
+     * TypeConverter for a BackoffPolicy to an int.
+     *
+     * @param backoffPolicy The input BackoffPolicy
+     * @return The associated int constant
+     */
+    @JvmStatic
+    @TypeConverter
+    fun backoffPolicyToInt(backoffPolicy: BackoffPolicy): Int {
+        return when (backoffPolicy) {
+            BackoffPolicy.EXPONENTIAL -> BackoffPolicyIds.EXPONENTIAL
+            BackoffPolicy.LINEAR -> BackoffPolicyIds.LINEAR
+        }
+    }
+
+    /**
+     * TypeConverter for an int to a BackoffPolicy.
+     *
+     * @param value The input integer
+     * @return The associated BackoffPolicy enum value
+     */
+    @JvmStatic
+    @TypeConverter
+    fun intToBackoffPolicy(value: Int): BackoffPolicy {
+        return when (value) {
+            BackoffPolicyIds.EXPONENTIAL -> BackoffPolicy.EXPONENTIAL
+            BackoffPolicyIds.LINEAR -> BackoffPolicy.LINEAR
+            else -> throw IllegalArgumentException("Could not convert $value to BackoffPolicy")
+        }
+    }
+
+    /**
+     * TypeConverter for a NetworkType to an int.
+     *
+     * @param networkType The input NetworkType
+     * @return The associated int constant
+     */
+    @JvmStatic
+    @TypeConverter
+    fun networkTypeToInt(networkType: NetworkType): Int {
+        return when (networkType) {
+            NetworkType.NOT_REQUIRED -> NetworkTypeIds.NOT_REQUIRED
+            NetworkType.CONNECTED -> NetworkTypeIds.CONNECTED
+            NetworkType.UNMETERED -> NetworkTypeIds.UNMETERED
+            NetworkType.NOT_ROAMING -> NetworkTypeIds.NOT_ROAMING
+            NetworkType.METERED -> NetworkTypeIds.METERED
+            else -> {
+                if (Build.VERSION.SDK_INT >= 30 && networkType == NetworkType.TEMPORARILY_UNMETERED)
+                    NetworkTypeIds.TEMPORARILY_UNMETERED
+                else
+                    throw IllegalArgumentException("Could not convert $networkType to int")
+            }
+        }
+    }
+
+    /**
+     * TypeConverter for an int to a NetworkType.
+     *
+     * @param value The input integer
+     * @return The associated NetworkType enum value
+     */
+    @JvmStatic
+    @TypeConverter
+    fun intToNetworkType(value: Int): NetworkType {
+        return when (value) {
+            NetworkTypeIds.NOT_REQUIRED -> NetworkType.NOT_REQUIRED
+            NetworkTypeIds.CONNECTED -> NetworkType.CONNECTED
+            NetworkTypeIds.UNMETERED -> NetworkType.UNMETERED
+            NetworkTypeIds.NOT_ROAMING -> NetworkType.NOT_ROAMING
+            NetworkTypeIds.METERED -> NetworkType.METERED
+            else -> {
+                if (Build.VERSION.SDK_INT >= 30 && value == NetworkTypeIds.TEMPORARILY_UNMETERED) {
+                    return NetworkType.TEMPORARILY_UNMETERED
+                } else throw IllegalArgumentException("Could not convert $value to NetworkType")
+            }
+        }
+    }
+
+    /**
+     * Converts a [OutOfQuotaPolicy] to an int.
+     *
+     * @param policy The [OutOfQuotaPolicy] policy being used
+     * @return the corresponding int representation.
+     */
+    @JvmStatic
+    @TypeConverter
+    fun outOfQuotaPolicyToInt(policy: OutOfQuotaPolicy): Int {
+        return when (policy) {
+            OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
+                OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST
+            OutOfQuotaPolicy.DROP_WORK_REQUEST -> OutOfPolicyIds.DROP_WORK_REQUEST
+        }
+    }
+
+    /**
+     * Converter from an int to a [OutOfQuotaPolicy].
+     *
+     * @param value The input integer
+     * @return An [OutOfQuotaPolicy]
+     */
+    @JvmStatic
+    @TypeConverter
+    fun intToOutOfQuotaPolicy(value: Int): OutOfQuotaPolicy {
+        return when (value) {
+            OutOfPolicyIds.RUN_AS_NON_EXPEDITED_WORK_REQUEST ->
+                OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST
+            OutOfPolicyIds.DROP_WORK_REQUEST -> OutOfQuotaPolicy.DROP_WORK_REQUEST
+            else -> throw IllegalArgumentException("Could not convert $value to OutOfQuotaPolicy")
+        }
+    }
+
+    /**
+     * Converts a list of [ContentUriTriggers.Trigger]s to byte array representation
+     * @param triggers the list of [ContentUriTriggers.Trigger]s to convert
+     * @return corresponding byte array representation
+     */
+    @JvmStatic
+    @TypeConverter
+    fun contentUriTriggersToByteArray(triggers: ContentUriTriggers): ByteArray? {
+        if (triggers.size() == 0) {
+            return null
+        }
+        val outputStream = ByteArrayOutputStream()
+        outputStream.use {
+            ObjectOutputStream(outputStream).use { objectOutputStream ->
+                objectOutputStream.writeInt(triggers.size())
+                for (trigger in triggers.triggers) {
+                    objectOutputStream.writeUTF(trigger.uri.toString())
+                    objectOutputStream.writeBoolean(trigger.shouldTriggerForDescendants())
+                }
+            }
+        }
+
+        return outputStream.toByteArray()
+    }
+
+    /**
+     * Converts a byte array to list of [ContentUriTriggers.Trigger]s
+     * @param bytes byte array representation to convert
+     * @return list of [ContentUriTriggers.Trigger]s
+     */
+    @JvmStatic
+    @TypeConverter
+    fun byteArrayToContentUriTriggers(bytes: ByteArray?): ContentUriTriggers {
+        val triggers = ContentUriTriggers()
+        if (bytes == null) {
+            // bytes will be null if there are no Content Uri Triggers
+            return triggers
+        }
+        val inputStream = ByteArrayInputStream(bytes)
+        inputStream.use {
+            try {
+                ObjectInputStream(inputStream).use { objectInputStream ->
+                    repeat(objectInputStream.readInt()) {
+                        val uri = Uri.parse(objectInputStream.readUTF())
+                        val triggersForDescendants = objectInputStream.readBoolean()
+                        triggers.add(uri, triggersForDescendants)
+                    }
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+        return triggers
+    }
+}
\ No newline at end of file
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/package-info.java b/work/work-runtime/src/main/java/androidx/work/impl/package-info.java
new file mode 100644
index 0000000..3f9ab0d
--- /dev/null
+++ b/work/work-runtime/src/main/java/androidx/work/impl/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+package androidx.work.impl;
+
+import androidx.annotation.RestrictTo;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
index dc71fce..994330f 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/IdGenerator.java
@@ -18,7 +18,7 @@
 
 import static android.content.Context.MODE_PRIVATE;
 
-import static androidx.work.impl.WorkDatabaseMigrations.INSERT_PREFERENCE;
+import static androidx.work.impl.utils.PreferenceUtils.INSERT_PREFERENCE;
 
 import android.content.Context;
 import android.content.SharedPreferences;
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
index 337f05a..02ea9f9 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/PreferenceUtils.java
@@ -18,7 +18,6 @@
 
 import static android.content.Context.MODE_PRIVATE;
 
-import static androidx.work.impl.WorkDatabaseMigrations.INSERT_PREFERENCE;
 
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -39,6 +38,14 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public class PreferenceUtils {
+    public static final String INSERT_PREFERENCE =
+            "INSERT OR REPLACE INTO `Preference`"
+                    + " (`key`, `long_value`) VALUES"
+                    + " (@key, @long_value)";
+
+    public static final String CREATE_PREFERENCE =
+            "CREATE TABLE IF NOT EXISTS `Preference` (`key` TEXT NOT NULL, `long_value` INTEGER, "
+                    + "PRIMARY KEY(`key`))";
 
     // For migration
     public static final String PREFERENCES_FILE_NAME = "androidx.work.util.preferences";