Merge "Merge SimpleCSP and FakeCaptureSequenceProcessor into a single class" into androidx-main
diff --git a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
index 65975e0..d8fe4cd 100644
--- a/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
+++ b/benchmark/baseline-profile-gradle-plugin/lint-baseline.xml
@@ -4,69 +4,6 @@
     <issue
         id="GradleProjectIsolation"
         message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        project.properties[&quot;androidx.benchmark.test.maxagpversion&quot;]?.let { str ->"
-        errorLine2="                ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        it in project.properties &amp;&amp; project.properties[it].toString().toBoolean()"
-        errorLine2="                      ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        it in project.properties &amp;&amp; project.properties[it].toString().toBoolean()"
-        errorLine2="                                            ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        project.properties.containsKey(PROP_SKIP_GENERATION)"
-        errorLine2="                ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)"
-        errorLine2="                ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        !project.properties.containsKey(PROP_DONT_DISABLE_RULES)"
-        errorLine2="                 ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)"
-        errorLine2="                 ~~~~~~~~~~">
-        <location
-            file="src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt"/>
-    </issue>
-
-    <issue
-        id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
         errorLine1="                    project.properties.filterKeys { k ->"
         errorLine2="                            ~~~~~~~~~~">
         <location
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
index 239eff6..aa184ec 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/producer/BaselineProfileProducerPlugin.kt
@@ -79,16 +79,16 @@
     private val baselineProfileExtension = BaselineProfileProducerExtension.register(project)
     private val configurationManager = ConfigurationManager(project)
     private val shouldSkipGeneration by lazy {
-        project.properties.containsKey(PROP_SKIP_GENERATION)
+        project.providers.gradleProperty(PROP_SKIP_GENERATION).isPresent
     }
     private val forceOnlyConnectedDevices: Boolean by lazy {
-        project.properties.containsKey(PROP_FORCE_ONLY_CONNECTED_DEVICES)
+        project.providers.gradleProperty(PROP_FORCE_ONLY_CONNECTED_DEVICES).isPresent
     }
     private val addEnabledRulesInstrumentationArgument by lazy {
-        !project.properties.containsKey(PROP_DONT_DISABLE_RULES)
+        !project.providers.gradleProperty(PROP_DONT_DISABLE_RULES).isPresent
     }
     private val addTargetPackageNameInstrumentationArgument by lazy {
-        !project.properties.containsKey(PROP_SEND_TARGET_PACKAGE_NAME)
+        !project.providers.gradleProperty(PROP_SEND_TARGET_PACKAGE_NAME).isPresent
     }
 
     // This maps all the extended build types to the original ones. Note that release does not
diff --git a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
index cbd7fa7..29f7c75 100644
--- a/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
+++ b/benchmark/baseline-profile-gradle-plugin/src/main/kotlin/androidx/baselineprofile/gradle/utils/AgpPlugin.kt
@@ -51,8 +51,9 @@
 
     // Properties that can be specified by cmd line using -P<property_name> when invoking gradle.
     val testMaxAgpVersion by lazy {
-        project.properties["androidx.benchmark.test.maxagpversion"]?.let { str ->
-            val parts = str.toString().split(".").map { it.toInt() }
+        project.providers.gradleProperty("androidx.benchmark.test.maxagpversion").orNull?.let { str
+            ->
+            val parts = str.split(".").map { it.toInt() }
             return@lazy AndroidPluginVersion(parts[0], parts[1], parts[2])
         } ?: return@lazy null
     }
diff --git a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
index 9ce75c2..42b07bb 100644
--- a/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
+++ b/benchmark/benchmark-darwin-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
 
     <issue
         id="GradleProjectIsolation"
@@ -11,6 +11,15 @@
     </issue>
 
     <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                            project.rootProject.projectDir, // frameworks/support"
+        errorLine2="                                    ~~~~~~~~~~~">
+        <location
+            file="src/main/kotlin/androidx/benchmark/darwin/gradle/DarwinBenchmarkPlugin.kt"/>
+    </issue>
+
+    <issue
         id="WithTypeWithoutConfigureEach"
         message="Avoid passing a closure to withType, use withType().configureEach instead"
         errorLine1="        project.plugins.withType(KotlinMultiplatformPluginWrapper::class.java) {"
diff --git a/benchmark/benchmark-junit4/proguard-rules.pro b/benchmark/benchmark-junit4/proguard-rules.pro
index 544c831..621d897 100644
--- a/benchmark/benchmark-junit4/proguard-rules.pro
+++ b/benchmark/benchmark-junit4/proguard-rules.pro
@@ -18,6 +18,7 @@
 
 ## needed for listeners instantiated by reflection (e.g. InstrumentationResultsRunListener)
 -keepclasseswithmembers class * extends androidx.test.internal.runner.listener.InstrumentationRunListener { *; }
+-keepclasseswithmembers class * extends org.junit.runner.notification.RunListener { *; }
 
 ## Needed due to b/328649293 - shouldn't be needed since they're ref'd by manifest
 ## May need to leave these in place long term to account for old gradle versions
diff --git a/benchmark/gradle-plugin/lint-baseline.xml b/benchmark/gradle-plugin/lint-baseline.xml
index 4363bf5..8c91fdc 100644
--- a/benchmark/gradle-plugin/lint-baseline.xml
+++ b/benchmark/gradle-plugin/lint-baseline.xml
@@ -3,27 +3,36 @@
 
     <issue
         id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="            if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {"
-        errorLine2="                         ~~~~~~~~~~~~">
+        message="Avoid using method getRootProject"
+        errorLine1="        if (!project.rootProject.tasks.exists(&quot;lockClocks&quot;)) {"
+        errorLine2="                     ~~~~~~~~~~~">
         <location
             file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
     </issue>
 
     <issue
         id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of findProperty"
-        errorLine1="                    project.findProperty(&quot;androidx.benchmark.lockClocks.cores&quot;)?.toString() ?: &quot;&quot;"
-        errorLine2="                            ~~~~~~~~~~~~">
+        message="Avoid using method getRootProject"
+        errorLine1="            project.rootProject.tasks.register(&quot;lockClocks&quot;, LockClocksTask::class.java).configure {"
+        errorLine2="                    ~~~~~~~~~~~">
         <location
             file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
     </issue>
 
     <issue
         id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="                if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {"
-        errorLine2="                             ~~~~~~~~~~">
+        message="Avoid using method getRootProject"
+        errorLine1="        if (!project.rootProject.tasks.exists(&quot;unlockClocks&quot;)) {"
+        errorLine2="                     ~~~~~~~~~~~">
+        <location
+            file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            project.rootProject.tasks"
+        errorLine2="                    ~~~~~~~~~~~">
         <location
             file="src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt"/>
     </issue>
diff --git a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
index a7bd8e3..8b2eb89 100644
--- a/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
+++ b/benchmark/gradle-plugin/src/main/kotlin/androidx/benchmark/gradle/BenchmarkPlugin.kt
@@ -101,14 +101,19 @@
         extension.buildTypes.named(testBuildType).configure { it.isDefault = true }
 
         if (
-            !project.rootProject.hasProperty("android.injected.invoked.from.ide") &&
+            !project.providers.gradleProperty("android.injected.invoked.from.ide").isPresent &&
                 !testInstrumentationArgs.containsKey("androidx.benchmark.output.enable")
         ) {
             // NOTE: This argument is checked by ResultWriter to enable CI reports.
             defaultConfig.testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] =
                 "true"
 
-            if (!project.findProperty(ADDITIONAL_TEST_OUTPUT_KEY).toString().toBoolean()) {
+            if (
+                !project.providers
+                    .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+                    .getOrElse("false")
+                    .toBoolean()
+            ) {
                 defaultConfig.testInstrumentationRunnerArguments["no-isolated-storage"] = "1"
             }
         }
@@ -119,7 +124,9 @@
             project.rootProject.tasks.register("lockClocks", LockClocksTask::class.java).configure {
                 it.adbPath.set(adbPathProvider)
                 it.coresArg.set(
-                    project.findProperty("androidx.benchmark.lockClocks.cores")?.toString() ?: ""
+                    project.providers
+                        .gradleProperty("androidx.benchmark.lockClocks.cores")
+                        .orElse("")
                 )
             }
         }
@@ -159,7 +166,12 @@
                     project.layout.buildDirectory.dir(
                         "outputs/connected_android_test_additional_output"
                     )
-                if (!project.properties[ADDITIONAL_TEST_OUTPUT_KEY].toString().toBoolean()) {
+                if (
+                    !project.providers
+                        .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
+                        .getOrElse("false")
+                        .toBoolean()
+                ) {
                     // Only enable pulling benchmark data through this plugin on older versions of
                     // AGP that do not yet enable this flag.
                     project.tasks
diff --git a/buildSrc-tests/lint-baseline.xml b/buildSrc-tests/lint-baseline.xml
index 235650f..656e12c 100644
--- a/buildSrc-tests/lint-baseline.xml
+++ b/buildSrc-tests/lint-baseline.xml
@@ -138,6 +138,69 @@
 
     <issue
         id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            val extensions = project.rootProject.extensions"
+        errorLine2="                                     ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/dependencyTracker/AffectedModuleDetector.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                val compilerProject = project.rootProject.resolveProject(&quot;:compose&quot;)"
+        errorLine2="                                              ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            project.rootProject.tasks.named(zipComposeMetricsTaskName).configure { zipTask ->"
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            project.rootProject.tasks.named(zipComposeReportsTaskName).configure { zipTask ->"
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    return project.rootProject.layout.buildDirectory"
+        errorLine2="                   ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    return project.rootProject.layout.buildDirectory"
+        errorLine2="                   ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXComposeImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    return File(rootProject.projectDir, &quot;../../external&quot;).canonicalFile"
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/AndroidXConfig.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
         message="Use providers.gradleProperty instead of getProperties"
         errorLine1="    for (propertyName in project.properties.keys) {"
         errorLine2="                                 ~~~~~~~~~~">
@@ -147,11 +210,146 @@
 
     <issue
         id="GradleProjectIsolation"
-        message="Use providers.gradleProperty instead of getProperties"
-        errorLine1="        if (properties.containsKey(&quot;android.injected.invoked.from.ide&quot;)) {"
-        errorLine2="            ~~~~~~~~~~">
+        message="Avoid using method getRootProject"
+        errorLine1="                        AndroidXPlaygroundRootImplPlugin.projectOrArtifact(rootProject, this)"
+        errorLine2="                                                                           ~~~~~~~~~~~">
         <location
-            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXRootImplPlugin.kt"/>
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        project.rootProject.tasks.named(&quot;createModuleInfo&quot;).configure {"
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="                    allProjectsExist || findProject(otherGradlePath) != null"
+        errorLine2="                                        ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                    project.rootProject.rootDir == project.getSupportRootFolder()"
+        errorLine2="                            ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.extensions.findByType&lt;NodeJsRootExtension>()?.version = getVersionByName(&quot;node&quot;)"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.extensions.findByType(YarnRootExtension::class.java)?.let {"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        rootProject.tasks.register(&quot;createYarnRcFile&quot;, CreateYarnRcFileTask::class.java) {"
+        errorLine2="        ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            it.yarnrcFile.set(rootProject.layout.buildDirectory.file(&quot;js/.yarnrc&quot;))"
+        errorLine2="                              ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.tasks.withType&lt;KotlinNpmInstallTask>().configureEach {"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXMultiplatformExtension.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="            val requested = rootProject.findProject(path)"
+        errorLine2="                                        ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                project.rootProject.tasks.named(NAME).configure { it.dependsOn(task) }"
+        errorLine2="                        ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/AndroidXPlaygroundRootImplPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.layout.buildDirectory.dir(&quot;test-xml-configs&quot;)"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.layout.buildDirectory.dir(&quot;privacysandbox-files&quot;)"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    val actualRootProject = if (project.isRoot) project else project.rootProject"
+        errorLine2="                                                                     ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/BuildServerConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.tasks.named(CREATE_AGGREGATE_BUILD_INFO_FILES_TASK).configure {"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/buildInfo/CreateAggregateLibraryBuildInfoFileTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    get() = this == rootProject"
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/gradle/Extensions.kt"/>
     </issue>
 
     <issue
@@ -174,6 +372,15 @@
 
     <issue
         id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                    rootProject.tasks.named(GLOBAL_TASK_NAME).configure { task ->"
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/FilteredAnchorTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
         message="Use providers.gradleProperty instead of getProperties"
         errorLine1="            task.pathPrefix = properties[PROP_PATH_PREFIX] as String"
         errorLine2="                              ~~~~~~~~~~">
@@ -191,6 +398,213 @@
     </issue>
 
     <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            it.parameters.workingDir.set(rootProject.layout.projectDirectory)"
+        errorLine2="                                         ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/gitclient/GitClient.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getParent"
+        errorLine1="                    ${project.parent}."
+        errorLine2="                              ~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/java/JavaCompileInputs.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        val rootBaseDir = if (compilerDaemonDisabled) projectDir else rootProject.projectDir"
+        errorLine2="                                                                      ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/KonanPrebuiltsSetup.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="    return project.rootProject.findProject(path)"
+        errorLine2="                               ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    return project.rootProject.findProject(path)"
+        errorLine2="                   ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="        project.rootProject.findProject(&quot;:lint:lint-gradle&quot;)?.let {"
+        errorLine2="                            ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        project.rootProject.findProject(&quot;:lint:lint-gradle&quot;)?.let {"
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/LintConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        val tasksByOutput = project.rootProject.findAllTasksByOutput()"
+        errorLine2="                                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ListTaskOutputsTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="            project.findProject(projectPath)?.plugins?.let { plugins ->"
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/MavenUploadHelper.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        project.rootProject.gradle.sharedServices.registerIfAbsent("
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/ProjectParser.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)"
+        errorLine2="                                   ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            regenerate(project.rootProject, groupId, artifactId, version, location)"
+        errorLine2="                               ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/metalava/RegenerateOldApisTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="            project.rootProject.maybeRegister("
+        errorLine2="                    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="                        project.rootProject.getRepositoryDirectory()"
+        errorLine2="                                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        return project.rootProject.maybeRegister("
+        errorLine2="                       ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/Release.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        val localPropsFile = rootProject.projectDir.resolve(&quot;local.properties&quot;)"
+        errorLine2="                             ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkHelper.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        project.rootProject.rootDir.toRelativeString(project.projectDir)"
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*3}/androidx/build/SdkResourceGenerator.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        (project.rootProject.extensions.extraProperties).let { it.get(&quot;supportRootFolder&quot;) as File }"
+        errorLine2="                 ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/studio/StudioTask.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.tasks"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.tasks.named&lt;ModuleInfoGenerator>(&quot;createModuleInfo&quot;).configure {"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getParent"
+        errorLine1="    val parentProject = project.parent!!"
+        errorLine2="                                ~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="        project.rootProject.tasks.findByName(FINALIZE_TEST_CONFIGS_WITH_APKS_TASK)!!.dependsOn(task)"
+        errorLine2="                ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    rootProject.tasks.named&lt;ModuleInfoGenerator>(&quot;createModuleInfo&quot;).configure {"
+        errorLine2="    ~~~~~~~~~~~">
+        <location
+            file="${:buildSrc-tests*main*MAIN*sourceProvider*0*javaDir*4}/androidx/build/testConfiguration/TestSuiteConfiguration.kt"/>
+    </issue>
+
+    <issue
         id="InternalAgpApiUsage"
         message="Avoid using internal Android Gradle Plugin APIs"
         errorLine1="import com.android.build.gradle.internal.lint.AndroidLintAnalysisTask"
@@ -382,8 +796,8 @@
     <issue
         id="WithPluginClasspathUsage"
         message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
-        errorLine1="            .withPluginClasspath()"
-        errorLine2="             ~~~~~~~~~~~~~~~~~~~">
+        errorLine1="                .withPluginClasspath()"
+        errorLine2="                 ~~~~~~~~~~~~~~~~~~~">
         <location
             file="src/test/java/androidx/build/buildInfo/CreateLibraryBuildInfoFileTaskTest.kt"/>
     </issue>
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
index e777875..ba8881f 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXRootImplPlugin.kt
@@ -61,7 +61,7 @@
 
         // If we're running inside Studio, validate the Android Gradle Plugin version.
         val expectedAgpVersion = System.getenv("EXPECTED_AGP_VERSION")
-        if (properties.containsKey("android.injected.invoked.from.ide")) {
+        if (providers.gradleProperty("android.injected.invoked.from.ide").isPresent) {
             if (expectedAgpVersion != ANDROID_GRADLE_PLUGIN_VERSION) {
                 throw GradleException(
                     """
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
index f632090..73aca82 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/FilteredAnchorTask.kt
@@ -62,7 +62,10 @@
  * match the requested path prefix and task name.
  */
 internal fun Project.addFilterableTasks(vararg taskProviders: TaskProvider<*>?) {
-    if (hasProperty(PROP_PATH_PREFIX) && hasProperty(PROP_TASK_NAME)) {
+    if (
+        providers.gradleProperty(PROP_PATH_PREFIX).isPresent &&
+            providers.gradleProperty(PROP_TASK_NAME).isPresent
+    ) {
         val pathPrefixes = (properties[PROP_PATH_PREFIX] as String).split(",")
         if (pathPrefixes.any { pathPrefix -> relativePathForFiltering().startsWith(pathPrefix) }) {
             val taskName = properties[PROP_TASK_NAME] as String
@@ -84,7 +87,10 @@
  * -Pandroidx.taskName=checkApi -Pandroidx.pathPrefix=core/core/
  */
 internal fun Project.maybeRegisterFilterableTask() {
-    if (hasProperty(PROP_TASK_NAME) && hasProperty(PROP_PATH_PREFIX)) {
+    if (
+        providers.gradleProperty(PROP_TASK_NAME).isPresent &&
+            providers.gradleProperty(PROP_PATH_PREFIX).isPresent
+    ) {
         tasks.register(GLOBAL_TASK_NAME, FilteredAnchorTask::class.java) { task ->
             task.pathPrefix = properties[PROP_PATH_PREFIX] as String
             task.taskName = properties[PROP_TASK_NAME] as String
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 b6e55c4..eace746 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
@@ -30,7 +30,6 @@
 import androidx.camera.camera2.pipe.integration.impl.StillCaptureRequestControl
 import androidx.camera.camera2.pipe.integration.impl.TorchControl
 import androidx.camera.camera2.pipe.integration.impl.UseCaseCamera
-import androidx.camera.camera2.pipe.integration.impl.UseCaseThreads
 import androidx.camera.camera2.pipe.integration.impl.ZoomControl
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
@@ -49,8 +48,8 @@
 import androidx.camera.core.impl.utils.futures.Futures
 import com.google.common.util.concurrent.ListenableFuture
 import javax.inject.Inject
+import kotlinx.coroutines.CompletableDeferred
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.async
 
 /**
  * Adapt the [CameraControlInternal] interface to [CameraPipe].
@@ -71,7 +70,6 @@
     private val focusMeteringControl: FocusMeteringControl,
     private val stillCaptureRequestControl: StillCaptureRequestControl,
     private val torchControl: TorchControl,
-    private val threads: UseCaseThreads,
     private val zoomControl: ZoomControl,
     private val zslControl: ZslControl,
     public val camera2cameraControl: Camera2CameraControl,
@@ -112,11 +110,10 @@
 
     override fun cancelFocusAndMetering(): ListenableFuture<Void> {
         return Futures.nonCancellationPropagating(
-            threads.sequentialScope
-                .async {
-                    focusMeteringControl.cancelFocusAndMeteringAsync().join()
+            CompletableDeferred<Void?>()
+                .also {
                     // Convert to null once the task is done, ignore the results.
-                    return@async null
+                    focusMeteringControl.cancelFocusAndMeteringAsync().propagateTo(it) { null }
                 }
                 .asListenableFuture()
         )
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
index 76d7012..5353105 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapters.kt
@@ -74,21 +74,48 @@
     return CallbackToFutureAdapter.getFuture(resolver)
 }
 
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ */
 public fun <T> Deferred<T>.propagateTo(destination: CompletableDeferred<T>) {
-    invokeOnCompletion { propagateOnceTo(destination, it) }
+    invokeOnCompletion { propagateCompletion(destination, it) }
 }
 
-@OptIn(ExperimentalCoroutinesApi::class)
-public fun <T> Deferred<T>.propagateOnceTo(
-    destination: CompletableDeferred<T>,
-    throwable: Throwable?,
+/**
+ * Propagates the result of this to `destination` parameter when this deferred is completed.
+ *
+ * Cancelling the destination is no-op returned from this function does not cancel the `Deferred`
+ * returned by `block`.
+ *
+ * @param destination The destination [CompletableDeferred] to which result is propagated to.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+public fun <T, R> Deferred<T>.propagateTo(
+    destination: CompletableDeferred<R>,
+    transform: (T) -> R,
 ) {
-    if (throwable != null) {
-        if (throwable is CancellationException) {
-            destination.cancel(throwable)
-        } else {
-            destination.completeExceptionally(throwable)
-        }
+    invokeOnCompletion { propagateCompletion(destination, it, transform) }
+}
+
+/**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ *   `Deferred.invokeOnCompletion`.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T> Deferred<T>.propagateCompletion(
+    destination: CompletableDeferred<T>,
+    completionCause: Throwable?,
+) {
+    if (completionCause != null) {
+        destination.completeFailing(completionCause)
     } else {
         // Ignore exceptions - This should never throw in this situation.
         destination.complete(getCompleted())
@@ -96,6 +123,46 @@
 }
 
 /**
+ * Propagates the result of this to `destination` parameter immediately.
+ *
+ * This function assumes that [Deferred.invokeOnCompletion] has already been invoked.
+ *
+ * @param destination The destination `Deferred` to which result is propagated to.
+ * @param completionCause The `Throwable` cause of completion that was passed in
+ *   `Deferred.invokeOnCompletion`.
+ * @param transform Transformation function to convert the result during propagation.
+ */
+@OptIn(ExperimentalCoroutinesApi::class)
+public fun <T, R> Deferred<T>.propagateCompletion(
+    destination: CompletableDeferred<R>,
+    completionCause: Throwable?,
+    transform: (T) -> R,
+) {
+    if (completionCause != null) {
+        destination.completeFailing(completionCause)
+    } else {
+        // Ignore exceptions - This should never throw in this situation.
+        destination.complete(transform(getCompleted()))
+    }
+}
+
+/**
+ * Completes this `Deferred` as failure based on the provided `cause`.
+ *
+ * @param cause If it's an instance of [CancellationException], [Deferred.cancel] is invoked for
+ *   this, otherwise, [CompletableDeferred.completeExceptionally] is invoked.
+ */
+public fun <T> CompletableDeferred<T>.completeFailing(
+    cause: Throwable,
+) {
+    if (cause is CancellationException) {
+        cancel(cause)
+    } else {
+        completeExceptionally(cause)
+    }
+}
+
+/**
  * Waits for [Deferred.await] to be completed until the given timeout.
  *
  * @return true if `Deferred.await` had completed, false otherwise.
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
index 82da040..2080cb8 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/MeteringRepeating.kt
@@ -45,6 +45,7 @@
 import androidx.camera.core.impl.UseCaseConfigFactory
 import androidx.camera.core.impl.UseCaseConfigFactory.CaptureType
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
+import androidx.camera.core.internal.TargetConfig.OPTION_TARGET_NAME
 import kotlin.math.min
 
 private val DEFAULT_PREVIEW_SIZE = Size(0, 0)
@@ -203,6 +204,7 @@
                     OPTION_SESSION_CONFIG_UNPACKER,
                     CameraUseCaseAdapter.DefaultSessionOptionsUnpacker
                 )
+                insertOption(OPTION_TARGET_NAME, "MeteringRepeating")
                 insertOption(OPTION_CAPTURE_TYPE, CaptureType.METERING_REPEATING)
             }
 
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
index 7ffef5d..94f03ea 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/StillCaptureRequestControl.kt
@@ -19,7 +19,7 @@
 import androidx.annotation.GuardedBy
 import androidx.camera.camera2.pipe.core.Log.debug
 import androidx.camera.camera2.pipe.integration.adapter.asListenableFuture
-import androidx.camera.camera2.pipe.integration.adapter.propagateOnceTo
+import androidx.camera.camera2.pipe.integration.adapter.propagateCompletion
 import androidx.camera.camera2.pipe.integration.config.CameraScope
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
@@ -197,7 +197,7 @@
                     }
                 }
             } else {
-                propagateOnceTo(submittedRequest.result, cause)
+                propagateCompletion(submittedRequest.result, cause)
             }
         }
     }
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index 54adf25..cedc7c4 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -17,6 +17,7 @@
 package androidx.camera.camera2.pipe.integration.impl
 
 import android.content.Context
+import android.graphics.ImageFormat
 import android.hardware.camera2.CameraCharacteristics
 import android.hardware.camera2.CameraDevice.TEMPLATE_PREVIEW
 import android.hardware.camera2.CaptureRequest
@@ -55,23 +56,30 @@
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraComponent
 import androidx.camera.camera2.pipe.integration.config.UseCaseCameraConfig
 import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
+import androidx.camera.camera2.pipe.integration.internal.DynamicRangeResolver
 import androidx.camera.camera2.pipe.integration.interop.Camera2CameraControl
 import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
 import androidx.camera.core.DynamicRange
+import androidx.camera.core.ImageCapture
 import androidx.camera.core.MirrorMode
+import androidx.camera.core.Preview
 import androidx.camera.core.UseCase
+import androidx.camera.core.impl.AttachedSurfaceInfo
 import androidx.camera.core.impl.CameraControlInternal
 import androidx.camera.core.impl.CameraInfoInternal
 import androidx.camera.core.impl.CameraInternal
 import androidx.camera.core.impl.CameraMode
 import androidx.camera.core.impl.CaptureConfig
 import androidx.camera.core.impl.DeferrableSurface
-import androidx.camera.core.impl.PreviewConfig
+import androidx.camera.core.impl.MutableOptionsBundle
 import androidx.camera.core.impl.SessionConfig
 import androidx.camera.core.impl.SessionConfig.OutputConfig.SURFACE_GROUP_ID_NONE
 import androidx.camera.core.impl.SessionConfig.ValidatingBuilder
 import androidx.camera.core.impl.SessionProcessor
+import androidx.camera.core.impl.SurfaceConfig
 import androidx.camera.core.impl.stabilization.StabilizationMode
+import androidx.camera.core.streamsharing.StreamSharing
+import androidx.camera.core.streamsharing.StreamSharingConfig
 import javax.inject.Inject
 import javax.inject.Provider
 import kotlinx.coroutines.Deferred
@@ -171,6 +179,8 @@
         )
     }
 
+    private val dynamicRangeResolver = DynamicRangeResolver(cameraProperties.metadata)
+
     @Volatile private var _activeComponent: UseCaseCameraComponent? = null
     public val camera: UseCaseCamera?
         get() = _activeComponent?.getUseCaseCamera()
@@ -602,7 +612,7 @@
             return activeSurfaces > 0 &&
                 with(attachedUseCases.withoutMetering()) {
                     (onlyVideoCapture() || requireMeteringRepeating()) &&
-                        supportMeteringCombination()
+                        isMeteringCombinationSupported()
                 }
         }
         return false
@@ -624,7 +634,7 @@
             return activeSurfaces == 0 ||
                 with(attachedUseCases.withoutMetering()) {
                     !(onlyVideoCapture() || requireMeteringRepeating()) ||
-                        !supportMeteringCombination()
+                        !isMeteringCombinationSupported()
                 }
         }
         return false
@@ -664,46 +674,133 @@
             }
     }
 
-    private fun Collection<UseCase>.supportMeteringCombination(): Boolean {
-        val useCases = this.toMutableList().apply { add(meteringRepeating) }
+    private fun Collection<UseCase>.isMeteringCombinationSupported(): Boolean {
         if (meteringRepeating.attachedSurfaceResolution == null) {
             meteringRepeating.setupSession()
         }
-        return isCombinationSupported(useCases).also {
-            Log.debug { "Combination of $useCases is supported: $it" }
+
+        val attachedSurfaceInfoList = getAttachedSurfaceInfoList()
+
+        if (attachedSurfaceInfoList.isEmpty()) {
+            return false
         }
+
+        val sessionSurfacesConfigs = getSessionSurfacesConfigs()
+
+        return supportedSurfaceCombination
+            .checkSupported(
+                SupportedSurfaceCombination.FeatureSettings(
+                    CameraMode.DEFAULT,
+                    getRequiredMaxBitDepth(attachedSurfaceInfoList),
+                    isPreviewStabilizationOn(),
+                    isUltraHdrOn()
+                ),
+                mutableListOf<SurfaceConfig>().apply {
+                    addAll(sessionSurfacesConfigs)
+                    add(createMeteringRepeatingSurfaceConfig())
+                }
+            )
+            .also {
+                Log.debug {
+                    "Combination of $sessionSurfacesConfigs + $meteringRepeating is supported: $it"
+                }
+            }
     }
 
-    private fun isCombinationSupported(currentUseCases: Collection<UseCase>): Boolean {
-        val surfaceConfigs =
-            currentUseCases.map { useCase ->
-                // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution
-                // is
-                //  implemented.
-                supportedSurfaceCombination.transformSurfaceConfig(
-                    CameraMode.DEFAULT,
-                    useCase.imageFormat,
-                    useCase.attachedSurfaceResolution!!
+    private fun getRequiredMaxBitDepth(attachedSurfaceInfoList: List<AttachedSurfaceInfo>): Int {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            dynamicRangeResolver
+                .resolveAndValidateDynamicRanges(
+                    attachedSurfaceInfoList,
+                    listOf(meteringRepeating.currentConfig),
+                    listOf(0)
+                )
+                .forEach { (_, u) ->
+                    if (u.bitDepth == DynamicRange.BIT_DEPTH_10_BIT) {
+                        return DynamicRange.BIT_DEPTH_10_BIT
+                    }
+                }
+        }
+
+        return DynamicRange.BIT_DEPTH_8_BIT
+    }
+
+    private fun Collection<UseCase>.getAttachedSurfaceInfoList(): List<AttachedSurfaceInfo> =
+        mutableListOf<AttachedSurfaceInfo>().apply {
+            [email protected] { useCase ->
+                val surfaceResolution = useCase.attachedSurfaceResolution
+                val streamSpec = useCase.attachedStreamSpec
+
+                // When collecting the info, the UseCases might be unbound to make these info
+                // become null.
+                if (surfaceResolution == null || streamSpec == null) {
+                    Log.warn { "Invalid surface resolution or stream spec is found." }
+                    clear()
+                    return@apply
+                }
+
+                val surfaceConfig =
+                    supportedSurfaceCombination.transformSurfaceConfig(
+                        // TODO: Test with correct Camera Mode when concurrent mode / ultra high
+                        // resolution is implemented.
+                        CameraMode.DEFAULT,
+                        useCase.currentConfig.inputFormat,
+                        surfaceResolution
+                    )
+                add(
+                    AttachedSurfaceInfo.create(
+                        surfaceConfig,
+                        useCase.currentConfig.inputFormat,
+                        surfaceResolution,
+                        streamSpec.dynamicRange,
+                        useCase.getCaptureTypes(),
+                        streamSpec.implementationOptions ?: MutableOptionsBundle.create(),
+                        useCase.currentConfig.getTargetFrameRate(null)
+                    )
                 )
             }
+        }
 
-        var isPreviewStabilizationOn = false
-        for (useCase in currentUseCases) {
-            if (useCase.currentConfig is PreviewConfig) {
-                isPreviewStabilizationOn =
-                    useCase.currentConfig.previewStabilizationMode == StabilizationMode.ON
+    private fun UseCase.getCaptureTypes() =
+        if (this is StreamSharing) {
+            (currentConfig as StreamSharingConfig).captureTypes
+        } else {
+            listOf(currentConfig.captureType)
+        }
+
+    private fun Collection<UseCase>.isPreviewStabilizationOn() =
+        filterIsInstance<Preview>().firstOrNull()?.currentConfig?.previewStabilizationMode ==
+            StabilizationMode.ON
+
+    private fun Collection<UseCase>.isUltraHdrOn() =
+        filterIsInstance<ImageCapture>().firstOrNull()?.currentConfig?.inputFormat ==
+            ImageFormat.JPEG_R
+
+    private fun Collection<UseCase>.getSessionSurfacesConfigs(): List<SurfaceConfig> =
+        mutableListOf<SurfaceConfig>().apply {
+            [email protected] { useCase ->
+                useCase.sessionConfig.surfaces.forEach { deferrableSurface ->
+                    add(
+                        supportedSurfaceCombination.transformSurfaceConfig(
+                            // TODO: Test with correct Camera Mode when concurrent mode / ultra high
+                            // resolution is implemented.
+                            CameraMode.DEFAULT,
+                            useCase.currentConfig.inputFormat,
+                            deferrableSurface.prescribedSize
+                        )
+                    )
+                }
             }
         }
 
-        return supportedSurfaceCombination.checkSupported(
-            SupportedSurfaceCombination.FeatureSettings(
-                CameraMode.DEFAULT,
-                DynamicRange.BIT_DEPTH_8_BIT,
-                isPreviewStabilizationOn
-            ),
-            surfaceConfigs
+    private fun createMeteringRepeatingSurfaceConfig() =
+        supportedSurfaceCombination.transformSurfaceConfig(
+            // TODO: Test with correct Camera Mode when concurrent mode / ultra high resolution is
+            // implemented.
+            CameraMode.DEFAULT,
+            meteringRepeating.imageFormat,
+            meteringRepeating.attachedSurfaceResolution!!
         )
-    }
 
     private fun Collection<UseCase>.surfaceCount(): Int =
         ValidatingBuilder().let { validatingBuilder ->
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
index 9b27a11..28b47b6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/adapter/CoroutineAdapterTest.kt
@@ -46,6 +46,23 @@
     }
 
     @Test
+    fun propagateTransformedCompleteResult(): Unit = runBlocking {
+        // Arrange.
+        val resultValue = 123
+        val resultValueTransformed = resultValue.toString()
+
+        val sourceDeferred = CompletableDeferred<Int>()
+        val resultDeferred = CompletableDeferred<String>()
+        sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+        // Act.
+        sourceDeferred.complete(resultValue)
+
+        // Assert.
+        assertThat(resultDeferred.await()).isEqualTo(resultValueTransformed)
+    }
+
+    @Test
     fun propagateCancelResult() {
         // Arrange.
         val sourceDeferred = CompletableDeferred<Unit>()
@@ -59,6 +76,20 @@
         assertThat(resultDeferred.isCancelled).isTrue()
     }
 
+    @Test
+    fun propagateCancelResult_whenTransformFunctionIsUsed() {
+        // Arrange.
+        val sourceDeferred = CompletableDeferred<Unit>()
+        val resultDeferred = CompletableDeferred<Unit>()
+        sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+
+        // Act.
+        sourceDeferred.cancel()
+
+        // Assert.
+        assertThat(resultDeferred.isCancelled).isTrue()
+    }
+
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun propagateExceptionResult() {
@@ -74,4 +105,20 @@
         // Assert.
         assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
     }
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Test
+    fun propagateExceptionResult_whenTransformFunctionIsUsed() {
+        // Arrange.
+        val sourceDeferred = CompletableDeferred<Unit>()
+        val resultDeferred = CompletableDeferred<Unit>()
+        sourceDeferred.propagateTo(resultDeferred) { res -> res.toString() }
+        val testThrowable = Throwable()
+
+        // Act.
+        sourceDeferred.completeExceptionally(testThrowable)
+
+        // Assert.
+        assertThat(resultDeferred.getCompletionExceptionOrNull()).isSameInstanceAs(testThrowable)
+    }
 }
diff --git a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
index 37d015b..89e2eb6 100644
--- a/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
+++ b/camera/camera-camera2-pipe-integration/src/test/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManagerTest.kt
@@ -228,6 +228,67 @@
     }
 
     @Test
+    fun meteringRepeatingEnabled_whenPreviewEnabledWithNoSurfaceProvider() = runTest {
+        // Arrange
+        initializeUseCaseThreads(this)
+        val useCaseManager = createUseCaseManager()
+        val preview = createPreview(/* withSurfaceProvider= */ false)
+        val imageCapture = createImageCapture()
+        useCaseManager.attach(listOf(preview, imageCapture))
+
+        // Act
+        useCaseManager.activate(preview)
+        useCaseManager.activate(imageCapture)
+
+        // Assert
+        val enabledUseCaseClasses =
+            useCaseManager.getRunningUseCasesForTest().map { it::class.java }
+        assertThat(enabledUseCaseClasses)
+            .containsExactly(
+                Preview::class.java,
+                ImageCapture::class.java,
+                MeteringRepeating::class.java
+            )
+    }
+
+    @Test
+    fun meteringRepeatingNotEnabled_whenImageAnalysisAndPreviewWithNoSurfaceProvider() = runTest {
+        // Arrange
+        initializeUseCaseThreads(this)
+        val useCaseManager = createUseCaseManager()
+        val preview = createPreview(/* withSurfaceProvider= */ false)
+        val imageAnalysis =
+            ImageAnalysis.Builder().build().apply {
+                setAnalyzer(useCaseThreads.backgroundExecutor) { image -> image.close() }
+            }
+        useCaseManager.attach(listOf(preview, imageAnalysis))
+
+        // Act
+        useCaseManager.activate(preview)
+        useCaseManager.activate(imageAnalysis)
+
+        // Assert
+        val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+        assertThat(enabledUseCases).containsExactly(preview, imageAnalysis)
+    }
+
+    @Test
+    fun meteringRepeatingNotEnabled_whenOnlyPreviewWithNoSurfaceProvider() = runTest {
+        // Arrange
+        initializeUseCaseThreads(this)
+        val useCaseManager = createUseCaseManager()
+        val preview = createPreview(/* withSurfaceProvider= */ false)
+        useCaseManager.attach(listOf(preview))
+
+        // Act
+        useCaseManager.activate(preview)
+
+        // Assert
+        val enabledUseCases = useCaseManager.getRunningUseCasesForTest()
+        assertThat(enabledUseCases).containsExactly(preview)
+    }
+
+    @Test
     fun meteringRepeatingEnabled_whenOnlyImageCaptureEnabled() = runTest {
         // Arrange
         initializeUseCaseThreads(this)
@@ -736,16 +797,18 @@
                 useCaseList.add(it)
             }
 
-    private fun createPreview(): Preview =
+    private fun createPreview(withSurfaceProvider: Boolean = true): Preview =
         Preview.Builder()
             .setCaptureOptionUnpacker(CameraUseCaseAdapter.DefaultCaptureOptionsUnpacker.INSTANCE)
             .setSessionOptionUnpacker(CameraUseCaseAdapter.DefaultSessionOptionsUnpacker)
             .build()
             .apply {
-                setSurfaceProvider(
-                    CameraXExecutors.mainThreadExecutor(),
-                    SurfaceTextureProvider.createSurfaceTextureProvider()
-                )
+                if (withSurfaceProvider) {
+                    setSurfaceProvider(
+                        CameraXExecutors.mainThreadExecutor(),
+                        SurfaceTextureProvider.createSurfaceTextureProvider()
+                    )
+                }
             }
             .also {
                 it.simulateActivation()
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
index 9ed683c..5d5ee33 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/CameraBackend.kt
@@ -40,7 +40,7 @@
         }
 
         public class CameraAvailable(public val cameraId: CameraId) : CameraStatus() {
-            override fun toString(): String = "CameraAvailable(camera=$cameraId"
+            override fun toString(): String = "CameraAvailable(camera=$cameraId)"
         }
     }
 }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
index 16a26fd..efe9f68 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Camera2CameraController.kt
@@ -176,7 +176,7 @@
                 ControllerState.ERROR ->
                     if (
                         cameraStatus is CameraStatus.CameraAvailable &&
-                            lastCameraError == CameraError.ERROR_CAMERA_DEVICE
+                            lastCameraError != CameraError.ERROR_GRAPH_CONFIG
                     ) {
                         shouldRestart = true
                     }
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
index 8039abf..f7532e5 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/Exceptions.kt
@@ -55,9 +55,19 @@
     } catch (e: Exception) {
         Log.warn { "Unexpected error: " + e.message }
         when (e) {
+            is CameraAccessException -> {
+                cameraErrorListener.onCameraError(
+                    cameraId,
+                    CameraError.from(e),
+                    // CameraAccessException indicates the task failed because the camera is
+                    // unavailable, such as when the camera is in use or disconnected. Such errors
+                    // can be recovered when the camera becomes available.
+                    willAttemptRetry = true,
+                )
+                return null
+            }
             is IllegalArgumentException,
             is IllegalStateException,
-            is CameraAccessException,
             is SecurityException,
             is UnsupportedOperationException,
             is NullPointerException -> {
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
index 263b7a5..b596010 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/ImageCaptureTest.java
@@ -124,7 +124,7 @@
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
 
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the cancel after the capture request has been successfully submitted
             fakeCameraControl.notifyAllRequestsOnCaptureCancelled();
         });
@@ -154,7 +154,7 @@
                 ImageCapture.OnImageCapturedCallback.class);
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             // Notify the failure after the capture request has been successfully submitted
             fakeCameraControl.notifyAllRequestsOnCaptureFailed();
         });
@@ -302,7 +302,7 @@
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
 
         // Simulates the case that the capture request failed after running in 300 ms.
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             CameraXExecutors.mainThreadExecutor().schedule(() -> {
                 fakeCameraControl.notifyAllRequestsOnCaptureFailed();
             }, 300, TimeUnit.MILLISECONDS);
@@ -395,7 +395,7 @@
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         FakeCameraControl.OnNewCaptureRequestListener mockCaptureRequestListener =
                 mock(FakeCameraControl.OnNewCaptureRequestListener.class);
-        fakeCameraControl.setOnNewCaptureRequestListener(mockCaptureRequestListener);
+        fakeCameraControl.addOnNewCaptureRequestListener(mockCaptureRequestListener);
 
         // Act.
         mInstrumentation.runOnMainSync(
@@ -463,7 +463,7 @@
         FakeCameraControl fakeCameraControl =
                 getCameraControlImplementation(mCameraUseCaseAdapter.getCameraControl());
         CountDownLatch latch = new CountDownLatch(1);
-        fakeCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        fakeCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             latch.countDown();
         });
 
@@ -492,7 +492,7 @@
     private void addExtraFailureNotificationsForRetry(FakeCameraControl cameraControl,
             int retryCount) {
         if (retryCount > 0) {
-            cameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+            cameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
                 addExtraFailureNotificationsForRetry(cameraControl, retryCount - 1);
                 cameraControl.notifyAllRequestsOnCaptureFailed();
             });
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
index a0557a5..94fa6ae 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
+++ b/camera/camera-core/src/main/java/androidx/camera/core/CameraProvider.kt
@@ -18,7 +18,6 @@
 import androidx.annotation.RestrictTo
 import androidx.annotation.RestrictTo.Scope
 import androidx.lifecycle.LifecycleOwner
-import com.google.common.util.concurrent.ListenableFuture
 
 /**
  * A [CameraProvider] provides basic access to a set of cameras such as querying for camera
@@ -92,12 +91,4 @@
     public fun getCameraInfo(cameraSelector: CameraSelector): CameraInfo {
         throw UnsupportedOperationException("The camera provider is not implemented properly.")
     }
-
-    /**
-     * Shuts down the camera provider.
-     *
-     * @return A [ListenableFuture] representing the shutdown status. Cancellation of this future is
-     *   a no-op.
-     */
-    @RestrictTo(Scope.LIBRARY_GROUP) public fun shutdownAsync(): ListenableFuture<Void>
 }
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 20b2ecd..0873c4b 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
@@ -1312,7 +1312,8 @@
 
         if (mTakePictureManager == null) {
             // mTakePictureManager is reused when the Surface is reset.
-            mTakePictureManager = new TakePictureManager(mImageCaptureControl);
+            mTakePictureManager = getCurrentConfig().getTakePictureManagerProvider().newInstance(
+                    mImageCaptureControl);
         }
         mTakePictureManager.setImagePipeline(mImagePipeline);
 
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
index b81133b..62245fd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageProcessingUtil.java
@@ -49,10 +49,11 @@
 public final class ImageProcessingUtil {
 
     private static final String TAG = "ImageProcessingUtil";
+    public static final String JNI_LIB_NAME = "image_processing_util_jni";
     private static int sImageCount = 0;
 
     static {
-        System.loadLibrary("image_processing_util_jni");
+        System.loadLibrary(JNI_LIB_NAME);
     }
 
     enum Result {
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
index d4ab73e..ceec2f8 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/Bitmap2JpegBytes.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.processing.Operation;
 import androidx.camera.core.processing.Packet;
@@ -37,7 +38,8 @@
  *
  * <p>The {@link Bitmap} will be recycled and should not be used after the processing.
  */
-class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class Bitmap2JpegBytes implements Operation<Bitmap2JpegBytes.In, Packet<byte[]>> {
 
     @NonNull
     @Override
@@ -79,16 +81,16 @@
      * Input of {@link Bitmap2JpegBytes} processor.
      */
     @AutoValue
-    abstract static class In {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract static class In {
 
         abstract Packet<Bitmap> getPacket();
 
         abstract int getJpegQuality();
 
         @NonNull
-        static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
+        public static In of(@NonNull Packet<Bitmap> imagePacket, int jpegQuality) {
             return new AutoValue_Bitmap2JpegBytes_In(imagePacket, jpegQuality);
         }
     }
 }
-
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
index 4dccd13..cb378f1 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/JpegBytes2Disk.java
@@ -27,6 +27,7 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.impl.utils.Exif;
@@ -48,7 +49,9 @@
 /**
  * Saves JPEG bytes to disk.
  */
-class JpegBytes2Disk implements Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class JpegBytes2Disk implements
+        Operation<JpegBytes2Disk.In, ImageCapture.OutputFileResults> {
 
     private static final String TEMP_FILE_PREFIX = "CameraX";
     private static final String TEMP_FILE_SUFFIX = ".tmp";
@@ -287,7 +290,8 @@
      * Input packet.
      */
     @AutoValue
-    abstract static class In {
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public abstract static class In {
 
         @NonNull
         abstract Packet<byte[]> getPacket();
@@ -296,7 +300,7 @@
         abstract ImageCapture.OutputFileOptions getOutputFileOptions();
 
         @NonNull
-        static In of(@NonNull Packet<byte[]> jpegBytes,
+        public static In of(@NonNull Packet<byte[]> jpegBytes,
                 @NonNull ImageCapture.OutputFileOptions outputFileOptions) {
             return new AutoValue_JpegBytes2Disk_In(jpegBytes, outputFileOptions);
         }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
index fdf33ed..5b06259 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManager.java
@@ -16,36 +16,15 @@
 
 package androidx.camera.core.imagecapture;
 
-import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
-import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
-import static androidx.camera.core.impl.utils.Threads.checkMainThread;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
-import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
-import static androidx.core.util.Preconditions.checkState;
-
-import static java.util.Objects.requireNonNull;
-
-import android.util.Log;
-
 import androidx.annotation.MainThread;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
-import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Logger;
-import androidx.camera.core.impl.utils.futures.FutureCallback;
-import androidx.camera.core.impl.utils.futures.Futures;
-import androidx.core.util.Pair;
 
 import com.google.auto.value.AutoValue;
-import com.google.common.util.concurrent.ListenableFuture;
 
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
 import java.util.List;
 
 /**
@@ -62,46 +41,13 @@
  *
  * <p>The thread safety is guaranteed by using the main thread.
  */
-public class TakePictureManager implements OnImageCloseListener, TakePictureRequest.RetryControl {
-
-    private static final String TAG = "TakePictureManager";
-
-    // Queue of new requests that have not been sent to the pipeline/camera.
-    @VisibleForTesting
-    final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
-    final ImageCaptureControl mImageCaptureControl;
-    ImagePipeline mImagePipeline;
-
-    // The current request being processed by the camera. Only one request can be processed by
-    // the camera at the same time. Null if the camera is idle.
-    @Nullable
-    private RequestWithCallback mCapturingRequest;
-    // The current requests that have not received a result or an error.
-    private final List<RequestWithCallback> mIncompleteRequests;
-
-    // Once paused, the class waits until the class is resumed to handle new requests.
-    boolean mPaused = false;
-
-    /**
-     * @param imageCaptureControl for controlling {@link ImageCapture}
-     */
-    @MainThread
-    public TakePictureManager(@NonNull ImageCaptureControl imageCaptureControl) {
-        checkMainThread();
-        mImageCaptureControl = imageCaptureControl;
-        mIncompleteRequests = new ArrayList<>();
-    }
-
+public interface TakePictureManager {
     /**
      * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
      * output.
      */
     @MainThread
-    public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
-        checkMainThread();
-        mImagePipeline = imagePipeline;
-        mImagePipeline.setOnImageCloseListener(this);
-    }
+    void setImagePipeline(@NonNull ImagePipeline imagePipeline);
 
     /**
      * Adds requests to the queue.
@@ -109,201 +55,52 @@
      * <p>The requests in the queue will be executed based on the order being added.
      */
     @MainThread
-    public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
-        checkMainThread();
-        mNewRequests.offer(takePictureRequest);
-        issueNextRequest();
-    }
-
-    @MainThread
-    @Override
-    public void retryRequest(@NonNull TakePictureRequest request) {
-        checkMainThread();
-        Logger.d(TAG, "Add a new request for retrying.");
-        // Insert the request to the front of the queue.
-        mNewRequests.addFirst(request);
-        // Try to issue the newly added request in case condition allows.
-        issueNextRequest();
-    }
+    void offerRequest(@NonNull TakePictureRequest takePictureRequest);
 
     /**
      * Pauses sending request to camera.
      */
     @MainThread
-    public void pause() {
-        checkMainThread();
-        mPaused = true;
-
-        // Always retry because the camera may not send an error callback during the reset.
-        if (mCapturingRequest != null) {
-            mCapturingRequest.abortSilentlyAndRetry();
-        }
-    }
+    void pause();
 
     /**
      * Resumes sending request to camera.
      */
     @MainThread
-    public void resume() {
-        checkMainThread();
-        mPaused = false;
-        issueNextRequest();
-    }
+    void resume();
 
     /**
      * Clears the requests queue.
      */
     @MainThread
-    public void abortRequests() {
-        checkMainThread();
-        ImageCaptureException exception =
-                new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
-
-        // Clear pending request first so aborting in-flight request won't trigger another capture.
-        for (TakePictureRequest request : mNewRequests) {
-            request.onError(exception);
-        }
-        mNewRequests.clear();
-
-        // Abort the in-flight request after clearing the pending requests.
-        // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
-        List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
-        for (RequestWithCallback request : requestsSnapshot) {
-            // TODO: optimize the performance by not processing aborted requests.
-            request.abortAndSendErrorToApp(exception);
-        }
-    }
+    void abortRequests();
 
     /**
-     * Issues the next request if conditions allow.
+     * Returns whether any capture request is being processed currently.
      */
-    @MainThread
-    void issueNextRequest() {
-        checkMainThread();
-        Log.d(TAG, "Issue the next TakePictureRequest.");
-        if (hasCapturingRequest()) {
-            Log.d(TAG, "There is already a request in-flight.");
-            return;
-        }
-        if (mPaused) {
-            Log.d(TAG, "The class is paused.");
-            return;
-        }
-        if (mImagePipeline.getCapacity() == 0) {
-            Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
-            return;
-        }
-        TakePictureRequest request = mNewRequests.poll();
-        if (request == null) {
-            Log.d(TAG, "No new request.");
-            return;
-        }
-
-        RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
-        trackCurrentRequests(requestWithCallback);
-
-        // Send requests.
-        Pair<CameraRequest, ProcessingRequest> requests =
-                mImagePipeline.createRequests(request, requestWithCallback,
-                        requestWithCallback.getCaptureFuture());
-        CameraRequest cameraRequest = requireNonNull(requests.first);
-        ProcessingRequest processingRequest = requireNonNull(requests.second);
-        mImagePipeline.submitProcessingRequest(processingRequest);
-        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
-        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
-    }
-
-    /**
-     * Waits for the request to finish before issuing the next.
-     */
-    private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
-        checkState(!hasCapturingRequest());
-        mCapturingRequest = requestWithCallback;
-
-        // Waits for the capture to finish before issuing the next.
-        mCapturingRequest.getCaptureFuture().addListener(() -> {
-            mCapturingRequest = null;
-            issueNextRequest();
-        }, directExecutor());
-
-        // Track all incomplete requests so we can abort them when UseCase is detached.
-        mIncompleteRequests.add(requestWithCallback);
-        requestWithCallback.getCompleteFuture().addListener(() -> {
-            mIncompleteRequests.remove(requestWithCallback);
-        }, directExecutor());
-    }
-
-    /**
-     * Submit a request to camera and post-processing pipeline.
-     *
-     * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
-     */
-    @MainThread
-    private ListenableFuture<Void> submitCameraRequest(
-            @NonNull CameraRequest cameraRequest) {
-        checkMainThread();
-        mImageCaptureControl.lockFlashMode();
-        ListenableFuture<Void> captureRequestFuture =
-                mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
-        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
-            @Override
-            public void onSuccess(@Nullable Void result) {
-                mImageCaptureControl.unlockFlashMode();
-            }
-
-            @Override
-            public void onFailure(@NonNull Throwable throwable) {
-                if (cameraRequest.isAborted()) {
-                    // When the pipeline is recreated, the in-flight request is aborted and
-                    // retried. On legacy devices, the camera may return CancellationException
-                    // for the aborted request which causes the retried request to fail. Return
-                    // early if the request has been aborted.
-                    return;
-                } else {
-                    int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
-                    if (throwable instanceof ImageCaptureException) {
-                        mImagePipeline.notifyCaptureError(
-                                CaptureError.of(requestId, (ImageCaptureException) throwable));
-                    } else {
-                        mImagePipeline.notifyCaptureError(
-                                CaptureError.of(requestId, new ImageCaptureException(
-                                        ERROR_CAPTURE_FAILED,
-                                        "Failed to submit capture request",
-                                        throwable)));
-                    }
-                }
-                mImageCaptureControl.unlockFlashMode();
-            }
-        }, mainThreadExecutor());
-        return captureRequestFuture;
-    }
-
     @VisibleForTesting
-    boolean hasCapturingRequest() {
-        return mCapturingRequest != null;
-    }
+    boolean hasCapturingRequest();
 
+    /**
+     * Returns the capture request being processed currently.
+     */
     @VisibleForTesting
     @Nullable
-    public RequestWithCallback getCapturingRequest() {
-        return mCapturingRequest;
-    }
+    RequestWithCallback getCapturingRequest();
 
+    /**
+     * Returns the requests that have not received a result or an error yet.
+     */
+    @NonNull
     @VisibleForTesting
-    List<RequestWithCallback> getIncompleteRequests() {
-        return mIncompleteRequests;
-    }
+    List<RequestWithCallback> getIncompleteRequests();
 
+    /**
+     * Returns the {@link ImagePipeline} instance used under the hood.
+     */
     @VisibleForTesting
     @NonNull
-    public ImagePipeline getImagePipeline() {
-        return mImagePipeline;
-    }
-
-    @Override
-    public void onImageClose(@NonNull ImageProxy image) {
-        mainThreadExecutor().execute(this::issueNextRequest);
-    }
+    ImagePipeline getImagePipeline();
 
     @AutoValue
     abstract static class CaptureError {
@@ -318,4 +115,18 @@
         }
     }
 
+    /**
+     * Interface for deferring creation of a {@link TakePictureManager}.
+     */
+    interface Provider {
+        /**
+         * Creates a new, initialized instance of a {@link TakePictureManager}.
+         *
+         * @param imageCaptureControl       Used by TakePictureManager to control an
+         *                                  {@link ImageCapture} instance.
+         * @return                          The {@code TakePictureManager} instance.
+         */
+        @NonNull
+        TakePictureManager newInstance(@NonNull ImageCaptureControl imageCaptureControl);
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
new file mode 100644
index 0000000..e356a50
--- /dev/null
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureManagerImpl.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.core.imagecapture;
+
+import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
+import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
+import static androidx.camera.core.impl.utils.Threads.checkMainThread;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor;
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
+
+import static java.util.Objects.requireNonNull;
+
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.ForwardingImageProxy.OnImageCloseListener;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.ImageProxy;
+import androidx.camera.core.Logger;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.core.util.Pair;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+/**
+ * Manages {@link ImageCapture#takePicture} calls.
+ *
+ * <p>In coming requests are added to a queue and later sent to camera one at a time. Only one
+ * in-flight request is allowed at a time. The next request cannot be sent until the current one
+ * is completed by camera. However, it allows multiple concurrent requests for post-processing,
+ * as {@link ImagePipeline} supports parallel processing.
+ *
+ * <p>This class selectively propagates callbacks from camera and {@link ImagePipeline} to the
+ * app. e.g. it may choose to retry the request before sending the {@link ImageCaptureException}
+ * to the app.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+public class TakePictureManagerImpl implements TakePictureManager, OnImageCloseListener,
+        TakePictureRequest.RetryControl {
+
+    private static final String TAG = "TakePictureManagerImpl";
+
+    // Queue of new requests that have not been sent to the pipeline/camera.
+    @VisibleForTesting
+    final Deque<TakePictureRequest> mNewRequests = new ArrayDeque<>();
+    final ImageCaptureControl mImageCaptureControl;
+    ImagePipeline mImagePipeline;
+
+    // The current request being processed by the camera. Only one request can be processed by
+    // the camera at the same time. Null if the camera is idle.
+    @Nullable
+    private RequestWithCallback mCapturingRequest;
+    // The current requests that have not received a result or an error.
+    private final List<RequestWithCallback> mIncompleteRequests;
+
+    // Once paused, the class waits until the class is resumed to handle new requests.
+    boolean mPaused = false;
+
+    /**
+     * @param imageCaptureControl for controlling {@link ImageCapture}
+     */
+    @MainThread
+    public TakePictureManagerImpl(@NonNull ImageCaptureControl imageCaptureControl) {
+        checkMainThread();
+        mImageCaptureControl = imageCaptureControl;
+        mIncompleteRequests = new ArrayList<>();
+    }
+
+    /**
+     * Sets the {@link ImagePipeline} for building capture requests and post-processing camera
+     * output.
+     */
+    @MainThread
+    @Override
+    public void setImagePipeline(@NonNull ImagePipeline imagePipeline) {
+        checkMainThread();
+        mImagePipeline = imagePipeline;
+        mImagePipeline.setOnImageCloseListener(this);
+    }
+
+    /**
+     * Adds requests to the queue.
+     *
+     * <p>The requests in the queue will be executed based on the order being added.
+     */
+    @MainThread
+    @Override
+    public void offerRequest(@NonNull TakePictureRequest takePictureRequest) {
+        checkMainThread();
+        mNewRequests.offer(takePictureRequest);
+        issueNextRequest();
+    }
+
+    @MainThread
+    @Override
+    public void retryRequest(@NonNull TakePictureRequest request) {
+        checkMainThread();
+        Logger.d(TAG, "Add a new request for retrying.");
+        // Insert the request to the front of the queue.
+        mNewRequests.addFirst(request);
+        // Try to issue the newly added request in case condition allows.
+        issueNextRequest();
+    }
+
+    /**
+     * Pauses sending request to camera.
+     */
+    @MainThread
+    @Override
+    public void pause() {
+        checkMainThread();
+        mPaused = true;
+
+        // Always retry because the camera may not send an error callback during the reset.
+        if (mCapturingRequest != null) {
+            mCapturingRequest.abortSilentlyAndRetry();
+        }
+    }
+
+    /**
+     * Resumes sending request to camera.
+     */
+    @MainThread
+    @Override
+    public void resume() {
+        checkMainThread();
+        mPaused = false;
+        issueNextRequest();
+    }
+
+    /**
+     * Clears the requests queue.
+     */
+    @MainThread
+    @Override
+    public void abortRequests() {
+        checkMainThread();
+        ImageCaptureException exception =
+                new ImageCaptureException(ERROR_CAMERA_CLOSED, "Camera is closed.", null);
+
+        // Clear pending request first so aborting in-flight request won't trigger another capture.
+        for (TakePictureRequest request : mNewRequests) {
+            request.onError(exception);
+        }
+        mNewRequests.clear();
+
+        // Abort the in-flight request after clearing the pending requests.
+        // Snapshot to avoid concurrent modification with the removal in getCompleteFuture().
+        List<RequestWithCallback> requestsSnapshot = new ArrayList<>(mIncompleteRequests);
+        for (RequestWithCallback request : requestsSnapshot) {
+            // TODO: optimize the performance by not processing aborted requests.
+            request.abortAndSendErrorToApp(exception);
+        }
+    }
+
+    /**
+     * Issues the next request if conditions allow.
+     */
+    @MainThread
+    void issueNextRequest() {
+        checkMainThread();
+        Log.d(TAG, "Issue the next TakePictureRequest.");
+        if (hasCapturingRequest()) {
+            Log.d(TAG, "There is already a request in-flight.");
+            return;
+        }
+        if (mPaused) {
+            Log.d(TAG, "The class is paused.");
+            return;
+        }
+        if (mImagePipeline.getCapacity() == 0) {
+            Log.d(TAG, "Too many acquire images. Close image to be able to process next.");
+            return;
+        }
+        TakePictureRequest request = mNewRequests.poll();
+        if (request == null) {
+            Log.d(TAG, "No new request.");
+            return;
+        }
+
+        RequestWithCallback requestWithCallback = new RequestWithCallback(request, this);
+        trackCurrentRequests(requestWithCallback);
+
+        // Send requests.
+        Pair<CameraRequest, ProcessingRequest> requests =
+                mImagePipeline.createRequests(request, requestWithCallback,
+                        requestWithCallback.getCaptureFuture());
+        CameraRequest cameraRequest = requireNonNull(requests.first);
+        ProcessingRequest processingRequest = requireNonNull(requests.second);
+        mImagePipeline.submitProcessingRequest(processingRequest);
+        ListenableFuture<Void> captureRequestFuture = submitCameraRequest(cameraRequest);
+        requestWithCallback.setCaptureRequestFuture(captureRequestFuture);
+    }
+
+    /**
+     * Waits for the request to finish before issuing the next.
+     */
+    private void trackCurrentRequests(@NonNull RequestWithCallback requestWithCallback) {
+        checkState(!hasCapturingRequest());
+        mCapturingRequest = requestWithCallback;
+
+        // Waits for the capture to finish before issuing the next.
+        mCapturingRequest.getCaptureFuture().addListener(() -> {
+            mCapturingRequest = null;
+            issueNextRequest();
+        }, directExecutor());
+
+        // Track all incomplete requests so we can abort them when UseCase is detached.
+        mIncompleteRequests.add(requestWithCallback);
+        requestWithCallback.getCompleteFuture().addListener(() -> {
+            mIncompleteRequests.remove(requestWithCallback);
+        }, directExecutor());
+    }
+
+    /**
+     * Submit a request to camera and post-processing pipeline.
+     *
+     * <p>Flash is locked/unlocked during the flight of a {@link CameraRequest}.
+     */
+    @MainThread
+    private ListenableFuture<Void> submitCameraRequest(
+            @NonNull CameraRequest cameraRequest) {
+        checkMainThread();
+        mImageCaptureControl.lockFlashMode();
+        ListenableFuture<Void> captureRequestFuture =
+                mImageCaptureControl.submitStillCaptureRequests(cameraRequest.getCaptureConfigs());
+        Futures.addCallback(captureRequestFuture, new FutureCallback<Void>() {
+            @Override
+            public void onSuccess(@Nullable Void result) {
+                mImageCaptureControl.unlockFlashMode();
+            }
+
+            @Override
+            public void onFailure(@NonNull Throwable throwable) {
+                if (cameraRequest.isAborted()) {
+                    // When the pipeline is recreated, the in-flight request is aborted and
+                    // retried. On legacy devices, the camera may return CancellationException
+                    // for the aborted request which causes the retried request to fail. Return
+                    // early if the request has been aborted.
+                    return;
+                } else {
+                    int requestId = cameraRequest.getCaptureConfigs().get(0).getId();
+                    if (throwable instanceof ImageCaptureException) {
+                        mImagePipeline.notifyCaptureError(
+                                CaptureError.of(requestId, (ImageCaptureException) throwable));
+                    } else {
+                        mImagePipeline.notifyCaptureError(
+                                CaptureError.of(requestId, new ImageCaptureException(
+                                        ERROR_CAPTURE_FAILED,
+                                        "Failed to submit capture request",
+                                        throwable)));
+                    }
+                }
+                mImageCaptureControl.unlockFlashMode();
+            }
+        }, mainThreadExecutor());
+        return captureRequestFuture;
+    }
+
+    @VisibleForTesting
+    @Override
+    public boolean hasCapturingRequest() {
+        return mCapturingRequest != null;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    @Override
+    public RequestWithCallback getCapturingRequest() {
+        return mCapturingRequest;
+    }
+
+    @NonNull
+    @VisibleForTesting
+    @Override
+    public List<RequestWithCallback> getIncompleteRequests() {
+        return mIncompleteRequests;
+    }
+
+    @VisibleForTesting
+    @NonNull
+    @Override
+    public ImagePipeline getImagePipeline() {
+        return mImagePipeline;
+    }
+
+    @Override
+    public void onImageClose(@NonNull ImageProxy image) {
+        mainThreadExecutor().execute(this::issueNextRequest);
+    }
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
index 4ef3f3b..d49c06b9 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/TakePictureRequest.java
@@ -82,14 +82,14 @@
      * Gets the app provided options for on-disk capture.
      */
     @Nullable
-    abstract ImageCapture.OutputFileOptions getOutputFileOptions();
+    public abstract ImageCapture.OutputFileOptions getOutputFileOptions();
 
     /**
      * A snapshot of {@link ImageCapture#getViewPortCropRect()} when
      * {@link ImageCapture#takePicture} is called.
      */
     @NonNull
-    abstract Rect getCropRect();
+    public abstract Rect getCropRect();
 
     /**
      * A snapshot of {@link ImageCapture#getSensorToBufferTransformMatrix()} when
@@ -102,14 +102,14 @@
      * A snapshot of rotation degrees when {@link ImageCapture#takePicture} is called.
      */
     @ImageOutputConfig.RotationValue
-    abstract int getRotationDegrees();
+    public abstract int getRotationDegrees();
 
     /**
      * A snapshot of {@link ImageCaptureConfig#getJpegQuality()} when
      * {@link ImageCapture#takePicture} is called.
      */
     @IntRange(from = 1, to = 100)
-    abstract int getJpegQuality();
+    public abstract int getJpegQuality();
 
     /**
      * Gets the capture mode of the request.
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
index 347a25c..9d6f159 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/UseCaseConfig.java
@@ -21,10 +21,16 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.camera.core.ExtendableBuilder;
+import androidx.camera.core.ImageCapture;
 import androidx.camera.core.UseCase;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
+import androidx.camera.core.imagecapture.TakePictureManagerImpl;
 import androidx.camera.core.impl.stabilization.StabilizationMode;
 import androidx.camera.core.internal.TargetConfig;
 
+import java.util.Objects;
+
 /**
  * Configuration containing options for use cases.
  *
@@ -108,6 +114,10 @@
     Option<Integer> OPTION_VIDEO_STABILIZATION_MODE =
             Option.create("camerax.core.useCase.videoStabilizationMode", int.class);
 
+    Option<TakePictureManager.Provider> OPTION_TAKE_PICTURE_MANAGER_PROVIDER =
+            Option.create("camerax.core.useCase.takePictureManagerProvider",
+                    TakePictureManager.Provider.class);
+
     // *********************************************************************************************
 
     /**
@@ -329,6 +339,22 @@
     }
 
     /**
+     * @return The {@link TakePictureManager} implementation for {@link ImageCapture} use case.
+     */
+    @NonNull
+    default TakePictureManager.Provider getTakePictureManagerProvider() {
+        return Objects.requireNonNull(retrieveOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+                new TakePictureManager.Provider() {
+                    @NonNull
+                    @Override
+                    public TakePictureManager newInstance(
+                            @NonNull ImageCaptureControl imageCaptureControl) {
+                        return new TakePictureManagerImpl(imageCaptureControl);
+                    }
+                }));
+    }
+
+    /**
      * Builder for a {@link UseCase}.
      *
      * @param <T> The type of the object which will be built by {@link #build()}.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
index be859db..45ba186 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/ImageCaptureExtTest.kt
@@ -28,7 +28,6 @@
 import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
-import java.io.File
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.MainScope
@@ -36,7 +35,9 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
+import org.junit.rules.TemporaryFolder
 import org.junit.runner.RunWith
 import org.robolectric.RobolectricTestRunner
 import org.robolectric.Shadows
@@ -47,9 +48,14 @@
 @DoNotInstrument
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class ImageCaptureExtTest {
+    @get:Rule
+    val temporaryFolder =
+        TemporaryFolder(ApplicationProvider.getApplicationContext<Context>().cacheDir)
+
     private val context = ApplicationProvider.getApplicationContext<Context>()
-    private val fakeOutputFileOptions =
-        ImageCapture.OutputFileOptions.Builder(File("fake_path")).build()
+    private val fakeOutputFileOptions by lazy {
+        ImageCapture.OutputFileOptions.Builder(temporaryFolder.newFile("fake_path")).build()
+    }
     private lateinit var cameraProvider: ProcessCameraProvider
     private lateinit var imageCapture: ImageCapture
 
@@ -88,12 +94,11 @@
     fun takePicture_inMemory_canGetImage(): Unit = runTest {
         // Arrange
         val imageProxy = FakeImageProxy(FakeImageInfo())
+        val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+        fakeTakePictureManager.enqueueImageProxy(imageProxy)
 
         // Arrange & Act.
         val takePictureAsync = MainScope().async { imageCapture.takePicture() }
-        Shadows.shadowOf(Looper.getMainLooper()).idle()
-        val imageCaptureCallback = imageCapture.getTakePictureRequest()?.inMemoryCallback
-        imageCaptureCallback?.onCaptureSuccess(imageProxy)
 
         // Assert.
         Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -135,6 +140,7 @@
         var callbackCalled = false
         val progress = 100
         var resultProgress = 0
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -163,6 +169,7 @@
         var callbackCalled = false
         val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
         lateinit var resultBitmap: Bitmap
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -189,15 +196,14 @@
     fun takePicture_onDisk_canGetResult(): Unit = runTest {
         // Arrange
         val outputFileResults = ImageCapture.OutputFileResults(null)
+        val fakeTakePictureManager = FakeAppConfig.getTakePictureManager()!!
+        fakeTakePictureManager.enqueueOutputFileResults(outputFileResults)
 
         // Arrange & Act.
         val takePictureAsync =
             MainScope().async {
                 imageCapture.takePicture(outputFileOptions = fakeOutputFileOptions)
             }
-        Shadows.shadowOf(Looper.getMainLooper()).idle()
-        val imageCaptureCallback = imageCapture.getTakePictureRequest()?.onDiskCallback
-        imageCaptureCallback?.onImageSaved(outputFileResults)
 
         // Assert.
         Shadows.shadowOf(Looper.getMainLooper()).idle()
@@ -245,6 +251,7 @@
         var callbackCalled = false
         val progress = 100
         var resultProgress = 0
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
@@ -274,6 +281,7 @@
         var callbackCalled = false
         val bitmap = Bitmap.createBitmap(800, 600, Bitmap.Config.ARGB_8888)
         lateinit var resultBitmap: Bitmap
+        FakeAppConfig.getTakePictureManager()!!.disableAutoComplete = true
 
         // Act.
         val takePictureAsync =
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
index 7b775e8..87e05b0 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/FakeTakePictureRequest.kt
@@ -112,7 +112,7 @@
         return fileOptions
     }
 
-    internal override fun getCropRect(): Rect {
+    override fun getCropRect(): Rect {
         return Rect(0, 0, 640, 480)
     }
 
@@ -120,11 +120,11 @@
         return Matrix()
     }
 
-    internal override fun getRotationDegrees(): Int {
+    override fun getRotationDegrees(): Int {
         return ROTATION_DEGREES
     }
 
-    internal override fun getJpegQuality(): Int {
+    override fun getJpegQuality(): Int {
         return JPEG_QUALITY
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
index db131ac..4f02ad7 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/TakePictureManagerTest.kt
@@ -50,7 +50,7 @@
     private val imagePipeline = FakeImagePipeline()
     private val imageCaptureControl = FakeImageCaptureControl()
     private val takePictureManager =
-        TakePictureManager(imageCaptureControl).also { it.imagePipeline = imagePipeline }
+        TakePictureManagerImpl(imageCaptureControl).also { it.imagePipeline = imagePipeline }
     private val exception = ImageCaptureException(ImageCapture.ERROR_UNKNOWN, "", null)
 
     @After
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
index bb2d4f4..667a60f 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/LifecycleCameraProviderImpl.kt
@@ -143,7 +143,7 @@
             }
         }
 
-    override fun shutdownAsync(): ListenableFuture<Void> {
+    internal fun shutdownAsync(): ListenableFuture<Void> {
         Threads.runOnMainSync {
             unbindAll()
             lifecycleCameraRepository.clear()
diff --git a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
index 9814e90..e40f3ed 100644
--- a/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
+++ b/camera/camera-lifecycle/src/main/java/androidx/camera/lifecycle/ProcessCameraProvider.kt
@@ -114,9 +114,8 @@
         return lifecycleCameraProvider.getCameraInfo(cameraSelector)
     }
 
-    // TODO: Remove the annotation when LifecycleCameraProvider is ready to be public.
     @VisibleForTesting
-    override fun shutdownAsync(): ListenableFuture<Void> {
+    public fun shutdownAsync(): ListenableFuture<Void> {
         return lifecycleCameraProvider.shutdownAsync()
     }
 
diff --git a/camera/camera-testing/api/current.txt b/camera/camera-testing/api/current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/current.txt
+++ b/camera/camera-testing/api/current.txt
@@ -42,10 +42,12 @@
     ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     method public void addInteropConfig(androidx.camera.core.impl.Config);
+    method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
     method public void clearInteropConfig();
-    method public void clearNewCaptureRequestListener();
+    method @Deprecated public void clearNewCaptureRequestListener();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public int getExposureCompensationIndex();
     method public int getFlashMode();
@@ -62,11 +64,14 @@
     method public void notifyAllRequestsOnCaptureCancelled();
     method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
     method public void notifyAllRequestsOnCaptureFailed();
+    method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+    method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public void setFlashMode(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
-    method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
-    method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/api/restricted_current.txt b/camera/camera-testing/api/restricted_current.txt
index fbf779a..161b797 100644
--- a/camera/camera-testing/api/restricted_current.txt
+++ b/camera/camera-testing/api/restricted_current.txt
@@ -42,10 +42,12 @@
     ctor public FakeCameraControl(androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     ctor public FakeCameraControl(java.util.concurrent.Executor, androidx.camera.core.impl.CameraControlInternal.ControlUpdateCallback);
     method public void addInteropConfig(androidx.camera.core.impl.Config);
+    method public void addOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void addOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void addZslConfig(androidx.camera.core.impl.SessionConfig.Builder);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> cancelFocusAndMetering();
     method public void clearInteropConfig();
-    method public void clearNewCaptureRequestListener();
+    method @Deprecated public void clearNewCaptureRequestListener();
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> enableTorch(boolean);
     method public int getExposureCompensationIndex();
     method public int getFlashMode();
@@ -62,11 +64,14 @@
     method public void notifyAllRequestsOnCaptureCancelled();
     method public void notifyAllRequestsOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
     method public void notifyAllRequestsOnCaptureFailed();
+    method public void notifyLastRequestOnCaptureCompleted(androidx.camera.core.impl.CameraCaptureResult);
+    method public void removeOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method public void removeOnNewCaptureRequestListeners(java.util.List<androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener!>);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Integer!> setExposureCompensationIndex(int);
     method public void setFlashMode(int);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setLinearZoom(float);
-    method public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
-    method public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
+    method @Deprecated public void setOnNewCaptureRequestListener(java.util.concurrent.Executor, androidx.camera.testing.fakes.FakeCameraControl.OnNewCaptureRequestListener);
     method public void setScreenFlash(androidx.camera.core.ImageCapture.ScreenFlash?);
     method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
     method public void setZslDisabledByUserCaseConfig(boolean);
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
index 41958c4..c7f41a5 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeAppConfig.java
@@ -28,6 +28,10 @@
 import androidx.camera.testing.impl.fakes.FakeCameraDeviceSurfaceManager;
 import androidx.camera.testing.impl.fakes.FakeCameraFactory;
 import androidx.camera.testing.impl.fakes.FakeUseCaseConfigFactory;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Convenience class for generating a fake {@link CameraXConfig}.
@@ -47,6 +51,9 @@
     @Nullable
     private static FakeCamera sFrontCamera = null;
 
+    @Nullable
+    private static FakeUseCaseConfigFactory sFakeUseCaseConfigFactory;
+
     /** Generates a fake {@link CameraXConfig}. */
     @NonNull
     public static CameraXConfig create() {
@@ -59,28 +66,26 @@
      */
     @NonNull
     public static CameraXConfig create(@Nullable CameraSelector availableCamerasSelector) {
+        FakeCameraFactory cameraFactory = createCameraFactory(availableCamerasSelector);
+
         final CameraFactory.Provider cameraFactoryProvider =
-                (ignored1, ignored2, ignored3, ignore4) -> {
-                    final FakeCameraFactory cameraFactory = new FakeCameraFactory(
-                            availableCamerasSelector);
-                    cameraFactory.insertCamera(CameraSelector.LENS_FACING_BACK,
-                            DEFAULT_BACK_CAMERA_ID,
-                            FakeAppConfig::getBackCamera);
-                    cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
-                            DEFAULT_FRONT_CAMERA_ID,
-                            FakeAppConfig::getFrontCamera);
-                    final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
-                    cameraFactory.setCameraCoordinator(cameraCoordinator);
-                    return cameraFactory;
-                };
+                (ignored1, ignored2, ignored3, ignore4) -> cameraFactory;
 
         final CameraDeviceSurfaceManager.Provider surfaceManagerProvider =
                 (ignored1, ignored2, ignored3) -> new FakeCameraDeviceSurfaceManager();
 
+        List<FakeCamera> fakeCameras = new ArrayList<>();
+        for (String cameraId : cameraFactory.getAvailableCameraIds()) {
+            fakeCameras.add((FakeCamera) cameraFactory.getCamera(cameraId));
+        }
+
+        sFakeUseCaseConfigFactory = new FakeUseCaseConfigFactory(fakeCameras);
+
         final CameraXConfig.Builder appConfigBuilder = new CameraXConfig.Builder()
                 .setCameraFactoryProvider(cameraFactoryProvider)
                 .setDeviceSurfaceManagerProvider(surfaceManagerProvider)
-                .setUseCaseConfigFactoryProvider(ignored -> new FakeUseCaseConfigFactory());
+                .setUseCaseConfigFactoryProvider(
+                        ignored -> sFakeUseCaseConfigFactory);
 
         if (availableCamerasSelector != null) {
             appConfigBuilder.setAvailableCamerasLimiter(availableCamerasSelector);
@@ -89,6 +94,21 @@
         return appConfigBuilder.build();
     }
 
+    private static FakeCameraFactory createCameraFactory(
+            @Nullable CameraSelector availableCamerasSelector) {
+        FakeCameraFactory cameraFactory = new FakeCameraFactory(availableCamerasSelector);
+        cameraFactory.insertCamera(
+                CameraSelector.LENS_FACING_BACK,
+                DEFAULT_BACK_CAMERA_ID,
+                FakeAppConfig::getBackCamera);
+        cameraFactory.insertCamera(CameraSelector.LENS_FACING_FRONT,
+                DEFAULT_FRONT_CAMERA_ID,
+                FakeAppConfig::getFrontCamera);
+        final CameraCoordinator cameraCoordinator = new FakeCameraCoordinator();
+        cameraFactory.setCameraCoordinator(cameraCoordinator);
+        return cameraFactory;
+    }
+
     /**
      * Returns the default fake back camera that is used internally by CameraX.
      */
@@ -126,4 +146,20 @@
             return create();
         }
     }
+
+    /**
+     * Returns the {@link TakePictureManagerWrapper} being used for image capture.
+     *
+     * <p> Note that this may be null if {@link androidx.camera.core.ImageCapture} is still not set
+     * up and bound to a camera.
+     */
+    @Nullable
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+    public static TakePictureManagerWrapper getTakePictureManager() {
+        if (sFakeUseCaseConfigFactory == null) {
+            return null;
+        }
+        return sFakeUseCaseConfigFactory.getTakePictureManager();
+    }
+
 }
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 a3b256b..b4d661b 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
@@ -21,6 +21,7 @@
 
 import android.graphics.Rect;
 
+import androidx.annotation.GuardedBy;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.camera.core.FocusMeteringAction;
@@ -46,6 +47,8 @@
 import com.google.common.util.concurrent.ListenableFuture;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 import java.util.concurrent.Executor;
@@ -76,13 +79,18 @@
      * <p> {@link CameraXExecutors#directExecutor} via default, unless some other executor is set
      * via {@link #FakeCameraControl(Executor, CameraControlInternal.ControlUpdateCallback)}.
      */
-    @NonNull private final Executor mExecutor;
+    @NonNull
+    private final Executor mExecutor;
     private final ControlUpdateCallback mControlUpdateCallback;
     private final SessionConfig.Builder mSessionConfigBuilder = new SessionConfig.Builder();
     @ImageCapture.FlashMode
     private int mFlashMode = FLASH_MODE_OFF;
     private final ArrayList<CaptureConfig> mSubmittedCaptureRequests = new ArrayList<>();
+    @Deprecated
     private Pair<Executor, OnNewCaptureRequestListener> mOnNewCaptureRequestListener;
+    @GuardedBy("mOnNewCaptureRequestListeners")
+    private final List<Pair<Executor, OnNewCaptureRequestListener>> mOnNewCaptureRequestListeners =
+            new ArrayList<>();
     private MutableOptionsBundle mInteropConfig = MutableOptionsBundle.create();
     private final ArrayList<CallbackToFutureAdapter.Completer<Void>> mSubmittedCompleterList =
             new ArrayList<>();
@@ -127,7 +135,8 @@
      * Constructs an instance of {@link FakeCameraControl} with the
      * provided {@link ControlUpdateCallback}.
      *
-     * @param executor {@link Executor} used to invoke the {@code controlUpdateCallback}.
+     * @param executor              {@link Executor} used to invoke the {@code
+     * controlUpdateCallback}.
      * @param controlUpdateCallback {@link ControlUpdateCallback} to notify events.
      */
     public FakeCameraControl(@NonNull Executor executor,
@@ -180,6 +189,38 @@
     }
 
     /**
+     * Notifies last submitted request using {@link CameraCaptureCallback#onCaptureCompleted},
+     * which is invoked in the thread denoted by {@link #mExecutor}.
+     *
+     * @param result The {@link CameraCaptureResult} which is notified to all the callbacks.
+     */
+    public void notifyLastRequestOnCaptureCompleted(@NonNull CameraCaptureResult result) {
+        if (mSubmittedCaptureRequests.isEmpty() || mSubmittedCompleterList.isEmpty()) {
+            Logger.e(TAG,
+                    "notifyLastRequestOnCaptureCompleted: returning early since either "
+                            + "mSubmittedCaptureRequests or mSubmittedCompleterList is empty, "
+                            + "mSubmittedCaptureRequests = "
+                            + mSubmittedCaptureRequests + ", mSubmittedCompleterList"
+                            + mSubmittedCompleterList);
+            return;
+        }
+
+        CaptureConfig captureConfig = mSubmittedCaptureRequests.get(
+                mSubmittedCaptureRequests.size() - 1);
+        for (CameraCaptureCallback cameraCaptureCallback :
+                captureConfig.getCameraCaptureCallbacks()) {
+            mExecutor.execute(() -> cameraCaptureCallback.onCaptureCompleted(
+                    captureConfig.getId(), result));
+        }
+        mSubmittedCaptureRequests.remove(captureConfig);
+
+        CallbackToFutureAdapter.Completer<Void> completer = mSubmittedCompleterList.get(
+                mSubmittedCompleterList.size() - 1);
+        completer.set(null);
+        mSubmittedCompleterList.remove(completer);
+    }
+
+    /**
      * Notifies all submitted requests using {@link CameraCaptureCallback#onCaptureCompleted},
      * which is invoked in the thread denoted by {@link #mExecutor}.
      *
@@ -288,6 +329,7 @@
     public ListenableFuture<List<Void>> submitStillCaptureRequests(
             @NonNull List<CaptureConfig> captureConfigs,
             int captureMode, int flashType) {
+        Logger.d(TAG, "submitStillCaptureRequests: captureConfigs = " + captureConfigs);
         mSubmittedCaptureRequests.addAll(captureConfigs);
         mExecutor.execute(
                 () -> mControlUpdateCallback.onCameraControlCaptureRequests(captureConfigs));
@@ -299,12 +341,16 @@
             }));
         }
 
-        if (mOnNewCaptureRequestListener != null) {
-            Executor executor = Objects.requireNonNull(mOnNewCaptureRequestListener.first);
-            OnNewCaptureRequestListener listener =
-                    Objects.requireNonNull(mOnNewCaptureRequestListener.second);
+        synchronized (mOnNewCaptureRequestListeners) {
+            Logger.d(TAG, "submitStillCaptureRequests: mOnNewCaptureRequestListeners = "
+                    + mOnNewCaptureRequestListeners);
 
-            executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+            for (Pair<Executor, FakeCameraControl.OnNewCaptureRequestListener> listenerPair :
+                    mOnNewCaptureRequestListeners) {
+                Executor executor = Objects.requireNonNull(listenerPair.first);
+                OnNewCaptureRequestListener listener = Objects.requireNonNull(listenerPair.second);
+                executor.execute(() -> listener.onNewCaptureRequests(captureConfigs));
+            }
         }
         return Futures.allAsList(fakeFutures);
     }
@@ -348,6 +394,58 @@
     }
 
     /**
+     * Adds a listener to be notified when there are new capture requests submitted.
+     *
+     * <p> Note that the listener will be executed on the calling thread directly using
+     * {@link CameraXExecutors#directExecutor}. To specify the execution thread, use
+     * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
+     *
+     * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     */
+    public void addOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+        addOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
+    }
+
+    /**
+     * Adds a listener to be notified when there are new capture requests submitted.
+     *
+     * @param executor {@link Executor} used to notify the {@code listener}.
+     * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     */
+    public void addOnNewCaptureRequestListener(@NonNull Executor executor,
+            @NonNull OnNewCaptureRequestListener listener) {
+        synchronized (mOnNewCaptureRequestListeners) {
+            mOnNewCaptureRequestListeners.add(new Pair<>(executor, listener));
+        }
+    }
+
+    /**
+     * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+     */
+    public void removeOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
+        removeOnNewCaptureRequestListeners(Collections.singletonList(listener));
+    }
+
+    /**
+     * Removes a listener set via {@link #addOnNewCaptureRequestListener}.
+     */
+    public void removeOnNewCaptureRequestListeners(
+            @NonNull List<OnNewCaptureRequestListener> listeners) {
+        synchronized (mOnNewCaptureRequestListeners) {
+            Iterator<Pair<Executor, OnNewCaptureRequestListener>> iterator =
+                    mOnNewCaptureRequestListeners.iterator();
+            while (iterator.hasNext()) {
+                Pair<Executor, OnNewCaptureRequestListener> element = iterator.next();
+                if (listeners.contains(element.second)) {
+                    iterator.remove();
+                }
+            }
+        }
+    }
+
+    /**
      * Sets a listener to be notified when there are new capture requests submitted.
      *
      * <p> Note that the listener will be executed on the calling thread directly using
@@ -355,8 +453,10 @@
      * {@link #setOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}.
      *
      * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
-     * {@link CaptureConfig} parameters when new capture requests are submitted.
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     * @deprecated Use {@link #addOnNewCaptureRequestListener(OnNewCaptureRequestListener)} instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void setOnNewCaptureRequestListener(@NonNull OnNewCaptureRequestListener listener) {
         setOnNewCaptureRequestListener(CameraXExecutors.directExecutor(), listener);
     }
@@ -366,17 +466,31 @@
      *
      * @param executor {@link Executor} used to notify the {@code listener}.
      * @param listener {@link OnNewCaptureRequestListener} that is notified with the submitted
-     * {@link CaptureConfig} parameters when new capture requests are submitted.
+     *                 {@link CaptureConfig} parameters when new capture requests are submitted.
+     * @deprecated Use
+     * {@link #addOnNewCaptureRequestListener(Executor, OnNewCaptureRequestListener)}
+     * instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void setOnNewCaptureRequestListener(@NonNull Executor executor,
             @NonNull OnNewCaptureRequestListener listener) {
         mOnNewCaptureRequestListener = new Pair<>(executor, listener);
+        addOnNewCaptureRequestListener(executor, listener);
     }
 
     /**
      * Clears any listener set via {@link #setOnNewCaptureRequestListener}.
+     *
+     * @deprecated Use {@link #removeOnNewCaptureRequestListener(OnNewCaptureRequestListener)}
+     * instead.
      */
+    @Deprecated // TODO: b/359458110 - Remove all usages
     public void clearNewCaptureRequestListener() {
+        if (mOnNewCaptureRequestListener == null) {
+            return;
+        }
+        removeOnNewCaptureRequestListener(
+                Objects.requireNonNull(mOnNewCaptureRequestListener.second));
         mOnNewCaptureRequestListener = null;
     }
 
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
index e960662..015bff9 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/CaptureSimulation.kt
@@ -16,8 +16,11 @@
 
 package androidx.camera.testing.impl
 
+import android.graphics.Bitmap
 import android.graphics.Rect
+import android.util.Size
 import android.view.Surface
+import androidx.camera.core.Logger
 import androidx.camera.core.impl.DeferrableSurface
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.futures.FutureCallback
@@ -36,7 +39,7 @@
 private const val TAG = "CaptureSimulation"
 
 /** Simulates a capture frame being drawn on all of the provided surfaces. */
-public suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
+internal suspend fun List<DeferrableSurface>.simulateCaptureFrame(): Unit = forEach {
     it.simulateCaptureFrame()
 }
 
@@ -45,7 +48,7 @@
  *
  * @throws IllegalStateException If [DeferrableSurface.getSurface] provides a null surface.
  */
-public suspend fun DeferrableSurface.simulateCaptureFrame() {
+internal suspend fun DeferrableSurface.simulateCaptureFrame() {
     val deferred = CompletableDeferred<Unit>()
 
     Futures.addCallback(
@@ -53,6 +56,7 @@
         object : FutureCallback<Surface?> {
             override fun onSuccess(surface: Surface?) {
                 if (surface == null) {
+                    Logger.w(TAG, "simulateCaptureFrame: surface obtained from $this is null!")
                     deferred.completeExceptionally(
                         IllegalStateException(
                             "Null surface obtained from ${this@simulateCaptureFrame}"
@@ -60,10 +64,9 @@
                     )
                     return
                 }
-                val canvas =
-                    surface.lockCanvas(Rect(0, 0, prescribedSize.width, prescribedSize.height))
                 // TODO: Draw something on the canvas (e.g. fake image bitmap or alternating color).
-                surface.unlockCanvasAndPost(canvas)
+                surface.simulateCaptureFrame(prescribedSize)
+
                 deferred.complete(Unit)
             }
 
@@ -77,6 +80,20 @@
     deferred.await()
 }
 
+/**
+ * Simulates a capture frame being drawn on a [Surface].
+ *
+ * @param canvasSize The canvas size for drawing.
+ * @param bitmap A bitmap to draw as the capture frame, if not null.
+ */
+internal fun Surface.simulateCaptureFrame(canvasSize: Size, bitmap: Bitmap? = null) {
+    val canvas = lockCanvas(Rect(0, 0, canvasSize.width, canvasSize.height))
+    if (bitmap != null) {
+        canvas.drawBitmap(bitmap, null, Rect(0, 0, canvasSize.width, canvasSize.height), null)
+    }
+    unlockCanvasAndPost(canvas)
+}
+
 // The following methods are adapters for Java invocations.
 
 /**
@@ -88,7 +105,7 @@
  * @return A [ListenableFuture] representing when the operation has been completed.
  */
 @JvmOverloads
-public fun List<DeferrableSurface>.simulateCaptureFrameAsync(
+internal fun List<DeferrableSurface>.simulateCaptureFrameAsync(
     executor: Executor = Dispatchers.Default.asExecutor()
 ): ListenableFuture<Void> {
     val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
@@ -104,7 +121,7 @@
  * @return A [ListenableFuture] representing when the operation has been completed.
  */
 @JvmOverloads
-public fun DeferrableSurface.simulateCaptureFrameAsync(
+internal fun DeferrableSurface.simulateCaptureFrameAsync(
     executor: Executor = Dispatchers.Default.asExecutor()
 ): ListenableFuture<Void> {
     val scope = CoroutineScope(SupervisorJob() + executor.asCoroutineDispatcher())
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
index 90c4bce..5acfa2bb 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeImageProxy.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.testing.impl.fakes;
 
+import android.graphics.Bitmap;
 import android.graphics.Rect;
 import android.media.Image;
 
@@ -47,6 +48,8 @@
     @NonNull
     private ImageInfo mImageInfo;
     private Image mImage;
+    @Nullable
+    private Bitmap mBitmap;
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
     final Object mReleaseLock = new Object();
     @SuppressWarnings("WeakerAccess") /* synthetic accessor */
@@ -60,6 +63,11 @@
         mImageInfo = imageInfo;
     }
 
+    public FakeImageProxy(@NonNull ImageInfo imageInfo, @NonNull Bitmap bitmap) {
+        mImageInfo = imageInfo;
+        mBitmap = bitmap;
+    }
+
     @Override
     public void close() {
         synchronized (mReleaseLock) {
@@ -196,4 +204,13 @@
             return mReleaseFuture;
         }
     }
+
+    @NonNull
+    @Override
+    public Bitmap toBitmap() {
+        if (mBitmap != null) {
+            return mBitmap;
+        }
+        return ImageProxy.super.toBitmap();
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
index f364c4e..9ccd9bd 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/fakes/FakeUseCaseConfigFactory.java
@@ -19,6 +19,7 @@
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_CAPTURE_CONFIG_UNPACKER;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_DEFAULT_SESSION_CONFIG;
 import static androidx.camera.core.impl.UseCaseConfig.OPTION_SESSION_CONFIG_UNPACKER;
+import static androidx.camera.core.impl.UseCaseConfig.OPTION_TAKE_PICTURE_MANAGER_PROVIDER;
 
 import android.annotation.SuppressLint;
 import android.hardware.camera2.CameraDevice;
@@ -30,21 +31,47 @@
 import androidx.camera.core.ExperimentalZeroShutterLag;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCapture.CaptureMode;
+import androidx.camera.core.imagecapture.ImageCaptureControl;
+import androidx.camera.core.imagecapture.TakePictureManager;
 import androidx.camera.core.impl.Config;
 import androidx.camera.core.impl.MutableOptionsBundle;
 import androidx.camera.core.impl.OptionsBundle;
 import androidx.camera.core.impl.SessionConfig;
 import androidx.camera.core.impl.UseCaseConfigFactory;
+import androidx.camera.testing.fakes.FakeCamera;
+import androidx.camera.testing.impl.wrappers.TakePictureManagerWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * A fake implementation of {@link UseCaseConfigFactory}.
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
 public final class FakeUseCaseConfigFactory implements UseCaseConfigFactory {
-
     @Nullable
     private CaptureType mLastRequestedCaptureType;
 
+    @Nullable
+    private TakePictureManagerWrapper mTakePictureManager;
+
+    @NonNull
+    private final List<FakeCamera> mFakeCameras = new ArrayList<>();
+
+    /**
+     * Creates a {@link FakeUseCaseConfigFactory} instance.
+     */
+    public FakeUseCaseConfigFactory() {
+    }
+
+    /**
+     * Creates a {@link FakeUseCaseConfigFactory} instance with the available {@link FakeCamera}
+     * instances.
+     */
+    public FakeUseCaseConfigFactory(@NonNull List<FakeCamera> fakeCameras) {
+        mFakeCameras.addAll(fakeCameras);
+    }
+
     /**
      * Returns the configuration for the given capture type, or <code>null</code> if the
      * configuration cannot be produced.
@@ -66,6 +93,20 @@
         mutableConfig.insertOption(OPTION_SESSION_CONFIG_UNPACKER,
                 new FakeSessionConfigOptionUnpacker());
 
+        if (captureType == CaptureType.IMAGE_CAPTURE) {
+            mutableConfig.insertOption(OPTION_TAKE_PICTURE_MANAGER_PROVIDER,
+                    new TakePictureManager.Provider() {
+                        @NonNull
+                        @Override
+                        public TakePictureManager newInstance(
+                                @NonNull ImageCaptureControl imageCaptureControl) {
+                            mTakePictureManager = new TakePictureManagerWrapper(
+                                    imageCaptureControl, mFakeCameras);
+                            return mTakePictureManager;
+                        }
+                    });
+        }
+
         return OptionsBundle.from(mutableConfig);
     }
 
@@ -97,4 +138,12 @@
                 return CameraDevice.TEMPLATE_PREVIEW;
         }
     }
+
+    /**
+     * Returns the last provided {@link TakePictureManagerWrapper} instance.
+     */
+    @Nullable
+    public TakePictureManagerWrapper getTakePictureManager() {
+        return mTakePictureManager;
+    }
 }
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
new file mode 100644
index 0000000..0de7dba
--- /dev/null
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/impl/wrappers/TakePictureManagerWrapper.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright 2024 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.testing.impl.wrappers
+
+import android.graphics.Bitmap
+import android.graphics.Matrix
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageCapture.OutputFileResults
+import androidx.camera.core.ImageProcessingUtil
+import androidx.camera.core.ImageProxy
+import androidx.camera.core.Logger
+import androidx.camera.core.imagecapture.Bitmap2JpegBytes
+import androidx.camera.core.imagecapture.ImageCaptureControl
+import androidx.camera.core.imagecapture.ImagePipeline
+import androidx.camera.core.imagecapture.JpegBytes2Disk
+import androidx.camera.core.imagecapture.JpegBytes2Image
+import androidx.camera.core.imagecapture.RequestWithCallback
+import androidx.camera.core.imagecapture.TakePictureManager
+import androidx.camera.core.imagecapture.TakePictureManagerImpl
+import androidx.camera.core.imagecapture.TakePictureRequest
+import androidx.camera.core.processing.Packet
+import androidx.camera.testing.fakes.FakeCamera
+import androidx.camera.testing.fakes.FakeCameraControl
+import androidx.camera.testing.impl.ExifUtil
+import androidx.camera.testing.impl.TestImageUtil
+import androidx.camera.testing.impl.fakes.FakeCameraCaptureResult
+import androidx.camera.testing.impl.fakes.FakeImageInfo
+import androidx.camera.testing.impl.fakes.FakeImageProxy
+
+/**
+ * A [TakePictureManager] implementation wrapped around the real implementation
+ * [TakePictureManagerImpl].
+ *
+ * It is used for fake cameras and provides fake image capture results when required from a camera.
+ */
+public class TakePictureManagerWrapper(
+    imageCaptureControl: ImageCaptureControl,
+    private val fakeCameras: List<FakeCamera>
+) : TakePictureManager {
+    // Try to keep the fake as close to real as possible
+    private val managerDelegate = TakePictureManagerImpl(imageCaptureControl)
+
+    private val bitmap2JpegBytes = Bitmap2JpegBytes()
+    private val jpegBytes2Disk = JpegBytes2Disk()
+    private val jpegBytes2Image = JpegBytes2Image()
+
+    private val imageProxyQueue = ArrayDeque<ImageProxy>()
+    private val outputFileResultsQueue = ArrayDeque<ImageCapture.OutputFileResults>()
+
+    /** Whether to disable auto capture completion. */
+    public var disableAutoComplete: Boolean = false
+
+    override fun setImagePipeline(imagePipeline: ImagePipeline) {
+        managerDelegate.imagePipeline = imagePipeline
+    }
+
+    override fun offerRequest(takePictureRequest: TakePictureRequest) {
+        val listeners = mutableListOf<FakeCameraControl.OnNewCaptureRequestListener>()
+
+        fakeCameras.forEach { camera ->
+            if (camera.cameraControlInternal is FakeCameraControl) {
+                (camera.cameraControlInternal as FakeCameraControl).apply {
+                    val listener =
+                        FakeCameraControl.OnNewCaptureRequestListener {
+                            if (!disableAutoComplete) {
+                                completeCapturingRequest(this)
+                            }
+                        }
+                    listeners.add(listener)
+                    addOnNewCaptureRequestListener(listener)
+                }
+            } else {
+                Logger.w(
+                    TAG,
+                    "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+                )
+            }
+        }
+
+        managerDelegate.offerRequest(takePictureRequest)
+
+        fakeCameras.forEach { camera ->
+            if (camera.cameraControlInternal is FakeCameraControl) {
+                (camera.cameraControlInternal as FakeCameraControl)
+                    .removeOnNewCaptureRequestListeners(listeners)
+            } else {
+                Logger.w(
+                    TAG,
+                    "Ignoring ${camera.cameraControlInternal} as it's not FakeCameraControl!"
+                )
+            }
+        }
+    }
+
+    override fun pause() {
+        managerDelegate.pause()
+    }
+
+    override fun resume() {
+        managerDelegate.resume()
+    }
+
+    override fun abortRequests() {
+        managerDelegate.abortRequests()
+    }
+
+    @VisibleForTesting
+    override fun hasCapturingRequest(): Boolean = managerDelegate.hasCapturingRequest()
+
+    @VisibleForTesting
+    override fun getCapturingRequest(): RequestWithCallback? = managerDelegate.capturingRequest
+
+    @VisibleForTesting
+    override fun getIncompleteRequests(): List<RequestWithCallback> =
+        managerDelegate.incompleteRequests
+
+    @VisibleForTesting
+    override fun getImagePipeline(): ImagePipeline = managerDelegate.imagePipeline
+
+    @VisibleForTesting
+    public fun completeCapturingRequest(fakeCameraControl: FakeCameraControl) {
+        Log.d(
+            TAG,
+            "completeCapturingRequest: capturingRequest = ${managerDelegate.capturingRequest}"
+        )
+        managerDelegate.capturingRequest?.apply {
+            onCaptureStarted()
+
+            // This ensures the future from CameraControlInternal#submitStillCaptureRequests() is
+            // completed and not garbage collected later
+            // TODO - notify all the new requests, not only the last one
+            fakeCameraControl.notifyLastRequestOnCaptureCompleted(FakeCameraCaptureResult())
+
+            onImageCaptured()
+
+            takePictureRequest.also { req ->
+                val outputFileOptions = req.outputFileOptions // enables smartcast for null check
+                if (req.onDiskCallback != null && outputFileOptions != null) {
+                    if (outputFileResultsQueue.isEmpty()) {
+                        onFinalResult(createOutputFileResults(req, outputFileOptions))
+                    } else {
+                        onFinalResult(outputFileResultsQueue.first())
+                        outputFileResultsQueue.removeFirst()
+                    }
+                } else {
+                    if (imageProxyQueue.isEmpty()) {
+                        onFinalResult(createImageProxy(req))
+                    } else {
+                        onFinalResult(imageProxyQueue.first())
+                        imageProxyQueue.removeFirst()
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Enqueues an [ImageProxy] to be used as result for the next image capture with
+     * [ImageCapture.OnImageCapturedCallback].
+     *
+     * Note that the provided [ImageProxy] is consumed by next image capture and is not available
+     * for following captures. If no result is available during a capture, CameraX will create a
+     * fake image by itself and provide result based on that.
+     */
+    public fun enqueueImageProxy(imageProxy: ImageProxy) {
+        imageProxyQueue.add(imageProxy)
+    }
+
+    /**
+     * Enqueues an [OutputFileResults] to be used as result for the next image capture with
+     * [ImageCapture.OnImageSavedCallback].
+     *
+     * Note that the provided [OutputFileResults] is consumed by next image capture and is not
+     * available for following captures. If no result is available during a capture, CameraX will
+     * create a fake image by itself and provide result based on that.
+     */
+    public fun enqueueOutputFileResults(outputFileResults: ImageCapture.OutputFileResults) {
+        outputFileResultsQueue.add(outputFileResults)
+    }
+
+    private fun createOutputFileResults(
+        takePictureRequest: TakePictureRequest,
+        outputFileOptions: OutputFileOptions
+    ): ImageCapture.OutputFileResults {
+        // TODO - Take a bitmap as input and use that directly
+        val bytesPacket =
+            takePictureRequest.convertBitmapToBytes(
+                TestImageUtil.createBitmap(
+                    takePictureRequest.cropRect.width(),
+                    takePictureRequest.cropRect.height()
+                )
+            )
+        return jpegBytes2Disk.apply(JpegBytes2Disk.In.of(bytesPacket, outputFileOptions))
+    }
+
+    private fun createImageProxy(
+        takePictureRequest: TakePictureRequest,
+    ): ImageProxy {
+        // TODO - Take a bitmap as input and use that directly
+        val bitmap =
+            TestImageUtil.createBitmap(
+                takePictureRequest.cropRect.width(),
+                takePictureRequest.cropRect.height()
+            )
+        if (canLoadImageProcessingUtilJniLib()) {
+            val bytesPacket =
+                takePictureRequest.convertBitmapToBytes(
+                    TestImageUtil.createBitmap(
+                        takePictureRequest.cropRect.width(),
+                        takePictureRequest.cropRect.height()
+                    )
+                )
+            return jpegBytes2Image.apply(bytesPacket).data
+        } else {
+            return bitmap.toFakeImageProxy()
+        }
+    }
+
+    private fun Bitmap.toFakeImageProxy(): ImageProxy {
+        return FakeImageProxy(FakeImageInfo(), this)
+    }
+
+    private fun TakePictureRequest.convertBitmapToBytes(bitmap: Bitmap): Packet<ByteArray> {
+        val inputPacket =
+            Packet.of(
+                bitmap,
+                ExifUtil.createExif(
+                    TestImageUtil.createJpegBytes(cropRect.width(), cropRect.height())
+                ),
+                cropRect,
+                rotationDegrees,
+                Matrix(),
+                FakeCameraCaptureResult()
+            )
+
+        return bitmap2JpegBytes.apply(Bitmap2JpegBytes.In.of(inputPacket, jpegQuality))
+    }
+
+    private fun canLoadImageProcessingUtilJniLib(): Boolean {
+        try {
+            System.loadLibrary(ImageProcessingUtil.JNI_LIB_NAME)
+            return true
+        } catch (e: UnsatisfiedLinkError) {
+            Logger.d(TAG, "canLoadImageProcessingUtilJniLib", e)
+            return false
+        }
+    }
+
+    private companion object {
+        private const val TAG = "TakePictureManagerWrap"
+    }
+}
diff --git a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
index a61ada7..d7264b0 100644
--- a/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
+++ b/camera/camera-testing/src/test/java/androidx/camera/testing/fakes/FakeCameraControlTest.java
@@ -173,6 +173,42 @@
     }
 
     @Test
+    public void notifiesLastRequestOnCaptureCompleted() {
+        CameraCaptureResult captureResult = new FakeCameraCaptureResult();
+
+        CountDownLatch latch = new CountDownLatch(1);
+        List<CameraCaptureResult> resultList = new ArrayList<>();
+        CaptureConfig captureConfig1 = createCaptureConfig(new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+            }
+        }, new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+            }
+        });
+        CaptureConfig captureConfig2 = createCaptureConfig(new CameraCaptureCallback() {
+            @Override
+            public void onCaptureCompleted(int captureConfigId,
+                    @NonNull CameraCaptureResult cameraCaptureResult) {
+                resultList.add(cameraCaptureResult);
+                latch.countDown();
+            }
+        });
+
+        mCameraControl.submitStillCaptureRequests(Arrays.asList(captureConfig1, captureConfig2),
+                ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY, ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH);
+        mCameraControl.notifyLastRequestOnCaptureCompleted(captureResult);
+
+        awaitLatch(latch);
+        assertThat(resultList).containsExactlyElementsIn(Collections.singletonList(captureResult));
+    }
+
+    @Test
     public void canUpdateFlashModeToOff() {
         mCameraControl.setFlashMode(ImageCapture.FLASH_MODE_OFF);
         assertThat(mCameraControl.getFlashMode()).isEqualTo(ImageCapture.FLASH_MODE_OFF);
@@ -319,7 +355,7 @@
         List<CaptureConfig> notifiedCaptureConfigs = new ArrayList<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             notifiedCaptureConfigs.addAll(captureConfigs);
             latch.countDown();
         });
@@ -335,7 +371,7 @@
         AtomicReference<Thread> listenerThread = new AtomicReference<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(captureConfigs -> {
+        mCameraControl.addOnNewCaptureRequestListener(captureConfigs -> {
             listenerThread.set(Thread.currentThread());
             latch.countDown();
         });
@@ -350,7 +386,7 @@
         AtomicReference<Thread> listenerThread = new AtomicReference<>();
         CountDownLatch latch = new CountDownLatch(1);
 
-        mCameraControl.setOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
+        mCameraControl.addOnNewCaptureRequestListener(CameraXExecutors.mainThreadExecutor(),
                 captureConfigs -> {
                     listenerThread.set(Thread.currentThread());
                     latch.countDown();
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
new file mode 100644
index 0000000..9e9562c
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ResolutionSelectorDeviceTest.kt
@@ -0,0 +1,692 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.graphics.ImageFormat
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+import android.util.Log
+import android.util.Range
+import android.util.Rational
+import android.util.Size
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.AspectRatio.RATIO_16_9
+import androidx.camera.core.AspectRatio.RATIO_4_3
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.core.UseCase
+import androidx.camera.core.impl.CameraInfoInternal
+import androidx.camera.core.impl.ImageFormatConstants
+import androidx.camera.core.impl.ImageOutputConfig
+import androidx.camera.core.impl.Quirk
+import androidx.camera.core.impl.RestrictedCameraControl
+import androidx.camera.core.impl.utils.AspectRatioUtil
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_16_9
+import androidx.camera.core.impl.utils.AspectRatioUtil.ASPECT_RATIO_4_3
+import androidx.camera.core.impl.utils.AspectRatioUtil.hasMatchingAspectRatio
+import androidx.camera.core.impl.utils.CompareSizesByArea
+import androidx.camera.core.internal.utils.SizeUtil
+import androidx.camera.core.internal.utils.SizeUtil.RESOLUTION_1080P
+import androidx.camera.core.resolutionselector.AspectRatioStrategy
+import androidx.camera.core.resolutionselector.AspectRatioStrategy.FALLBACK_RULE_AUTO
+import androidx.camera.core.resolutionselector.ResolutionFilter
+import androidx.camera.core.resolutionselector.ResolutionSelector
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+import androidx.camera.core.resolutionselector.ResolutionSelector.PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+import androidx.camera.core.resolutionselector.ResolutionStrategy
+import androidx.camera.core.resolutionselector.ResolutionStrategy.FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.impl.CameraPipeConfigTestRule
+import androidx.camera.testing.impl.CameraUtil
+import androidx.camera.testing.impl.fakes.FakeLifecycleOwner
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.testutils.fail
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import org.junit.After
+import org.junit.Assume.assumeFalse
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+/**
+ * ResolutionSelector related test on the real device.
+ *
+ * Make the ResolutionSelectorDeviceTest focus on the generic ResolutionSelector selection results
+ * for all the normal devices. Skips the tests when the devices have any of the quirks that might
+ * affect the selected resolution.
+ */
+@LargeTest
+@RunWith(Parameterized::class)
+@SdkSuppress(minSdkVersion = 21)
+class ResolutionSelectorDeviceTest(
+    private val implName: String,
+    private var cameraSelector: CameraSelector,
+    private val cameraConfig: CameraXConfig,
+) {
+    @get:Rule
+    val cameraPipeConfigTestRule =
+        CameraPipeConfigTestRule(
+            active = implName.contains(CameraPipeConfig::class.simpleName!!),
+        )
+
+    @get:Rule
+    val cameraRule =
+        CameraUtil.grantCameraPermissionAndPreTestAndPostTest(
+            CameraUtil.PreTestCameraIdList(cameraConfig)
+        )
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule =
+        GrantPermissionRule.grant(Manifest.permission.RECORD_AUDIO)
+
+    private val useCaseFormatMap =
+        mapOf(
+            Pair(Preview::class.java, ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE),
+            Pair(ImageCapture::class.java, ImageFormat.JPEG),
+            Pair(ImageAnalysis::class.java, ImageFormat.YUV_420_888)
+        )
+
+    companion object {
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() =
+            listOf(
+                arrayOf(
+                    "back+" + Camera2Config::class.simpleName,
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    Camera2Config.defaultConfig(),
+                ),
+                arrayOf(
+                    "front+" + Camera2Config::class.simpleName,
+                    CameraSelector.DEFAULT_FRONT_CAMERA,
+                    Camera2Config.defaultConfig(),
+                ),
+                arrayOf(
+                    "back+" + CameraPipeConfig::class.simpleName,
+                    CameraSelector.DEFAULT_BACK_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                ),
+                arrayOf(
+                    "front+" + CameraPipeConfig::class.simpleName,
+                    CameraSelector.DEFAULT_FRONT_CAMERA,
+                    CameraPipeConfig.defaultConfig(),
+                ),
+            )
+    }
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val context: Context = ApplicationProvider.getApplicationContext()
+    private lateinit var cameraProvider: ProcessCameraProvider
+    private lateinit var lifecycleOwner: FakeLifecycleOwner
+    private lateinit var camera: Camera
+    private lateinit var cameraInfoInternal: CameraInfoInternal
+
+    @Before
+    fun initializeCameraX() {
+        assumeTrue(CameraUtil.hasCameraWithLensFacing(cameraSelector.lensFacing!!))
+        ProcessCameraProvider.configureInstance(cameraConfig)
+        cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+
+        instrumentation.runOnMainSync {
+            lifecycleOwner = FakeLifecycleOwner()
+            lifecycleOwner.startAndResume()
+
+            camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector)
+            cameraInfoInternal = camera.cameraInfo as CameraInfoInternal
+        }
+
+        assumeNotAspectRatioQuirkDevice()
+        assumeNotOutputSizeQuirkDevice()
+    }
+
+    @After
+    fun shutdownCameraX() {
+        if (::cameraProvider.isInitialized) {
+            cameraProvider.shutdownAsync()[10, TimeUnit.SECONDS]
+        }
+    }
+
+    @Test
+    fun canSelect4x3ResolutionForPreviewImageCaptureAndImageAnalysis() {
+        canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_4_3)
+    }
+
+    @Test
+    fun canSelect16x9ResolutionForPreviewImageCaptureAndImageAnalysis() {
+        canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(RATIO_16_9)
+    }
+
+    private fun canSelectTargetAspectRatioResolutionForPreviewImageCaptureAndImageAnalysis(
+        targetAspectRatio: Int
+    ) {
+        val preview = createUseCaseWithResolutionSelector(Preview::class.java, targetAspectRatio)
+        val imageCapture =
+            createUseCaseWithResolutionSelector(ImageCapture::class.java, targetAspectRatio)
+        val imageAnalysis =
+            createUseCaseWithResolutionSelector(ImageAnalysis::class.java, targetAspectRatio)
+        instrumentation.runOnMainSync {
+            cameraProvider.bindToLifecycle(
+                lifecycleOwner,
+                cameraSelector,
+                preview,
+                imageCapture,
+                imageAnalysis
+            )
+        }
+        assertThat(isResolutionAspectRatioBestMatched(preview, targetAspectRatio)).isTrue()
+        assertThat(isResolutionAspectRatioBestMatched(imageCapture, targetAspectRatio)).isTrue()
+        assertThat(isResolutionAspectRatioBestMatched(imageAnalysis, targetAspectRatio)).isTrue()
+    }
+
+    private fun isResolutionAspectRatioBestMatched(
+        useCase: UseCase,
+        targetAspectRatio: Int
+    ): Boolean {
+        val isMatched =
+            hasMatchingAspectRatio(
+                useCase.attachedSurfaceResolution!!,
+                aspectRatioToRational(targetAspectRatio)
+            )
+
+        if (isMatched) {
+            return true
+        }
+
+        // PRIV/PREVIEW + YUV/PREVIEW + JPEG/MAXIMUM will be used to select resolutions for the
+        // combination of Preview + ImageAnalysis + ImageCapture
+        val closestAspectRatioSizes =
+            if (useCase is Preview || useCase is ImageAnalysis) {
+                getClosestAspectRatioSizesUnderPreviewSize(targetAspectRatio, useCase.javaClass)
+            } else {
+                getClosestAspectRatioSizes(targetAspectRatio, useCase.javaClass)
+            }
+
+        Log.d(
+            "ResolutionSelectorDeviceTest",
+            "The selected resolution (${useCase.attachedSurfaceResolution!!}) does not exactly" +
+                " match the target aspect ratio. It is selected from the closest aspect ratio" +
+                " sizes: $closestAspectRatioSizes"
+        )
+
+        return closestAspectRatioSizes.contains(useCase.attachedSurfaceResolution!!)
+    }
+
+    @Test
+    fun canSelect4x3ResolutionForPreviewByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_4_3)
+
+    @Test
+    fun canSelect16x9ResolutionForPreviewByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(Preview::class.java, RATIO_16_9)
+
+    @Test
+    fun canSelect4x3ResolutionForImageCaptureByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_4_3)
+
+    @Test
+    fun canSelect16x9ResolutionForImageCaptureByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(ImageCapture::class.java, RATIO_16_9)
+
+    @Test
+    fun canSelect4x3ResolutionForImageAnalysisByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_4_3)
+
+    @Test
+    fun canSelect16x9ResolutionForImageAnalysisByResolutionStrategy() =
+        canSelectResolutionByResolutionStrategy(ImageAnalysis::class.java, RATIO_16_9)
+
+    private fun <T : UseCase> canSelectResolutionByResolutionStrategy(
+        useCaseClass: Class<T>,
+        ratio: Int
+    ) {
+        // Filters the output sizes matching the target aspect ratio
+        cameraInfoInternal
+            .getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+            .filter { size -> hasMatchingAspectRatio(size, aspectRatioToRational(ratio)) }
+            .let {
+                // Picks the item in the middle of the list to run the test
+                it.elementAtOrNull(it.size / 2)?.let { boundSize ->
+                    {
+                        val useCase =
+                            createUseCaseWithResolutionSelector(
+                                useCaseClass,
+                                aspectRatio = ratio,
+                                aspectRatioStrategyFallbackRule = FALLBACK_RULE_AUTO,
+                                boundSize = boundSize,
+                                resolutionStrategyFallbackRule =
+                                    FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+                            )
+                        instrumentation.runOnMainSync {
+                            cameraProvider.unbindAll()
+                            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+                        }
+                        assertThat(useCase.attachedSurfaceResolution).isEqualTo(boundSize)
+                    }
+                }
+            }
+    }
+
+    @Test
+    fun canSelectAnyResolutionForPreviewByResolutionFilter() =
+        canSelectAnyResolutionByResolutionFilter(
+            Preview::class.java,
+            // For Preview, need to override resolution strategy so that the output sizes larger
+            // than PREVIEW size can be selected.
+            cameraInfoInternal
+                .getSupportedResolutions(useCaseFormatMap[Preview::class.java]!!)
+                .maxWithOrNull(CompareSizesByArea())
+        )
+
+    @Test
+    fun canSelectAnyHighResolutionForPreviewByResolutionFilter() =
+        canSelectAnyHighResolutionByResolutionFilter(
+            Preview::class.java,
+            // For Preview, need to override resolution strategy so that the output sizes larger
+            // than PREVIEW size can be selected.
+            cameraInfoInternal
+                .getSupportedHighResolutions(useCaseFormatMap[Preview::class.java]!!)
+                .maxWithOrNull(CompareSizesByArea())
+        )
+
+    @Test
+    fun canSelectAnyResolutionForImageCaptureByResolutionFilter() =
+        canSelectAnyResolutionByResolutionFilter(ImageCapture::class.java)
+
+    @Test
+    fun canSelectAnyHighResolutionForImageCaptureByResolutionFilter() =
+        canSelectAnyHighResolutionByResolutionFilter(ImageCapture::class.java)
+
+    @Test
+    fun canSelectAnyResolutionForImageAnalysisByResolutionFilter() =
+        canSelectAnyResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+    @Test
+    fun canSelectAnyHighResolutionForImageAnalysisByResolutionFilter() =
+        canSelectAnyHighResolutionByResolutionFilter(ImageAnalysis::class.java)
+
+    private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+        useCaseClass: Class<T>,
+        boundSize: Size? = null,
+        resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+    ) =
+        canSelectAnyResolutionByResolutionFilter(
+            useCaseClass,
+            cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!),
+            boundSize,
+            resolutionStrategyFallbackRule
+        )
+
+    private fun <T : UseCase> canSelectAnyHighResolutionByResolutionFilter(
+        useCaseClass: Class<T>,
+        boundSize: Size? = null,
+        resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER
+    ) =
+        canSelectAnyResolutionByResolutionFilter(
+            useCaseClass,
+            cameraInfoInternal.getSupportedHighResolutions(useCaseFormatMap[useCaseClass]!!),
+            boundSize,
+            resolutionStrategyFallbackRule,
+            PREFER_HIGHER_RESOLUTION_OVER_CAPTURE_RATE
+        )
+
+    private fun <T : UseCase> canSelectAnyResolutionByResolutionFilter(
+        useCaseClass: Class<T>,
+        outputSizes: List<Size>,
+        boundSize: Size? = null,
+        resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+        allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+    ) {
+        outputSizes.forEach { targetResolution ->
+            val useCase =
+                createUseCaseWithResolutionSelector(
+                    useCaseClass,
+                    boundSize = boundSize,
+                    resolutionStrategyFallbackRule = resolutionStrategyFallbackRule,
+                    resolutionFilter = { _, _ -> mutableListOf(targetResolution) },
+                    allowedResolutionMode = allowedResolutionMode
+                )
+            instrumentation.runOnMainSync {
+                cameraProvider.unbindAll()
+                cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, useCase)
+            }
+            assertThat(useCase.attachedSurfaceResolution).isEqualTo(targetResolution)
+        }
+    }
+
+    @Test
+    fun canSelectResolutionForSixtyFpsPreview() {
+        assumeTrue(isSixtyFpsSupported())
+
+        val preview = Preview.Builder().setTargetFrameRate(Range.create(60, 60)).build()
+        val imageCapture = ImageCapture.Builder().build()
+
+        instrumentation.runOnMainSync {
+            cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageCapture)
+        }
+
+        assertThat(getMaxFrameRate(preview.attachedSurfaceResolution!!)).isEqualTo(60)
+    }
+
+    private fun isSixtyFpsSupported() =
+        CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)
+            ?.get(CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)
+            ?.any { range -> range.upper == 60 } ?: false
+
+    private fun getMaxFrameRate(size: Size) =
+        (1_000_000_000.0 /
+                CameraUtil.getCameraCharacteristics(cameraSelector.lensFacing!!)!!.get(
+                        CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+                    )!!
+                    .getOutputMinFrameDuration(SurfaceTexture::class.java, size) + 0.5)
+            .toInt()
+
+    private fun <T : UseCase> createUseCaseWithResolutionSelector(
+        useCaseClass: Class<T>,
+        aspectRatio: Int? = null,
+        aspectRatioStrategyFallbackRule: Int = FALLBACK_RULE_AUTO,
+        boundSize: Size? = null,
+        resolutionStrategyFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+        resolutionFilter: ResolutionFilter? = null,
+        allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+    ): UseCase {
+        val builder =
+            when (useCaseClass) {
+                Preview::class.java -> Preview.Builder()
+                ImageCapture::class.java -> ImageCapture.Builder()
+                ImageAnalysis::class.java -> ImageAnalysis.Builder()
+                else -> throw IllegalArgumentException("Unsupported class type!!")
+            }
+
+        (builder as ImageOutputConfig.Builder<*>).setResolutionSelector(
+            createResolutionSelector(
+                aspectRatio,
+                aspectRatioStrategyFallbackRule,
+                boundSize,
+                resolutionStrategyFallbackRule,
+                resolutionFilter,
+                allowedResolutionMode
+            )
+        )
+
+        return builder.build()
+    }
+
+    private fun createResolutionSelector(
+        aspectRatio: Int? = null,
+        aspectRatioFallbackRule: Int = FALLBACK_RULE_AUTO,
+        boundSize: Size? = null,
+        resolutionFallbackRule: Int = FALLBACK_RULE_CLOSEST_HIGHER_THEN_LOWER,
+        resolutionFilter: ResolutionFilter? = null,
+        allowedResolutionMode: Int = PREFER_CAPTURE_RATE_OVER_HIGHER_RESOLUTION
+    ) =
+        ResolutionSelector.Builder()
+            .apply {
+                aspectRatio?.let {
+                    setAspectRatioStrategy(
+                        AspectRatioStrategy(aspectRatio, aspectRatioFallbackRule)
+                    )
+                }
+                boundSize?.let {
+                    setResolutionStrategy(ResolutionStrategy(boundSize, resolutionFallbackRule))
+                }
+                resolutionFilter?.let { setResolutionFilter(resolutionFilter) }
+                setAllowedResolutionMode(allowedResolutionMode)
+            }
+            .build()
+
+    private fun aspectRatioToRational(ratio: Int) =
+        if (ratio == RATIO_16_9) {
+            ASPECT_RATIO_16_9
+        } else {
+            ASPECT_RATIO_4_3
+        }
+
+    private fun <T : UseCase> getClosestAspectRatioSizesUnderPreviewSize(
+        targetAspectRatio: Int,
+        useCaseClass: Class<T>
+    ): List<Size> {
+        val outputSizes =
+            cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+        return outputSizes
+            .getSmallerThanOrEqualToPreviewScaleSizeSublist()
+            .getClosestAspectRatioSublist(targetAspectRatio)
+    }
+
+    private fun <T : UseCase> getClosestAspectRatioSizes(
+        targetAspectRatio: Int,
+        useCaseClass: Class<T>
+    ): List<Size> {
+        val outputSizes =
+            cameraInfoInternal.getSupportedResolutions(useCaseFormatMap[useCaseClass]!!)
+        return outputSizes.getClosestAspectRatioSublist(targetAspectRatio)
+    }
+
+    private fun List<Size>.getSmallerThanOrEqualToPreviewScaleSizeSublist() = filter { size ->
+        SizeUtil.getArea(size) <= SizeUtil.getArea(getPreviewScaleSize())
+    }
+
+    @Suppress("DEPRECATION")
+    private fun getPreviewScaleSize(): Size {
+        val point = Point()
+        DisplayInfoManager.getInstance(context).getMaxSizeDisplay(false).getRealSize(point)
+        val displaySize = Size(point.x, point.y)
+        return if (SizeUtil.isSmallerByArea(RESOLUTION_1080P, displaySize)) {
+            RESOLUTION_1080P
+        } else {
+            displaySize
+        }
+    }
+
+    private fun List<Size>.getClosestAspectRatioSublist(targetAspectRatio: Int): List<Size> {
+        val sensorRect = (camera.cameraControl as RestrictedCameraControl).sensorRect
+        val aspectRatios = getResolutionListGroupingAspectRatioKeys(this)
+        val sortedAspectRatios =
+            aspectRatios.sortedWith(
+                AspectRatioUtil.CompareAspectRatiosByMappingAreaInFullFovAspectRatioSpace(
+                    aspectRatioToRational(targetAspectRatio),
+                    Rational(sensorRect.width(), sensorRect.height())
+                )
+            )
+        val groupedRatioToSizesMap = groupSizesByAspectRatio(this)
+
+        for (ratio in sortedAspectRatios) {
+            groupedRatioToSizesMap[ratio]?.let {
+                if (it.isNotEmpty()) {
+                    return it
+                }
+            }
+        }
+
+        fail("There should have one non-empty size list returned.")
+    }
+
+    /**
+     * Returns the grouping aspect ratio keys of the input resolution list.
+     *
+     * Some sizes might be mod16 case. When grouping, those sizes will be grouped into an existing
+     * aspect ratio group if the aspect ratio can match by the mod16 rule.
+     */
+    private fun getResolutionListGroupingAspectRatioKeys(
+        resolutionCandidateList: List<Size>
+    ): List<Rational> {
+        val aspectRatios = mutableListOf<Rational>()
+
+        // Adds the default 4:3 and 16:9 items first to avoid their mod16 sizes to create
+        // additional items.
+        aspectRatios.add(ASPECT_RATIO_4_3)
+        aspectRatios.add(ASPECT_RATIO_16_9)
+
+        // Tries to find the aspect ratio which the target size belongs to.
+        for (size in resolutionCandidateList) {
+            val newRatio = Rational(size.width, size.height)
+            val aspectRatioFound = aspectRatios.contains(newRatio)
+
+            // The checking size might be a mod16 size which can be mapped to an existing aspect
+            // ratio group.
+            if (!aspectRatioFound) {
+                var hasMatchingAspectRatio = false
+                for (aspectRatio in aspectRatios) {
+                    if (hasMatchingAspectRatio(size, aspectRatio)) {
+                        hasMatchingAspectRatio = true
+                        break
+                    }
+                }
+                if (!hasMatchingAspectRatio) {
+                    aspectRatios.add(newRatio)
+                }
+            }
+        }
+
+        return aspectRatios
+    }
+
+    /** Groups the input sizes into an aspect ratio to size list map. */
+    private fun groupSizesByAspectRatio(sizes: List<Size>): Map<Rational, MutableList<Size>> {
+        val aspectRatioSizeListMap = mutableMapOf<Rational, MutableList<Size>>()
+        val aspectRatioKeys = getResolutionListGroupingAspectRatioKeys(sizes)
+
+        for (aspectRatio in aspectRatioKeys) {
+            aspectRatioSizeListMap[aspectRatio] = mutableListOf()
+        }
+
+        for (outputSize in sizes) {
+            for (key in aspectRatioSizeListMap.keys) {
+                // Put the size into all groups that is matched in mod16 condition since a size
+                // may match multiple aspect ratio in mod16 algorithm.
+                if (hasMatchingAspectRatio(outputSize, key)) {
+                    aspectRatioSizeListMap[key]!!.add(outputSize)
+                }
+            }
+        }
+
+        return aspectRatioSizeListMap
+    }
+
+    // Skips the tests when the devices have any of the quirks that might affect the selected
+    // resolution.
+    private fun assumeNotAspectRatioQuirkDevice() {
+        assumeFalse(hasAspectRatioLegacyApi21Quirk())
+        assumeFalse(hasNexus4AndroidLTargetAspectRatioQuirk())
+        assumeFalse(hasExtraCroppingQuirk())
+    }
+
+    // Checks whether it is the device for AspectRatioLegacyApi21Quirk
+    private fun hasAspectRatioLegacyApi21Quirk(): Boolean {
+        val quirks = cameraInfoInternal.cameraQuirks
+
+        return if (implName == CameraPipeConfig::class.simpleName) {
+            quirks.contains(
+                androidx.camera.camera2.pipe.integration.compat.quirk
+                        .AspectRatioLegacyApi21Quirk::class
+                    .java
+            )
+        } else {
+            quirks.contains(
+                androidx.camera.camera2.internal.compat.quirk.AspectRatioLegacyApi21Quirk::class
+                    .java
+            )
+        }
+    }
+
+    // Checks whether it is the device for Nexus4AndroidLTargetAspectRatioQuirk
+    private fun hasNexus4AndroidLTargetAspectRatioQuirk() =
+        if (implName == CameraPipeConfig::class.simpleName) {
+            hasDeviceQuirk(
+                androidx.camera.camera2.pipe.integration.compat.quirk
+                        .Nexus4AndroidLTargetAspectRatioQuirk::class
+                    .java
+            )
+        } else {
+            hasDeviceQuirk(
+                androidx.camera.camera2.internal.compat.quirk
+                        .Nexus4AndroidLTargetAspectRatioQuirk::class
+                    .java
+            )
+        }
+
+    // Checks whether it is the device for ExtraCroppingQuirk
+    private fun hasExtraCroppingQuirk() =
+        if (implName == CameraPipeConfig::class.simpleName) {
+            hasDeviceQuirk(
+                androidx.camera.camera2.pipe.integration.compat.quirk.ExtraCroppingQuirk::class.java
+            )
+        } else {
+            hasDeviceQuirk(
+                androidx.camera.camera2.internal.compat.quirk.ExtraCroppingQuirk::class.java
+            )
+        }
+
+    // Skips the tests when the devices have any of the quirks that might affect the selected
+    // resolution.
+    private fun assumeNotOutputSizeQuirkDevice() {
+        assumeFalse(hasExcludedSupportedSizesQuirk())
+        assumeFalse(hasExtraSupportedOutputSizeQuirk())
+    }
+
+    private fun hasExcludedSupportedSizesQuirk() =
+        if (implName == CameraPipeConfig::class.simpleName) {
+            hasDeviceQuirk(
+                androidx.camera.camera2.pipe.integration.compat.quirk
+                        .ExcludedSupportedSizesQuirk::class
+                    .java
+            )
+        } else {
+            hasDeviceQuirk(
+                androidx.camera.camera2.internal.compat.quirk.ExcludedSupportedSizesQuirk::class
+                    .java
+            )
+        }
+
+    private fun hasExtraSupportedOutputSizeQuirk() =
+        if (implName == CameraPipeConfig::class.simpleName) {
+            hasDeviceQuirk(
+                androidx.camera.camera2.pipe.integration.compat.quirk
+                        .ExtraSupportedOutputSizeQuirk::class
+                    .java
+            )
+        } else {
+            hasDeviceQuirk(
+                androidx.camera.camera2.internal.compat.quirk.ExtraSupportedOutputSizeQuirk::class
+                    .java
+            )
+        }
+
+    private fun <T : Quirk?> hasDeviceQuirk(quirkClass: Class<T>) =
+        if (implName == CameraPipeConfig::class.simpleName) {
+            androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks.get(quirkClass)
+        } else {
+            androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.get(quirkClass)
+        } != null
+}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
index af52c44..ef4fe9e 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/fakecamera/ImageCaptureTest.kt
@@ -42,7 +42,6 @@
 import kotlinx.coroutines.withContext
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -87,7 +86,7 @@
     @Test
     fun canSubmitTakePictureRequest(): Unit = runBlocking {
         val countDownLatch = CountDownLatch(1)
-        cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+        cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
 
         imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
 
@@ -96,10 +95,9 @@
 
     // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
     // reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runBlocking {
-        val callback = FakeOnImageCapturedCallback()
+        val callback = FakeOnImageCapturedCallback(closeImageOnSuccess = false)
         imageCapture.takePicture(CameraXExecutors.directExecutor(), callback)
         callback.awaitCapturesAndAssert(capturedImagesCount = 1)
         callback.results.first().image.toBitmap()
@@ -107,7 +105,6 @@
 
     // Duplicate to ImageCaptureTest on core-test-app JVM tests, any change here may need to be
     // reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val saveLocation = temporaryFolder.newFile()
@@ -126,7 +123,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val initialCount = getMediaStoreCameraXImageCount()
diff --git a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
index 459187c..15cbefe 100644
--- a/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
+++ b/camera/integration-tests/coretestapp/src/test/java/androidx/camera/integration/core/ImageCaptureTest.kt
@@ -44,7 +44,6 @@
 import kotlinx.coroutines.test.runTest
 import org.junit.After
 import org.junit.Before
-import org.junit.Ignore
 import org.junit.Rule
 import org.junit.Test
 import org.junit.rules.TemporaryFolder
@@ -92,7 +91,7 @@
     @Test
     fun canSubmitTakePictureRequest(): Unit = runTest {
         val countDownLatch = CountDownLatch(1)
-        cameraControl.setOnNewCaptureRequestListener { countDownLatch.countDown() }
+        cameraControl.addOnNewCaptureRequestListener { countDownLatch.countDown() }
 
         imageCapture.takePicture(CameraXExecutors.directExecutor(), FakeOnImageCapturedCallback())
 
@@ -101,7 +100,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canCreateBitmapFromTakenImage_whenImageCapturedCallbackIsUsed(): Unit = runTest {
         val callback = FakeOnImageCapturedCallback()
@@ -112,7 +110,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindImage_whenFileStorageAndImageSavedCallbackIsUsed(): Unit = runTest {
         val saveLocation = temporaryFolder.newFile()
@@ -131,7 +128,6 @@
 
     // Duplicate to ImageCaptureTest on androidTest/fakecamera/ImageCaptureTest, any change here may
     // need to be reflected there too
-    @Ignore("b/318314454")
     @Test
     fun canFindFakeImageUri_whenMediaStoreAndImageSavedCallbackIsUsed(): Unit = runBlocking {
         val callback = FakeOnImageSavedCallback()
diff --git a/car/app/app/api/1.7.0-beta02.txt b/car/app/app/api/1.7.0-beta02.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/1.7.0-beta02.txt
+++ b/car/app/app/api/1.7.0-beta02.txt
@@ -900,9 +900,20 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
+  public class MediaIntentExtras {
+    field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+    field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+    field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+    field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+    field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+    field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+    field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+  }
+
   public final class MetadataExtras {
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/current.ignore b/car/app/app/api/current.ignore
new file mode 100644
index 0000000..e1d8307
--- /dev/null
+++ b/car/app/app/api/current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+    Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/current.txt b/car/app/app/api/current.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/current.txt
+++ b/car/app/app/api/current.txt
@@ -900,9 +900,20 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
+  public class MediaIntentExtras {
+    field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+    field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+    field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+    field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+    field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+    field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+    field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+  }
+
   public final class MetadataExtras {
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/restricted_1.7.0-beta02.txt b/car/app/app/api/restricted_1.7.0-beta02.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/restricted_1.7.0-beta02.txt
+++ b/car/app/app/api/restricted_1.7.0-beta02.txt
@@ -900,9 +900,20 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
+  public class MediaIntentExtras {
+    field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+    field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+    field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+    field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+    field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+    field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+    field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+  }
+
   public final class MetadataExtras {
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/api/restricted_current.ignore b/car/app/app/api/restricted_current.ignore
new file mode 100644
index 0000000..e1d8307
--- /dev/null
+++ b/car/app/app/api/restricted_current.ignore
@@ -0,0 +1,3 @@
+// Baseline format: 1.0
+AddedField: androidx.car.app.mediaextensions.MediaBrowserExtras#KEY_ROOT_HINT_MEDIA_HOST_VERSION:
+    Added field androidx.car.app.mediaextensions.MediaBrowserExtras.KEY_ROOT_HINT_MEDIA_HOST_VERSION
diff --git a/car/app/app/api/restricted_current.txt b/car/app/app/api/restricted_current.txt
index 4458c04..6939bce 100644
--- a/car/app/app/api/restricted_current.txt
+++ b/car/app/app/api/restricted_current.txt
@@ -900,9 +900,20 @@
     field public static final String KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_ITEMS_WHILE_RESTRICTED";
     field public static final String KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW = "androidx.car.app.mediaextensions.KEY_HINT_VIEW_MAX_LIST_ITEMS_COUNT_PER_ROW";
     field public static final String KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MAX_QUEUE_ITEMS_WHILE_RESTRICTED";
+    field public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
     field public static final String KEY_ROOT_HINT_MEDIA_SESSION_API = "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_SESSION_API";
   }
 
+  public class MediaIntentExtras {
+    field public static final String ACTION_MEDIA_TEMPLATE_V2 = "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+    field public static final String EXTRA_KEY_MEDIA_COMPONENT = "android.car.intent.extra.MEDIA_COMPONENT";
+    field public static final String EXTRA_KEY_MEDIA_ID = "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+    field public static final String EXTRA_KEY_SEARCH_ACTION = "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+    field public static final String EXTRA_KEY_SEARCH_QUERY = "android.car.media.extra.SEARCH_QUERY";
+    field public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0; // 0x0
+    field public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1; // 0x1
+  }
+
   public final class MetadataExtras {
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_LARGE_ICON_URI";
     field public static final String KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI = "androidx.car.app.mediaextensions.KEY_CONTENT_FORMAT_TINTABLE_SMALL_ICON_URI";
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
index 473f447..413cd71 100644
--- a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaBrowserExtras.java
@@ -37,6 +37,17 @@
 
     /**
      * {@link Bundle} key used in the rootHints bundle passed to
+     * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)}
+     * to indicate the version of the caller. Note that this should only be used for analytics and
+     * is different than {@link #KEY_ROOT_HINT_MEDIA_SESSION_API}.
+     *
+     * <p>TYPE: string - the version info.
+     */
+    public static final String KEY_ROOT_HINT_MEDIA_HOST_VERSION =
+            "androidx.car.app.mediaextensions.KEY_ROOT_HINT_MEDIA_HOST_VERSION";
+
+    /**
+     * {@link Bundle} key used in the rootHints bundle passed to
      * {@link androidx.media.MediaBrowserServiceCompat#onGetRoot(String, int, Bundle)} to indicate
      * which version of the media api is used by the caller
      *
diff --git a/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java
new file mode 100644
index 0000000..88bcb08
--- /dev/null
+++ b/car/app/app/src/main/java/androidx/car/app/mediaextensions/MediaIntentExtras.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.car.app.mediaextensions;
+
+/**
+ * Defines constants for action and extra keys for CarMediaApp.
+ */
+public class MediaIntentExtras {
+
+    // Do not instantiate
+    private MediaIntentExtras() {
+    }
+
+    /**
+     * Activity Action: Provide media playing through a media template app. The usage is the same as
+     * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#ACTION_MEDIA_TEMPLATE">ACTION_MEDIA_TEMPLATE</a>
+     * A V2 is provided so that the media apps can know whether the system they run on supports the
+     * new parameters.
+     * <p> Input: these optional extras
+     * <ul>
+     * <li> {@link #EXTRA_KEY_MEDIA_COMPONENT} </li>
+     * <li> {@link #EXTRA_KEY_MEDIA_ID} </li>
+     * <li> {@link #EXTRA_KEY_SEARCH_QUERY} </li>
+     * <li> {@link #EXTRA_KEY_SEARCH_ACTION} </li>
+     * </ul>
+     * If no extra is specified, the current media source is opened.
+     */
+    public static final String ACTION_MEDIA_TEMPLATE_V2 =
+            "androidx.car.app.mediaextensions.action.MEDIA_TEMPLATE_V2";
+
+    /**
+     * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+     * specify the MediaBrowserService that user wants to start the media on.
+     * <p>TYPE: String.
+     * The value of this extra is the same as the
+     * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#EXTRA_MEDIA_COMPONENT">EXTRA_MEDIA_COMPONENT</a>
+     * for easy access for 3P developers.
+     */
+    @SuppressWarnings("ActionValue")
+    public static final String EXTRA_KEY_MEDIA_COMPONENT =
+            "android.car.intent.extra.MEDIA_COMPONENT";
+
+    /**
+     * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+     * specify the media item that should be displayed in the browse view. Must match the ids used
+     * in the MediaBrowserServiceCompat api.
+     * <p>TYPE: String.
+     */
+    public static final String EXTRA_KEY_MEDIA_ID =
+            "androidx.car.app.mediaextensions.extra.KEY_MEDIA_ID";
+
+    /**
+     * {@link Bundle} key used as a string extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+     * specify the search query to send either to the current MediaBrowserService or the one
+     * specified with {@link #EXTRA_KEY_MEDIA_COMPONENT}.
+     * <p>TYPE: String.
+     * The value of this extra is the same as the
+     * <a href="https://developer.android.com/reference/android/car/media/CarMediaIntents#EXTRA_SEARCH_QUERY">EXTRA_SEARCH_QUERY</a>
+     * for easy access for 3P developers.
+     */
+    @SuppressWarnings("ActionValue")
+    public static final String EXTRA_KEY_SEARCH_QUERY =
+            "android.car.media.extra.SEARCH_QUERY";
+
+    /**
+     * {@link Bundle} key used as an int extra field with {@link #ACTION_MEDIA_TEMPLATE_V2} to
+     * specify the action for the Media Center to do after the search query is loaded.
+     * <p>TYPE: int.
+     * The value will be one of the following:
+     * {@link #EXTRA_VALUE_NO_SEARCH_ACTION},
+     * {@link #EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH},
+     * This extra should only be used together with {@link #EXTRA_KEY_SEARCH_QUERY}. If this extra
+     * is not specified, then no further action will be taken after the search results are loaded.
+     */
+    public static final String EXTRA_KEY_SEARCH_ACTION =
+            "androidx.car.app.mediaextensions.extra.KEY_SEARCH_ACTION";
+
+    /**
+     * The extra value to indicate that no further action will be taken after the search results are
+     * loaded from a search query. Used with {@link #EXTRA_KEY_SEARCH_QUERY}.
+     */
+    public static final int EXTRA_VALUE_NO_SEARCH_ACTION = 0;
+
+    /**
+     * The extra value to indicate that the first playable item will automatically be played from
+     * the displayed search results after a search query is done. Used with
+     * {@link #EXTRA_KEY_SEARCH_QUERY}.
+     */
+    public static final int EXTRA_VALUE_PLAY_FIRST_ITEM_FROM_SEARCH = 1;
+}
diff --git a/compose/material3/adaptive/adaptive-layout/build.gradle b/compose/material3/adaptive/adaptive-layout/build.gradle
index f37fbdb..2f637a0 100644
--- a/compose/material3/adaptive/adaptive-layout/build.gradle
+++ b/compose/material3/adaptive/adaptive-layout/build.gradle
@@ -111,10 +111,6 @@
     samples(project(":compose:material3:adaptive:adaptive-samples"))
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/adaptive/adaptive-navigation/build.gradle b/compose/material3/adaptive/adaptive-navigation/build.gradle
index dbbcdc2..7992414 100644
--- a/compose/material3/adaptive/adaptive-navigation/build.gradle
+++ b/compose/material3/adaptive/adaptive-navigation/build.gradle
@@ -109,10 +109,6 @@
     metalavaK2UastEnabled = false
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/adaptive/adaptive/build.gradle b/compose/material3/adaptive/adaptive/build.gradle
index 1cb594e..24f0da1 100644
--- a/compose/material3/adaptive/adaptive/build.gradle
+++ b/compose/material3/adaptive/adaptive/build.gradle
@@ -109,10 +109,6 @@
     metalavaK2UastEnabled = false
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 // Screenshot tests related setup
 android {
     compileSdk 35
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
index d57c4a2..da0d64f 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/IconButtonScreenshotTest.kt
@@ -24,6 +24,8 @@
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.FavoriteBorder
+import androidx.compose.material.icons.outlined.Favorite
 import androidx.compose.material.icons.outlined.FavoriteBorder
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
@@ -190,7 +192,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -202,7 +204,7 @@
         rule.setMaterialContent(darkColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 IconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -274,7 +276,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -290,7 +292,7 @@
                     onCheckedChange = { /* doSomething() */ },
                     enabled = false
                 ) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -302,7 +304,7 @@
         rule.setMaterialContent(darkColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 FilledIconToggleButton(checked = false, onCheckedChange = { /* doSomething() */ }) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -377,7 +379,7 @@
                     checked = false,
                     onCheckedChange = { /* doSomething() */ }
                 ) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -392,7 +394,7 @@
                     checked = false,
                     onCheckedChange = { /* doSomething() */ }
                 ) {
-                    Icon(Icons.Filled.Favorite, contentDescription = "Localized description")
+                    Icon(Icons.Outlined.Favorite, contentDescription = "Localized description")
                 }
             }
         }
@@ -434,10 +436,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 OutlinedIconButton(onClick = { /* doSomething() */ }) {
-                    Icon(
-                        Icons.Outlined.FavoriteBorder,
-                        contentDescription = "Localized description"
-                    )
+                    Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
                 }
             }
         }
@@ -449,10 +448,7 @@
         rule.setMaterialContent(lightColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 OutlinedIconButton(onClick = { /* doSomething() */ }, enabled = false) {
-                    Icon(
-                        Icons.Outlined.FavoriteBorder,
-                        contentDescription = "Localized description"
-                    )
+                    Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
                 }
             }
         }
@@ -464,10 +460,7 @@
         rule.setMaterialContent(darkColorScheme()) {
             Box(wrap.testTag(wrapperTestTag)) {
                 OutlinedIconButton(onClick = { /* doSomething() */ }) {
-                    Icon(
-                        Icons.Outlined.FavoriteBorder,
-                        contentDescription = "Localized description"
-                    )
+                    Icon(Icons.Filled.FavoriteBorder, contentDescription = "Localized description")
                 }
             }
         }
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
index 06b13ec..bfe9fa0 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/SplitButtonScreenshotTest.kt
@@ -23,7 +23,8 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.wrapContentSize
 import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.KeyboardArrowDown
 import androidx.compose.material.icons.outlined.KeyboardArrowDown
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Alignment
@@ -33,6 +34,7 @@
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performTouchInput
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.screenshot.AndroidXScreenshotTestRule
@@ -52,6 +54,8 @@
 
     private val wrap = Modifier.wrapContentSize(Alignment.Center)
     private val wrapperTestTag = "splitButtonWrapper"
+    private val leadingButtonTag = "leadingButton"
+    private val trailingButtonTag = "trailingButton"
 
     @Test
     fun splitButton() {
@@ -63,7 +67,7 @@
                             onClick = { /* Do Nothing */ },
                         ) {
                             Icon(
-                                Icons.Outlined.Edit,
+                                Icons.Filled.Edit,
                                 modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
                                 contentDescription = "Localized description",
                             )
@@ -100,7 +104,7 @@
                     checked = false,
                     leadingContent = {
                         Icon(
-                            Icons.Outlined.Edit,
+                            Icons.Filled.Edit,
                             contentDescription = "Localized description",
                             Modifier.size(SplitButtonDefaults.LeadingIconSize)
                         )
@@ -136,7 +140,7 @@
                     onTrailingButtonClick = {},
                     leadingContent = {
                         Icon(
-                            Icons.Outlined.Edit,
+                            Icons.Filled.Edit,
                             contentDescription = "Localized description",
                             Modifier.size(SplitButtonDefaults.LeadingIconSize)
                         )
@@ -145,7 +149,7 @@
                     },
                     trailingContent = {
                         Icon(
-                            Icons.Outlined.KeyboardArrowDown,
+                            Icons.Filled.KeyboardArrowDown,
                             modifier =
                                 Modifier.size(SplitButtonDefaults.TrailingIconSize).graphicsLayer {
                                     this.rotationZ = 180f
@@ -170,7 +174,7 @@
                     checked = false,
                     leadingContent = {
                         Icon(
-                            Icons.Outlined.Edit,
+                            Icons.Filled.Edit,
                             contentDescription = "Localized description",
                             Modifier.size(SplitButtonDefaults.LeadingIconSize)
                         )
@@ -203,7 +207,7 @@
                     checked = false,
                     leadingContent = {
                         Icon(
-                            Icons.Outlined.Edit,
+                            Icons.Filled.Edit,
                             contentDescription = "Localized description",
                             Modifier.size(SplitButtonDefaults.LeadingIconSize)
                         )
@@ -236,7 +240,7 @@
                     checked = false,
                     leadingContent = {
                         Icon(
-                            Icons.Outlined.Edit,
+                            Icons.Filled.Edit,
                             contentDescription = "Localized description",
                             Modifier.size(SplitButtonDefaults.LeadingIconSize)
                         )
@@ -269,7 +273,7 @@
                             onClick = { /* Do Nothing */ },
                         ) {
                             Icon(
-                                Icons.Outlined.Edit,
+                                Icons.Filled.Edit,
                                 contentDescription = "Localized description",
                                 Modifier.size(SplitButtonDefaults.LeadingIconSize)
                             )
@@ -322,6 +326,82 @@
         assertAgainstGolden("splitButton_textLeadingButton_${scheme.name}")
     }
 
+    @Test
+    fun splitButton_leadingButton_pressed() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                SplitButton(
+                    leadingButton = {
+                        SplitButtonDefaults.LeadingButton(
+                            onClick = { /* Do Nothing */ },
+                            modifier = Modifier.testTag(leadingButtonTag),
+                        ) {
+                            Icon(
+                                Icons.Filled.Edit,
+                                modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+                                contentDescription = "Localized description",
+                            )
+                            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+                            Text("My Button")
+                        }
+                    },
+                    trailingButton = {
+                        SplitButtonDefaults.TrailingButton(
+                            onClick = {},
+                            checked = false,
+                        ) {
+                            Icon(
+                                Icons.Outlined.KeyboardArrowDown,
+                                contentDescription = "Localized description",
+                                Modifier.size(SplitButtonDefaults.TrailingIconSize)
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertPressed(leadingButtonTag, "splitButton_leadingButton_pressed_${scheme.name}")
+    }
+
+    @Test
+    fun splitButton_trailingButton_pressed() {
+        rule.setMaterialContent(scheme.colorScheme) {
+            Box(wrap.testTag(wrapperTestTag)) {
+                SplitButton(
+                    leadingButton = {
+                        SplitButtonDefaults.LeadingButton(
+                            onClick = { /* Do Nothing */ },
+                        ) {
+                            Icon(
+                                Icons.Filled.Edit,
+                                modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize),
+                                contentDescription = "Localized description",
+                            )
+                            Spacer(Modifier.size(ButtonDefaults.IconSpacing))
+                            Text("My Button")
+                        }
+                    },
+                    trailingButton = {
+                        SplitButtonDefaults.TrailingButton(
+                            onClick = {},
+                            checked = false,
+                            modifier = Modifier.testTag(trailingButtonTag),
+                        ) {
+                            Icon(
+                                Icons.Outlined.KeyboardArrowDown,
+                                contentDescription = "Localized description",
+                                Modifier.size(SplitButtonDefaults.TrailingIconSize)
+                            )
+                        }
+                    }
+                )
+            }
+        }
+
+        assertPressed(trailingButtonTag, "splitButton_trailingButton_pressed_${scheme.name}")
+    }
+
     private fun assertAgainstGolden(goldenName: String) {
         rule
             .onNodeWithTag(wrapperTestTag)
@@ -329,6 +409,21 @@
             .assertAgainstGolden(screenshotRule, goldenName)
     }
 
+    private fun assertPressed(tag: String, goldenName: String) {
+        rule.mainClock.autoAdvance = false
+        rule.onNodeWithTag(tag).performTouchInput { down(center) }
+
+        rule.mainClock.advanceTimeByFrame()
+        rule.waitForIdle() // Wait for measure
+        rule.mainClock.advanceTimeBy(milliseconds = 200)
+
+        // Ripples are drawn on the RenderThread, not the main (UI) thread, so we can't wait for
+        // synchronization. Instead just wait until after the ripples are finished animating.
+        Thread.sleep(300)
+
+        assertAgainstGolden(goldenName)
+    }
+
     // Provide the ColorScheme and their name parameter in a ColorSchemeWrapper.
     // This makes sure that the default method name and the initial Scuba image generated
     // name is as expected.
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
index d6dcd91..a699d92 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/ToggleButtonScreenshotTest.kt
@@ -22,6 +22,7 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.outlined.Favorite
 import androidx.compose.testutils.assertAgainstGolden
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.platform.testTag
@@ -448,7 +449,7 @@
             Box(Modifier.testTag(wrapperTestTag)) {
                 ToggleButton(checked = false, onCheckedChange = {}) {
                     Icon(
-                        Icons.Filled.Favorite,
+                        Icons.Outlined.Favorite,
                         contentDescription = "Localized description",
                         modifier = Modifier.size(ToggleButtonDefaults.IconSize)
                     )
@@ -466,7 +467,7 @@
             Box(Modifier.testTag(wrapperTestTag)) {
                 ToggleButton(checked = false, onCheckedChange = {}, enabled = false) {
                     Icon(
-                        Icons.Filled.Favorite,
+                        Icons.Outlined.Favorite,
                         contentDescription = "Localized description",
                         modifier = Modifier.size(ToggleButtonDefaults.IconSize)
                     )
@@ -484,7 +485,7 @@
             Box(Modifier.testTag(wrapperTestTag)) {
                 ToggleButton(checked = false, onCheckedChange = {}) {
                     Icon(
-                        Icons.Filled.Favorite,
+                        Icons.Outlined.Favorite,
                         contentDescription = "Localized description",
                         modifier = Modifier.size(ToggleButtonDefaults.IconSize)
                     )
@@ -539,7 +540,7 @@
             Box(Modifier.testTag(wrapperTestTag)) {
                 ToggleButton(checked = false, onCheckedChange = {}) {
                     Icon(
-                        Icons.Filled.Favorite,
+                        Icons.Outlined.Favorite,
                         contentDescription = "Localized description",
                         modifier = Modifier.size(ToggleButtonDefaults.IconSize)
                     )
diff --git a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
index 2ca56a9..bf7443d 100644
--- a/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
+++ b/compose/material3/material3/src/androidInstrumentedTest/kotlin/androidx/compose/material3/anchoredDraggable/AnchoredDraggableStateTest.kt
@@ -697,7 +697,7 @@
                 initialValue = A,
                 defaultPositionalThreshold,
                 defaultVelocityThreshold,
-                animationSpec = defaultAnimationSpec,
+                animationSpec = defaultAnimationSpec
             )
         anchoredDraggableState.updateAnchors(
             DraggableAnchors {
@@ -735,40 +735,6 @@
         dragJob.cancel()
     }
 
-    @Test
-    fun anchoredDraggable_anchoredDrag_doesNotUpdateOnConfirmValueChange() = runTest {
-        val anchoredDraggableState =
-            AnchoredDraggableState(
-                initialValue = B,
-                defaultPositionalThreshold,
-                defaultVelocityThreshold,
-                animationSpec = defaultAnimationSpec,
-                confirmValueChange = { false }
-            )
-        anchoredDraggableState.updateAnchors(
-            DraggableAnchors {
-                A at 0f
-                B at 200f
-            }
-        )
-
-        assertThat(anchoredDraggableState.targetValue).isEqualTo(B)
-
-        val unexpectedTarget = A
-        val targetUpdates = Channel<Float>()
-        val dragJob =
-            launch(Dispatchers.Unconfined) {
-                anchoredDraggableState.anchoredDrag(unexpectedTarget) { anchors, latestTarget ->
-                    targetUpdates.send(anchors.positionOf(latestTarget))
-                    suspendIndefinitely()
-                }
-            }
-
-        val firstTarget = targetUpdates.receive()
-        assertThat(firstTarget).isEqualTo(200f)
-        dragJob.cancel()
-    }
-
     @OptIn(ExperimentalCoroutinesApi::class)
     @Test
     fun anchoredDraggable_dragCompletesExceptionally_cleansUp() = runTest {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
index 6f5dfbc..4ebaa83 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt
@@ -380,8 +380,14 @@
             modifier =
                 Modifier.offset {
                         drawerState.currentOffset.let { offset ->
-                            if (offset.isNaN()) IntOffset.Zero
-                            else IntOffset(offset.roundToInt(), 0)
+                            val offsetX =
+                                when {
+                                    !offset.isNaN() -> offset.roundToInt()
+                                    // If offset is NaN, set offset based on open/closed state
+                                    drawerState.isOpen -> 0
+                                    else -> -DrawerDefaults.MaximumDrawerWidth.roundToPx()
+                                }
+                            IntOffset(offsetX, 0)
                         }
                     }
                     .semantics {
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
index 4cf6819..63959a5 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/SplitButton.kt
@@ -477,7 +477,7 @@
     /** Default size for the leading button end corners and trailing button start corners */
     // TODO update token to dp size and use it here
     val InnerCornerSize = SplitButtonSmallTokens.InnerCornerSize
-    private val InnerCornerSizePressed = ShapeDefaults.CornerSmall
+    private val InnerCornerSizePressed = ShapeDefaults.CornerMedium
 
     /**
      * Default percentage size for the leading button start corners and trailing button end corners
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
index 09b667b..fa71348 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/internal/AnchoredDraggable.kt
@@ -565,7 +565,7 @@
         if (anchors.hasAnchorFor(targetValue)) {
             try {
                 dragMutex.mutate(dragPriority) {
-                    dragTarget = if (confirmValueChange(targetValue)) targetValue else currentValue
+                    dragTarget = targetValue
                     restartable(inputs = { anchors to [email protected] }) {
                         (latestAnchors, latestTarget) ->
                         anchoredDragScope.block(latestAnchors, latestTarget)
diff --git a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
index 4b1b684..e92c777 100644
--- a/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
+++ b/compose/runtime/runtime-test-utils/src/commonMain/kotlin/androidx/compose/runtime/mock/ViewApplier.kt
@@ -20,6 +20,8 @@
 
 @Suppress("EXTENSION_SHADOWED_BY_MEMBER")
 class ViewApplier(root: View) : AbstractApplier<View>(root) {
+    var called = false
+
     var onBeginChangesCalled = 0
         private set
 
@@ -28,29 +30,53 @@
 
     override fun insertTopDown(index: Int, instance: View) {
         // Ignored as the tree is built bottom-up.
+        called = true
     }
 
     override fun insertBottomUp(index: Int, instance: View) {
         current.addAt(index, instance)
+        called = true
     }
 
     override fun remove(index: Int, count: Int) {
         current.removeAt(index, count)
+        called = true
     }
 
     override fun move(from: Int, to: Int, count: Int) {
         current.moveAt(from, to, count)
+        called = true
     }
 
     override fun onClear() {
         root.removeAllChildren()
+        called = true
     }
 
     override fun onBeginChanges() {
         onBeginChangesCalled++
+        called = true
     }
 
     override fun onEndChanges() {
         onEndChangesCalled++
+        called = true
+    }
+
+    override var current: View
+        get() = super.current.also { if (it != root) called = true }
+        set(value) {
+            super.current = value
+            called = true
+        }
+
+    override fun down(node: View) {
+        super.down(node)
+        called = true
+    }
+
+    override fun up() {
+        super.up()
+        called = true
     }
 }
diff --git a/compose/runtime/runtime/api/current.ignore b/compose/runtime/runtime/api/current.ignore
index fc9bb64..f02ef5a 100644
--- a/compose/runtime/runtime/api/current.ignore
+++ b/compose/runtime/runtime/api/current.ignore
@@ -1,19 +1,3 @@
 // Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
-    Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
-    Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
-    Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+    Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/current.txt b/compose/runtime/runtime/api/current.txt
index 110569b..9212ade 100644
--- a/compose/runtime/runtime/api/current.txt
+++ b/compose/runtime/runtime/api/current.txt
@@ -22,6 +22,7 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+    method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
     method public void clear();
     method public void down(N node);
     method public N getCurrent();
@@ -31,6 +32,7 @@
     method public default void onBeginChanges();
     method public default void onEndChanges();
     method public void remove(int index, int count);
+    method public default void reuse();
     method public void up();
     property public abstract N current;
   }
@@ -286,6 +288,7 @@
     method public void recordModificationsOf(java.util.Set<?> values);
     method public void recordReadOf(Object value);
     method public void recordWriteOf(Object value);
+    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
     method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
     property public abstract boolean hasPendingChanges;
     property public abstract boolean isComposing;
@@ -459,6 +462,15 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
   }
 
+  public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+    method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public final class PausableCompositionKt {
+    method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+  }
+
   public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
     ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
     method public boolean isPaused();
@@ -468,6 +480,14 @@
     property public final boolean isPaused;
   }
 
+  public interface PausedComposition {
+    method public void apply();
+    method public void cancel();
+    method public boolean isComplete();
+    method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+    property public abstract boolean isComplete;
+  }
+
   public final class PrimitiveSnapshotStateKt {
     method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
     method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/api/restricted_current.ignore b/compose/runtime/runtime/api/restricted_current.ignore
index 5826792..f02ef5a 100644
--- a/compose/runtime/runtime/api/restricted_current.ignore
+++ b/compose/runtime/runtime/api/restricted_current.ignore
@@ -1,23 +1,3 @@
 // Baseline format: 1.0
-AddedAbstractMethod: androidx.compose.runtime.Composer#endReplaceGroup():
-    Added method androidx.compose.runtime.Composer.endReplaceGroup()
-AddedAbstractMethod: androidx.compose.runtime.Composer#startReplaceGroup(int):
-    Added method androidx.compose.runtime.Composer.startReplaceGroup(int)
-AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#abandonChanges():
-    Added method androidx.compose.runtime.ControlledComposition.abandonChanges()
-
-
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#key(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.key(Object[] keys, kotlin.jvm.functions.Function0<? extends T> block)
-InvalidNullConversion: androidx.compose.runtime.ComposablesKt#remember(Object[], kotlin.jvm.functions.Function0<? extends T>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.ComposablesKt.remember(Object[] keys, kotlin.jvm.functions.Function0<? extends T> calculation)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#DisposableEffect(Object[], kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.DisposableEffect(Object[] keys, kotlin.jvm.functions.Function1<? super androidx.compose.runtime.DisposableEffectScope,? extends androidx.compose.runtime.DisposableEffectResult> effect)
-InvalidNullConversion: androidx.compose.runtime.EffectsKt#LaunchedEffect(Object[], kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #0:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.EffectsKt.LaunchedEffect(Object[] keys, kotlin.jvm.functions.Function2<? super kotlinx.coroutines.CoroutineScope,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> block)
-InvalidNullConversion: androidx.compose.runtime.SnapshotStateKt#produceState(T, Object[], kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>) parameter #1:
-    Attempted to change parameter from @Nullable to @NonNull: incompatible change for parameter keys in androidx.compose.runtime.SnapshotStateKt.produceState(T initialValue, Object[] keys, kotlin.jvm.functions.Function2<? super androidx.compose.runtime.ProduceStateScope<T>,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?> producer)
-
-
-RemovedClass: androidx.compose.runtime.ExpectKt:
-    Removed class androidx.compose.runtime.ExpectKt
+AddedAbstractMethod: androidx.compose.runtime.ControlledComposition#setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>):
+    Added method androidx.compose.runtime.ControlledComposition.setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>)
diff --git a/compose/runtime/runtime/api/restricted_current.txt b/compose/runtime/runtime/api/restricted_current.txt
index 186ecc4..9580ec3 100644
--- a/compose/runtime/runtime/api/restricted_current.txt
+++ b/compose/runtime/runtime/api/restricted_current.txt
@@ -26,6 +26,7 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface Applier<N> {
+    method public default void apply(kotlin.jvm.functions.Function2<? super N,java.lang.Object?,kotlin.Unit> block, Object? value);
     method public void clear();
     method public void down(N node);
     method public N getCurrent();
@@ -35,6 +36,7 @@
     method public default void onBeginChanges();
     method public default void onEndChanges();
     method public void remove(int index, int count);
+    method public default void reuse();
     method public void up();
     property public abstract N current;
   }
@@ -313,6 +315,7 @@
     method public void recordModificationsOf(java.util.Set<?> values);
     method public void recordReadOf(Object value);
     method public void recordWriteOf(Object value);
+    method public kotlin.jvm.functions.Function0<java.lang.Boolean>? setShouldPauseCallback(kotlin.jvm.functions.Function0<java.lang.Boolean>? shouldPause);
     method @SuppressCompatibility @androidx.compose.runtime.InternalComposeApi public void verifyConsistent();
     property public abstract boolean hasPendingChanges;
     property public abstract boolean isComposing;
@@ -487,6 +490,15 @@
   @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.SOURCE) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER}) public @interface NonSkippableComposable {
   }
 
+  public interface PausableComposition extends androidx.compose.runtime.ReusableComposition {
+    method public androidx.compose.runtime.PausedComposition setPausableContent(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+    method public androidx.compose.runtime.PausedComposition setPausableContentWithReuse(kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  public final class PausableCompositionKt {
+    method public static androidx.compose.runtime.PausableComposition PausableComposition(androidx.compose.runtime.Applier<? extends java.lang.Object?> applier, androidx.compose.runtime.CompositionContext parent);
+  }
+
   public final class PausableMonotonicFrameClock implements androidx.compose.runtime.MonotonicFrameClock {
     ctor public PausableMonotonicFrameClock(androidx.compose.runtime.MonotonicFrameClock frameClock);
     method public boolean isPaused();
@@ -496,6 +508,14 @@
     property public final boolean isPaused;
   }
 
+  public interface PausedComposition {
+    method public void apply();
+    method public void cancel();
+    method public boolean isComplete();
+    method public boolean resume(kotlin.jvm.functions.Function0<java.lang.Boolean> shouldPause);
+    property public abstract boolean isComplete;
+  }
+
   public final class PrimitiveSnapshotStateKt {
     method public static inline operator float getValue(androidx.compose.runtime.FloatState, Object? thisObj, kotlin.reflect.KProperty<? extends java.lang.Object?> property);
     method @androidx.compose.runtime.snapshots.StateFactoryMarker public static androidx.compose.runtime.MutableFloatState mutableFloatStateOf(float value);
diff --git a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
index 7335b2c..d298a52 100644
--- a/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
+++ b/compose/runtime/runtime/compose-runtime-benchmark/src/androidTest/java/androidx/compose/runtime/benchmark/ComposeBenchmark.kt
@@ -17,6 +17,7 @@
 package androidx.compose.runtime.benchmark
 
 import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material.Text
@@ -30,8 +31,13 @@
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.text.drawText
+import androidx.compose.ui.text.rememberTextMeasurer
 import androidx.compose.ui.unit.dp
 import androidx.test.annotation.UiThreadTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -236,6 +242,21 @@
     fun benchmark_f_compose_Rect_100() = runBlockingTestWithFrameClock {
         measureComposeFocused { repeat(100) { Rect() } }
     }
+
+    @UiThreadTest
+    @Test
+    fun benchmark_g_group_eliding_focused_1000() = runBlockingTestWithFrameClock {
+        measureCompose { repeat(1000) { MyLayout { SimpleText("Value: $it") } } }
+    }
+}
+
+@Composable
+fun MyLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Layout(content = content, measurePolicy = EmptyMeasurePolicy, modifier = modifier)
+}
+
+internal val EmptyMeasurePolicy = MeasurePolicy { _, constraints ->
+    layout(constraints.minWidth, constraints.minHeight) {}
 }
 
 class ColorModel(color: Color = Color.Black) {
@@ -254,6 +275,12 @@
 }
 
 @Composable
+private fun SimpleText(text: String) {
+    val measurer = rememberTextMeasurer()
+    Box(modifier = Modifier.drawBehind { drawText(measurer, text) })
+}
+
+@Composable
 private fun Rect(color: Color) {
     val modifier = remember(color) { Modifier.background(color) }
     Column(modifier) {}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
index e1cd951..5b33661 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt
@@ -174,6 +174,16 @@
      * root to be used as the target of a new composition in the future.
      */
     fun clear()
+
+    /** Apply a change to the current node. */
+    fun apply(block: N.(Any?) -> Unit, value: Any?) {
+        current.block(value)
+    }
+
+    /** Notify [current] is is being reused in reusable content. */
+    fun reuse() {
+        (current as? ComposeNodeLifecycleCallback)?.onReuse()
+    }
 }
 
 /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
index 28f2850..172e582 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt
@@ -101,6 +101,15 @@
         priority: Int,
         endRelativeAfter: Int
     )
+
+    /** The restart scope is pausing */
+    fun rememberPausingScope(scope: RecomposeScopeImpl)
+
+    /** The restart scope is resuming */
+    fun startResumingScope(scope: RecomposeScopeImpl)
+
+    /** The restart scope is finished resuming */
+    fun endResumingScope(scope: RecomposeScopeImpl)
 }
 
 /**
@@ -1356,6 +1365,9 @@
     private var insertAnchor: Anchor = insertTable.read { it.anchor(0) }
     private var insertFixups = FixupList()
 
+    private var pausable: Boolean = false
+    private var shouldPauseCallback: (() -> Boolean)? = null
+
     override val applyCoroutineContext: CoroutineContext
         @TestOnly get() = parentContext.effectCoroutineContext
 
@@ -2726,7 +2738,10 @@
                 providerCache = null
 
                 // Invoke the scope's composition function
+                val shouldRestartReusing = !reusing && firstInRange.scope.reusing
+                if (shouldRestartReusing) reusing = true
                 firstInRange.scope.compose(this)
+                if (shouldRestartReusing) reusing = false
 
                 // We could have moved out of a provider so the provider cache is invalid.
                 providerCache = null
@@ -3038,8 +3053,34 @@
     }
 
     @ComposeCompilerApi
-    @Suppress("UNUSED")
     override fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean {
+        // We only want to pause when we are not resuming and only when inserting new content or
+        // when reusing content. This 0 bit of `flags` is only 1 if this function was restarted by
+        // the restart lambda. The other bits of this flags are currently all 0's and are reserved
+        // for future use.
+        if (((flags and 1) == 0) && (inserting || reusing)) {
+            val callback = shouldPauseCallback ?: return true
+            val scope = currentRecomposeScope ?: return true
+            val pausing = callback()
+            if (pausing) {
+                scope.used = true
+                // Force the composer back into the reusing state when this scope restarts.
+                scope.reusing = reusing
+                scope.paused = true
+                // Remember a place-holder object to ensure all remembers are sent in the correct
+                // order. The remember manager will record the remember callback for the resumed
+                // content into a place-holder to ensure that, when the remember callbacks are
+                // dispatched, the callbacks for the resumed content are dispatched in the same
+                // order they would have been had the content not paused.
+                changeListWriter.rememberPausingScope(scope)
+                parentContext.reportPausedScope(scope)
+                return false
+            }
+            return true
+        }
+
+        // Otherwise we should execute the function if the parameters have changed or when
+        // skipping is disabled.
         return parametersChanged || !skipping
     }
 
@@ -3118,6 +3159,11 @@
                     }
             invalidateStack.push(scope)
             scope.start(compositionToken)
+            if (scope.paused) {
+                scope.paused = false
+                scope.resuming = true
+                changeListWriter.startResumingScope(scope)
+            }
         }
     }
 
@@ -3133,8 +3179,16 @@
         // exception stack unwinding that might have not called the doneJoin/endRestartGroup in the
         // the correct order.
         val scope = if (invalidateStack.isNotEmpty()) invalidateStack.pop() else null
-        scope?.requiresRecompose = false
-        scope?.end(compositionToken)?.let { changeListWriter.endCompositionScope(it, composition) }
+        if (scope != null) {
+            scope.requiresRecompose = false
+            scope.end(compositionToken)?.let {
+                changeListWriter.endCompositionScope(it, composition)
+            }
+            if (scope.resuming) {
+                scope.resuming = false
+                changeListWriter.endResumingScope(scope)
+            }
+        }
         val result =
             if (scope != null && !scope.skipped && (scope.used || forceRecomposeScopes)) {
                 if (scope.anchor == null) {
@@ -3438,10 +3492,16 @@
      */
     internal fun composeContent(
         invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
-        content: @Composable () -> Unit
+        content: @Composable () -> Unit,
+        shouldPause: (() -> Boolean)?
     ) {
         runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
-        doCompose(invalidationsRequested, content)
+        this.shouldPauseCallback = shouldPause
+        try {
+            doCompose(invalidationsRequested, content)
+        } finally {
+            this.shouldPauseCallback = null
+        }
     }
 
     internal fun prepareCompose(block: () -> Unit) {
@@ -3460,6 +3520,7 @@
      */
     internal fun recompose(
         invalidationsRequested: ScopeMap<RecomposeScopeImpl, Any>,
+        shouldPause: (() -> Boolean)?
     ): Boolean {
         runtimeCheck(changes.isEmpty()) { "Expected applyChanges() to have been called" }
         // even if invalidationsRequested is empty we still need to recompose if the Composer has
@@ -3467,7 +3528,12 @@
         // there were a change for a state which was used by the child composition. such changes
         // will be tracked and added into `invalidations` list.
         if (invalidationsRequested.size > 0 || invalidations.isNotEmpty() || forciblyRecompose) {
-            doCompose(invalidationsRequested, null)
+            shouldPauseCallback = shouldPause
+            try {
+                doCompose(invalidationsRequested, null)
+            } finally {
+                shouldPauseCallback = null
+            }
             return changes.isNotEmpty()
         }
         return false
@@ -3786,6 +3852,10 @@
             parentContext.unregisterComposition(composition)
         }
 
+        override fun reportPausedScope(scope: RecomposeScopeImpl) {
+            parentContext.reportPausedScope(scope)
+        }
+
         override val effectCoroutineContext: CoroutineContext
             get() = parentContext.effectCoroutineContext
 
@@ -3802,6 +3872,20 @@
             parentContext.composeInitial(composition, content)
         }
 
+        override fun composeInitialPaused(
+            composition: ControlledComposition,
+            shouldPause: () -> Boolean,
+            content: @Composable () -> Unit
+        ): ScatterSet<RecomposeScopeImpl> =
+            parentContext.composeInitialPaused(composition, shouldPause, content)
+
+        override fun recomposePaused(
+            composition: ControlledComposition,
+            shouldPause: () -> Boolean,
+            invalidScopes: ScatterSet<RecomposeScopeImpl>
+        ): ScatterSet<RecomposeScopeImpl> =
+            parentContext.recomposePaused(composition, shouldPause, invalidScopes)
+
         override fun invalidate(composition: ControlledComposition) {
             // Invalidate ourselves with our parent before we invalidate a child composer.
             // This ensures that when we are scheduling recompositions, parents always
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 e2efd82..f674dbe 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
@@ -18,13 +18,12 @@
 
 package androidx.compose.runtime
 
-import androidx.collection.MutableIntList
 import androidx.collection.MutableScatterSet
-import androidx.collection.mutableScatterSetOf
 import androidx.compose.runtime.changelist.ChangeList
 import androidx.compose.runtime.collection.ScopeMap
 import androidx.compose.runtime.collection.fastForEach
 import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.RememberEventDispatcher
 import androidx.compose.runtime.internal.trace
 import androidx.compose.runtime.snapshots.ReaderKind
 import androidx.compose.runtime.snapshots.StateObjectImpl
@@ -289,6 +288,29 @@
      * used to compose as if the scopes have already been changed.
      */
     fun <R> delegateInvalidations(to: ControlledComposition?, groupIndex: Int, block: () -> R): R
+
+    /**
+     * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`.
+     * Setting the callback to `null` disables pausing.
+     *
+     * @return the previous value of the callback which will be restored once the callback is no
+     *   longer needed.
+     * @see PausableComposition
+     */
+    fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)?
+}
+
+/** Utility function to set and restore a should pause callback. */
+internal inline fun <R> ControlledComposition.pausable(
+    noinline shouldPause: () -> Boolean,
+    block: () -> R
+): R {
+    val previous = setShouldPauseCallback(shouldPause)
+    return try {
+        block()
+    } finally {
+        setShouldPauseCallback(previous)
+    }
 }
 
 /**
@@ -409,7 +431,12 @@
     /** The applier to use to update the tree managed by the composition. */
     private val applier: Applier<*>,
     recomposeContext: CoroutineContext? = null
-) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices {
+) :
+    ControlledComposition,
+    ReusableComposition,
+    RecomposeScopeOwner,
+    CompositionServices,
+    PausableComposition {
     /**
      * `null` if a composition isn't pending to apply. `Set<Any>` or `Array<Set<Any>>` if there are
      * modifications to record [PendingApplyNoModifications] if a composition is pending to apply,
@@ -520,6 +547,14 @@
     @Suppress("MemberVisibilityCanBePrivate") // published as internal
     internal var pendingInvalidScopes = false
 
+    /**
+     * If the [shouldPause] callback is set the composition is pausable and should pause whenever
+     * the [shouldPause] callback returns `true`.
+     */
+    private var shouldPause: (() -> Boolean)? = null
+
+    private var pendingPausedComposition: PausedCompositionImpl? = null
+
     private var invalidationDelegate: CompositionImpl? = null
 
     private var invalidationDelegateGroup: Int = 0
@@ -572,10 +607,16 @@
         get() = synchronized(lock) { composer.hasPendingChanges }
 
     override fun setContent(content: @Composable () -> Unit) {
+        checkPrecondition(pendingPausedComposition == null) {
+            "A pausable composition is in progress"
+        }
         composeInitial(content)
     }
 
     override fun setContentWithReuse(content: @Composable () -> Unit) {
+        checkPrecondition(pendingPausedComposition == null) {
+            "A pausable composition is in progress"
+        }
         composer.startReuseFromRoot()
 
         composeInitial(content)
@@ -583,6 +624,50 @@
         composer.endReuseFromRoot()
     }
 
+    override fun setPausableContent(content: @Composable () -> Unit): PausedComposition {
+        checkPrecondition(!disposed) { "The composition is disposed" }
+        checkPrecondition(pendingPausedComposition == null) {
+            "A pausable composition is in progress"
+        }
+        val pausedComposition =
+            PausedCompositionImpl(
+                composition = this,
+                context = parent,
+                composer = composer,
+                content = content,
+                reusable = false,
+                abandonSet = abandonSet,
+                applier = applier,
+                lock = lock,
+            )
+        pendingPausedComposition = pausedComposition
+        return pausedComposition
+    }
+
+    override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition {
+        checkPrecondition(!disposed) { "The composition is disposed" }
+        checkPrecondition(pendingPausedComposition == null) {
+            "A pausable composition is in progress"
+        }
+        val pausedComposition =
+            PausedCompositionImpl(
+                composition = this,
+                context = parent,
+                composer = composer,
+                content = content,
+                reusable = true,
+                abandonSet = abandonSet,
+                applier = applier,
+                lock = lock,
+            )
+        pendingPausedComposition = pausedComposition
+        return pausedComposition
+    }
+
+    internal fun pausedCompositionFinished() {
+        pendingPausedComposition = null
+    }
+
     private fun composeInitial(content: @Composable () -> Unit) {
         checkPrecondition(!disposed) { "The composition is disposed" }
         this.composable = content
@@ -701,7 +786,7 @@
                             invalidations.asMap() as Map<RecomposeScope, Set<Any>>
                         )
                     }
-                    composer.composeContent(invalidations, content)
+                    composer.composeContent(invalidations, content, shouldPause)
                     observer?.onEndComposition(this)
                 }
             }
@@ -911,7 +996,7 @@
                         this,
                         invalidations.asMap() as Map<RecomposeScope, Set<Any>>
                     )
-                    composer.recompose(invalidations).also { shouldDrain ->
+                    composer.recompose(invalidations, shouldPause).also { shouldDrain ->
                         // Apply would normally do this for us; do it now if apply shouldn't happen.
                         if (!shouldDrain) drainPendingModificationsLocked()
                         observer?.onEndComposition(this)
@@ -939,11 +1024,13 @@
         try {
             if (changes.isEmpty()) return
             trace("Compose:applyChanges") {
+                val applier = pendingPausedComposition?.pausableApplier ?: applier
+                val rememberManager = pendingPausedComposition?.rememberManager ?: manager
                 applier.onBeginChanges()
 
                 // Apply all changes
                 slotTable.write { slots ->
-                    changes.executeAndFlushAllPendingChanges(applier, slots, manager)
+                    changes.executeAndFlushAllPendingChanges(applier, slots, rememberManager)
                 }
                 applier.onEndChanges()
             }
@@ -962,9 +1049,12 @@
                 }
             }
         } finally {
-            // Only dispatch abandons if we do not have any late changes. The instances in the
-            // abandon set can be remembered in the late changes.
-            if (this.lateChanges.isEmpty()) manager.dispatchAbandons()
+            // Only dispatch abandons if we do not have any late changes or pending paused
+            // compositions. The instances in the abandon set can be remembered in the late changes
+            // or when the paused composition is applied.
+            if (this.lateChanges.isEmpty() && pendingPausedComposition == null) {
+                manager.dispatchAbandons()
+            }
         }
     }
 
@@ -1062,6 +1152,12 @@
         } else block()
     }
 
+    override fun setShouldPauseCallback(shouldPause: (() -> Boolean)?): (() -> Boolean)? {
+        val previous = this.shouldPause
+        this.shouldPause = shouldPause
+        return previous
+    }
+
     override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult {
         if (scope.defaultsInScope) {
             scope.defaultsInvalid = true
@@ -1241,218 +1337,6 @@
 
     // This is only used in tests to ensure the stacks do not silently leak.
     internal fun composerStacksSizes(): Int = composer.stacksSize()
-
-    /** Helper for collecting remember observers for later strictly ordered dispatch. */
-    private class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
-        RememberManager {
-        private val remembering = mutableListOf<RememberObserver>()
-        private val leaving = mutableListOf<Any>()
-        private val sideEffects = mutableListOf<() -> Unit>()
-        private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
-        private val pending = mutableListOf<Any>()
-        private val priorities = MutableIntList()
-        private val afters = MutableIntList()
-
-        override fun remembering(instance: RememberObserver) {
-            remembering.add(instance)
-        }
-
-        override fun forgetting(
-            instance: RememberObserver,
-            endRelativeOrder: Int,
-            priority: Int,
-            endRelativeAfter: Int
-        ) {
-            recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
-        }
-
-        override fun sideEffect(effect: () -> Unit) {
-            sideEffects += effect
-        }
-
-        override fun deactivating(
-            instance: ComposeNodeLifecycleCallback,
-            endRelativeOrder: Int,
-            priority: Int,
-            endRelativeAfter: Int
-        ) {
-            recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
-        }
-
-        override fun releasing(
-            instance: ComposeNodeLifecycleCallback,
-            endRelativeOrder: Int,
-            priority: Int,
-            endRelativeAfter: Int
-        ) {
-            val releasing =
-                releasing
-                    ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
-
-            releasing += instance
-            recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
-        }
-
-        fun dispatchRememberObservers() {
-            // Add any pending out-of-order forgotten objects
-            processPendingLeaving(Int.MIN_VALUE)
-
-            // Send forgets and node callbacks
-            if (leaving.isNotEmpty()) {
-                trace("Compose:onForgotten") {
-                    val releasing = releasing
-                    for (i in leaving.size - 1 downTo 0) {
-                        val instance = leaving[i]
-                        if (instance is RememberObserver) {
-                            abandoning.remove(instance)
-                            instance.onForgotten()
-                        }
-                        if (instance is ComposeNodeLifecycleCallback) {
-                            // node callbacks are in the same queue as forgets to ensure ordering
-                            if (releasing != null && instance in releasing) {
-                                instance.onRelease()
-                            } else {
-                                instance.onDeactivate()
-                            }
-                        }
-                    }
-                }
-            }
-
-            // Send remembers
-            if (remembering.isNotEmpty()) {
-                trace("Compose:onRemembered") {
-                    remembering.fastForEach { instance ->
-                        abandoning.remove(instance)
-                        instance.onRemembered()
-                    }
-                }
-            }
-        }
-
-        fun dispatchSideEffects() {
-            if (sideEffects.isNotEmpty()) {
-                trace("Compose:sideeffects") {
-                    sideEffects.fastForEach { sideEffect -> sideEffect() }
-                    sideEffects.clear()
-                }
-            }
-        }
-
-        fun dispatchAbandons() {
-            if (abandoning.isNotEmpty()) {
-                trace("Compose:abandons") {
-                    val iterator = abandoning.iterator()
-                    // remove elements one by one to ensure that abandons will not be dispatched
-                    // second time in case [onAbandoned] throws.
-                    while (iterator.hasNext()) {
-                        val instance = iterator.next()
-                        iterator.remove()
-                        instance.onAbandoned()
-                    }
-                }
-            }
-        }
-
-        private fun recordLeaving(
-            instance: Any,
-            endRelativeOrder: Int,
-            priority: Int,
-            endRelativeAfter: Int
-        ) {
-            processPendingLeaving(endRelativeOrder)
-            if (endRelativeAfter in 0 until endRelativeOrder) {
-                pending.add(instance)
-                priorities.add(priority)
-                afters.add(endRelativeAfter)
-            } else {
-                leaving.add(instance)
-            }
-        }
-
-        private fun processPendingLeaving(endRelativeOrder: Int) {
-            if (pending.isNotEmpty()) {
-                var index = 0
-                var toAdd: MutableList<Any>? = null
-                var toAddAfter: MutableIntList? = null
-                var toAddPriority: MutableIntList? = null
-                while (index < afters.size) {
-                    if (endRelativeOrder <= afters[index]) {
-                        val instance = pending.removeAt(index)
-                        val endRelativeAfter = afters.removeAt(index)
-                        val priority = priorities.removeAt(index)
-
-                        if (toAdd == null) {
-                            toAdd = mutableListOf(instance)
-                            toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
-                            toAddPriority = MutableIntList().also { it.add(priority) }
-                        } else {
-                            toAddPriority as MutableIntList
-                            toAddAfter as MutableIntList
-                            toAdd.add(instance)
-                            toAddAfter.add(endRelativeAfter)
-                            toAddPriority.add(priority)
-                        }
-                    } else {
-                        index++
-                    }
-                }
-                if (toAdd != null) {
-                    toAddPriority as MutableIntList
-                    toAddAfter as MutableIntList
-
-                    // Sort the list into [after, -priority] order where it is ordered by after
-                    // in ascending order as the primary key and priority in descending order as
-                    // secondary key.
-
-                    // For example if remember occurs after a child group it must be added after
-                    // all the remembers of the child. This is reported with an after which is the
-                    // slot index of the child's last slot. As this slot might be at the same
-                    // location as where its parents ends this would be ambiguous which should
-                    // first if both the two groups request a slot to be after the same slot.
-                    // Priority is used to break the tie here which is the group index of the group
-                    // which is leaving. Groups that are lower must be added before the parent's
-                    // remember when they have the same after.
-
-                    // The sort must be stable as as consecutive remembers in the same group after
-                    // the same child will have the same after and priority.
-
-                    // A selection sort is used here because it is stable and the groups are
-                    // typically very short so this quickly exit list of one and not loop for
-                    // for sizes of 2. As the information is split between three lists, to
-                    // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
-                    // an option to supply a custom swap.
-                    for (i in 0 until toAdd.size - 1) {
-                        for (j in i + 1 until toAdd.size) {
-                            val iAfter = toAddAfter[i]
-                            val jAfter = toAddAfter[j]
-                            if (
-                                iAfter < jAfter ||
-                                    (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
-                            ) {
-                                toAdd.swap(i, j)
-                                toAddPriority.swap(i, j)
-                                toAddAfter.swap(i, j)
-                            }
-                        }
-                    }
-                    leaving.addAll(toAdd)
-                }
-            }
-        }
-    }
-}
-
-private fun <T> MutableList<T>.swap(a: Int, b: Int) {
-    val item = this[a]
-    this[a] = this[b]
-    this[b] = item
-}
-
-private fun MutableIntList.swap(a: Int, b: Int) {
-    val item = this[a]
-    this[a] = this[b]
-    this[b] = item
 }
 
 internal object ScopeInvalidated
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
index 561890c..e5b6d6b 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt
@@ -16,6 +16,7 @@
 
 package androidx.compose.runtime
 
+import androidx.collection.ScatterSet
 import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf
 import androidx.compose.runtime.tooling.CompositionData
 import kotlin.coroutines.CoroutineContext
@@ -52,6 +53,20 @@
         content: @Composable () -> Unit
     )
 
+    internal abstract fun composeInitialPaused(
+        composition: ControlledComposition,
+        shouldPause: () -> Boolean,
+        content: @Composable () -> Unit
+    ): ScatterSet<RecomposeScopeImpl>
+
+    internal abstract fun recomposePaused(
+        composition: ControlledComposition,
+        shouldPause: () -> Boolean,
+        invalidScopes: ScatterSet<RecomposeScopeImpl>
+    ): ScatterSet<RecomposeScopeImpl>
+
+    internal abstract fun reportPausedScope(scope: RecomposeScopeImpl)
+
     internal abstract fun invalidate(composition: ControlledComposition)
 
     internal abstract fun invalidateScope(scope: RecomposeScopeImpl)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
new file mode 100644
index 0000000..0eff8d6
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt
@@ -0,0 +1,381 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(InternalComposeApi::class)
+
+package androidx.compose.runtime
+
+import androidx.collection.emptyScatterSet
+import androidx.collection.mutableIntListOf
+import androidx.collection.mutableObjectListOf
+import androidx.compose.runtime.internal.RememberEventDispatcher
+
+/**
+ * A [PausableComposition] is a sub-composition that can be composed incrementally as it supports
+ * being paused and resumed.
+ *
+ * Pausable sub-composition can be used between frames to prepare a sub-composition before it is
+ * required by the main composition. For example, this is used in lazy lists to prepare list items
+ * in between frames to that are likely to be scrolled in. The composition is paused when the start
+ * of the next frame is near allowing composition to be spread across multiple frames without
+ * delaying the production of the next frame.
+ *
+ * The result of the composition should not be used (e.g. the nodes should not added to a layout
+ * tree or placed in layout) until [PausedComposition.isComplete] is `true` and
+ * [PausedComposition.apply] has been called. The composition is incomplete and will not
+ * automatically recompose until after [PausedComposition.apply] is called.
+ *
+ * A [PausableComposition] is a [ReusableComposition] but [setPausableContent] should be used
+ * instead of [ReusableComposition.setContentWithReuse] to create a paused composition.
+ *
+ * If [Composition.setContent] or [ReusableComposition.setContentWithReuse] are used then the
+ * composition behaves as if it wasn't pausable. If there is a [PausedComposition] that has not yet
+ * been applied, an exception is thrown.
+ *
+ * @see Composition
+ * @see ReusableComposition
+ */
+interface PausableComposition : ReusableComposition {
+    /**
+     * Set the content of the composition. A [PausedComposition] that is currently paused. No
+     * composition is performed until [PausedComposition.resume] is called.
+     * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+     * The composition should not be used until [PausedComposition.isComplete] is `true` and
+     * [PausedComposition.apply] has been called.
+     *
+     * @see Composition.setContent
+     * @see ReusableComposition.setContentWithReuse
+     */
+    fun setPausableContent(content: @Composable () -> Unit): PausedComposition
+
+    /**
+     * Set the content of a resuable composition. A [PausedComposition] that is currently paused. No
+     * composition is performed until [PausedComposition.resume] is called.
+     * [PausedComposition.resume] should be called until [PausedComposition.isComplete] is `true`.
+     * The composition should not be used until [PausedComposition.isComplete] is `true` and
+     * [PausedComposition.apply] has been called.
+     *
+     * @see Composition.setContent
+     * @see ReusableComposition.setContentWithReuse
+     */
+    fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
+}
+
+/**
+ * [PausedComposition] is the result of calling [PausableComposition.setContent] or
+ * [PausableComposition.setContentWithReuse]. It is used to drive the paused composition to
+ * completion. A [PausedComposition] should not be used until [isComplete] is `true` and [apply] has
+ * been called.
+ *
+ * A [PausedComposition] is created paused and will only compose the `content` parameter when
+ * [resume] is called the first time.
+ */
+interface PausedComposition {
+    /**
+     * Returns `true` when the [PausedComposition] is complete. [isComplete] matches the last value
+     * returned from [resume]. Once a [PausedComposition] is [isComplete] the [apply] method should
+     * be called.
+     */
+    val isComplete: Boolean
+
+    /**
+     * Resume the composition that has been paused. This method should be called until [resume]
+     * returns `true` or [isComplete] is `true` which has the same result as the last result of
+     * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the
+     * composition should be paused. For example, in lazy lists this returns `false` until just
+     * prior to the next frame starting in which it returns `true`
+     *
+     * Calling [resume] after it returns `true` or when `isComplete` is true will throw an
+     * exception.
+     *
+     * @param shouldPause A lambda that is used to determine if the composition should be paused.
+     *   This lambda is called often so should be a very simple calculation. Returning `true` does
+     *   not guarantee the composition will pause, it should only be considered a request to pause
+     *   the composition. Not all composable functions are pausable and only pausable composition
+     *   functions will pause.
+     * @return `true` if the composition is complete and `false` if one or more calls to `resume`
+     *   are required to complete composition.
+     */
+    fun resume(shouldPause: () -> Boolean): Boolean
+
+    /**
+     * Apply the composition. This is the last step of a paused composition and is required to be
+     * called prior to the composition is usable.
+     */
+    fun apply()
+
+    /**
+     * Cancels the paused composition. This should only be used if the composition is going to be
+     * disposed and the entire composition is not going to be used.
+     */
+    fun cancel()
+}
+
+/**
+ * Create a [PausableComposition]. A [PausableComposition] can create a [PausedComposition] which
+ * allows pausing and resuming the composition.
+ *
+ * @param applier The [Applier] instance to be used in the composition.
+ * @param parent The parent [CompositionContext].
+ * @see Applier
+ * @see CompositionContext
+ * @see PausableComposition
+ */
+fun PausableComposition(applier: Applier<*>, parent: CompositionContext): PausableComposition =
+    CompositionImpl(parent, applier)
+
+internal enum class PausedCompositionState {
+    Invalid,
+    Cancelled,
+    InitialPending,
+    RecomposePending,
+    ApplyPending,
+    Applied,
+}
+
+internal class PausedCompositionImpl(
+    val composition: CompositionImpl,
+    val context: CompositionContext,
+    val composer: ComposerImpl,
+    abandonSet: MutableSet<RememberObserver>,
+    val content: @Composable () -> Unit,
+    val reusable: Boolean,
+    val applier: Applier<*>,
+    val lock: SynchronizedObject,
+) : PausedComposition {
+    private var state = PausedCompositionState.InitialPending
+    private var invalidScopes = emptyScatterSet<RecomposeScopeImpl>()
+    internal val rememberManager = RememberEventDispatcher(abandonSet)
+    internal val pausableApplier = RecordingApplier(applier.current)
+
+    override val isComplete: Boolean
+        get() = state >= PausedCompositionState.ApplyPending
+
+    override fun resume(shouldPause: () -> Boolean): Boolean {
+        try {
+            when (state) {
+                PausedCompositionState.InitialPending -> {
+                    if (reusable) composer.startReuseFromRoot()
+                    try {
+                        invalidScopes =
+                            context.composeInitialPaused(composition, shouldPause, content)
+                    } finally {
+                        if (reusable) composer.endReuseFromRoot()
+                    }
+                    state = PausedCompositionState.RecomposePending
+                    if (invalidScopes.isEmpty()) markComplete()
+                }
+                PausedCompositionState.RecomposePending -> {
+                    invalidScopes = context.recomposePaused(composition, shouldPause, invalidScopes)
+                    if (invalidScopes.isEmpty()) markComplete()
+                }
+                PausedCompositionState.ApplyPending ->
+                    error("Pausable composition is complete and apply() should be applied")
+                PausedCompositionState.Applied -> error("The paused composition has been applied")
+                PausedCompositionState.Cancelled ->
+                    error("The paused composition has been cancelled")
+                PausedCompositionState.Invalid ->
+                    error("The paused composition is invalid because of a previous exception")
+            }
+        } catch (e: Exception) {
+            state = PausedCompositionState.Invalid
+        }
+        return isComplete
+    }
+
+    override fun apply() {
+        try {
+            when (state) {
+                PausedCompositionState.InitialPending,
+                PausedCompositionState.RecomposePending ->
+                    error("The paused composition has not completed yet")
+                PausedCompositionState.ApplyPending -> {
+                    applyChanges()
+                    state = PausedCompositionState.Applied
+                }
+                PausedCompositionState.Applied ->
+                    error("The paused composition has already been applied")
+                PausedCompositionState.Cancelled ->
+                    error("The paused composition has been cancelled")
+                PausedCompositionState.Invalid ->
+                    error("The paused composition is invalid because of a previous exception")
+            }
+        } catch (e: Exception) {
+            state = PausedCompositionState.Invalid
+            throw e
+        }
+    }
+
+    override fun cancel() {
+        state = PausedCompositionState.Cancelled
+        rememberManager.dispatchAbandons()
+        composition.pausedCompositionFinished()
+    }
+
+    private fun markComplete() {
+        state = PausedCompositionState.ApplyPending
+    }
+
+    private fun applyChanges() {
+        synchronized(lock) {
+            @Suppress("UNCHECKED_CAST")
+            try {
+                pausableApplier.playTo(applier as Applier<Any?>)
+                rememberManager.dispatchRememberObservers()
+                rememberManager.dispatchSideEffects()
+            } finally {
+                rememberManager.dispatchAbandons()
+                composition.pausedCompositionFinished()
+            }
+        }
+    }
+}
+
+internal class RecordingApplier<N>(root: N) : Applier<N> {
+    private val stack = mutableObjectListOf<N>()
+    private val operations = mutableIntListOf()
+    private val instances = mutableObjectListOf<Any?>()
+
+    override var current: N = root
+
+    override fun down(node: N) {
+        operations.add(DOWN)
+        instances.add(node)
+        stack.add(current)
+        current = node
+    }
+
+    override fun up() {
+        operations.add(UP)
+        current = stack.removeAt(stack.size - 1)
+    }
+
+    override fun remove(index: Int, count: Int) {
+        operations.add(REMOVE)
+        operations.add(index)
+        operations.add(count)
+    }
+
+    override fun move(from: Int, to: Int, count: Int) {
+        operations.add(MOVE)
+        operations.add(from)
+        operations.add(to)
+        operations.add(count)
+    }
+
+    override fun clear() {
+        operations.add(CLEAR)
+    }
+
+    override fun insertBottomUp(index: Int, instance: N) {
+        operations.add(INSERT_BOTTOM_UP)
+        operations.add(index)
+        instances.add(instance)
+    }
+
+    override fun insertTopDown(index: Int, instance: N) {
+        operations.add(INSERT_TOP_DOWN)
+        operations.add(index)
+        instances.add(instance)
+    }
+
+    override fun apply(block: N.(Any?) -> Unit, value: Any?) {
+        operations.add(APPLY)
+        instances.add(block)
+        instances.add(value)
+    }
+
+    override fun reuse() {
+        operations.add(REUSE)
+    }
+
+    fun playTo(applier: Applier<N>) {
+        var currentOperation = 0
+        var currentInstance = 0
+        val operations = operations
+        val size = operations.size
+        val instances = instances
+        applier.onBeginChanges()
+        try {
+            while (currentOperation < size) {
+                val operation = operations[currentOperation++]
+                when (operation) {
+                    UP -> {
+                        applier.up()
+                    }
+                    DOWN -> {
+                        @Suppress("UNCHECKED_CAST") val node = instances[currentInstance++] as N
+                        applier.down(node)
+                    }
+                    REMOVE -> {
+                        val index = operations[currentOperation++]
+                        val count = operations[currentOperation++]
+                        applier.remove(index, count)
+                    }
+                    MOVE -> {
+                        val from = operations[currentOperation++]
+                        val to = operations[currentOperation++]
+                        val count = operations[currentOperation++]
+                        applier.move(from, to, count)
+                    }
+                    CLEAR -> {
+                        applier.clear()
+                    }
+                    INSERT_TOP_DOWN -> {
+                        val index = operations[currentOperation++]
+
+                        @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+                        applier.insertTopDown(index, instance)
+                    }
+                    INSERT_BOTTOM_UP -> {
+                        val index = operations[currentOperation++]
+
+                        @Suppress("UNCHECKED_CAST") val instance = instances[currentInstance++] as N
+                        applier.insertBottomUp(index, instance)
+                    }
+                    APPLY -> {
+                        @Suppress("UNCHECKED_CAST")
+                        val block = instances[currentInstance++] as Any?.(Any?) -> Unit
+                        val value = instances[currentInstance++]
+                        applier.apply(block, value)
+                    }
+                    REUSE -> {
+                        applier.reuse()
+                    }
+                }
+            }
+            runtimeCheck(currentInstance == instances.size) { "Applier operation size mismatch" }
+            instances.clear()
+            operations.clear()
+        } finally {
+            applier.onEndChanges()
+        }
+    }
+
+    // These commands need to be an integer, not just a enum value, as they are stored along side
+    // the commands integer parameters, so the values are explicitly set.
+    companion object {
+        const val UP = 0
+        const val DOWN = UP + 1
+        const val REMOVE = DOWN + 1
+        const val MOVE = REMOVE + 1
+        const val CLEAR = MOVE + 1
+        const val INSERT_BOTTOM_UP = CLEAR + 1
+        const val INSERT_TOP_DOWN = INSERT_BOTTOM_UP + 1
+        const val APPLY = INSERT_TOP_DOWN + 1
+        const val REUSE = APPLY + 1
+    }
+}
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
index 88245a7..48657d5 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt
@@ -55,13 +55,16 @@
         ((lowBits shl 1) and highBits))
 }
 
-private const val UsedFlag = 0x01
-private const val DefaultsInScopeFlag = 0x02
-private const val DefaultsInvalidFlag = 0x04
-private const val RequiresRecomposeFlag = 0x08
-private const val SkippedFlag = 0x10
-private const val RereadingFlag = 0x20
-private const val ForcedRecomposeFlag = 0x40
+private const val UsedFlag = 0x001
+private const val DefaultsInScopeFlag = 0x002
+private const val DefaultsInvalidFlag = 0x004
+private const val RequiresRecomposeFlag = 0x008
+private const val SkippedFlag = 0x010
+private const val RereadingFlag = 0x020
+private const val ForcedRecomposeFlag = 0x040
+private const val ForceReusing = 0x080
+private const val Paused = 0x100
+private const val Resuming = 0x200
 
 internal interface RecomposeScopeOwner {
     fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult
@@ -110,11 +113,51 @@
     var used: Boolean
         get() = flags and UsedFlag != 0
         set(value) {
-            if (value) {
-                flags = flags or UsedFlag
-            } else {
-                flags = flags and UsedFlag.inv()
-            }
+            flags =
+                if (value) {
+                    flags or UsedFlag
+                } else {
+                    flags and UsedFlag.inv()
+                }
+        }
+
+    /**
+     * Used to force a scope to the reusing state when a composition is paused while reusing
+     * content.
+     */
+    var reusing: Boolean
+        get() = flags and ForceReusing != 0
+        set(value) {
+            flags =
+                if (value) {
+                    flags or ForceReusing
+                } else {
+                    flags and ForceReusing.inv()
+                }
+        }
+
+    /** Used to flag a scope as paused for pausable compositions */
+    var paused: Boolean
+        get() = flags and Paused != 0
+        set(value) {
+            flags =
+                if (value) {
+                    flags or Paused
+                } else {
+                    flags and Paused.inv()
+                }
+        }
+
+    /** Used to flag a scope as paused for pausable compositions */
+    var resuming: Boolean
+        get() = flags and Resuming != 0
+        set(value) {
+            flags =
+                if (value) {
+                    flags or Resuming
+                } else {
+                    flags and Resuming.inv()
+                }
         }
 
     /**
@@ -299,7 +342,9 @@
     }
 
     fun scopeSkipped() {
-        skipped = true
+        if (!reusing) {
+            skipped = true
+        }
     }
 
     /**
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
index 67d0d8a..e347076 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt
@@ -17,12 +17,15 @@
 package androidx.compose.runtime
 
 import androidx.collection.MutableScatterSet
+import androidx.collection.ScatterSet
+import androidx.collection.emptyScatterSet
 import androidx.collection.mutableScatterSetOf
 import androidx.compose.runtime.collection.fastForEach
 import androidx.compose.runtime.collection.mutableVectorOf
 import androidx.compose.runtime.collection.wrapIntoSet
 import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf
 import androidx.compose.runtime.internal.AtomicReference
+import androidx.compose.runtime.internal.SnapshotThreadLocal
 import androidx.compose.runtime.internal.logError
 import androidx.compose.runtime.internal.trace
 import androidx.compose.runtime.snapshots.MutableSnapshot
@@ -232,6 +235,7 @@
     // End properties guarded by stateLock
 
     private val _state = MutableStateFlow(State.Inactive)
+    private val pausedScopes = SnapshotThreadLocal<MutableScatterSet<RecomposeScopeImpl>?>()
 
     /**
      * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its
@@ -1116,6 +1120,54 @@
         }
     }
 
+    internal override fun composeInitialPaused(
+        composition: ControlledComposition,
+        shouldPause: () -> Boolean,
+        content: @Composable () -> Unit
+    ): ScatterSet<RecomposeScopeImpl> {
+        return try {
+            composition.pausable(shouldPause) {
+                composeInitial(composition, content)
+                pausedScopes.get() ?: emptyScatterSet()
+            }
+        } finally {
+            pausedScopes.set(null)
+        }
+    }
+
+    internal override fun recomposePaused(
+        composition: ControlledComposition,
+        shouldPause: () -> Boolean,
+        invalidScopes: ScatterSet<RecomposeScopeImpl>
+    ): ScatterSet<RecomposeScopeImpl> {
+        return try {
+            recordComposerModifications()
+            composition.recordModificationsOf(invalidScopes.wrapIntoSet())
+            composition.pausable(shouldPause) {
+                val needsApply = performRecompose(composition, null)
+                if (needsApply != null) {
+                    performInitialMovableContentInserts(composition)
+                    needsApply.applyChanges()
+                    needsApply.applyLateChanges()
+                }
+                pausedScopes.get() ?: emptyScatterSet()
+            }
+        } finally {
+            pausedScopes.set(null)
+        }
+    }
+
+    override fun reportPausedScope(scope: RecomposeScopeImpl) {
+        val scopes =
+            pausedScopes.get()
+                ?: run {
+                    val newScopes = mutableScatterSetOf<RecomposeScopeImpl>()
+                    pausedScopes.set(newScopes)
+                    newScopes
+                }
+        scopes.add(scope)
+    }
+
     private fun performInitialMovableContentInserts(composition: ControlledComposition) {
         synchronized(stateLock) {
             if (!compositionValuesAwaitingInsert.fastAny { it.composition == composition }) return
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
index 4780cdb..e2eaa76 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ChangeList.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.MovableContentState
 import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
 import androidx.compose.runtime.RememberManager
 import androidx.compose.runtime.RememberObserver
 import androidx.compose.runtime.SlotTable
@@ -40,6 +41,7 @@
 import androidx.compose.runtime.changelist.Operation.EndCompositionScope
 import androidx.compose.runtime.changelist.Operation.EndCurrentGroup
 import androidx.compose.runtime.changelist.Operation.EndMovableContentPlacement
+import androidx.compose.runtime.changelist.Operation.EndResumingScope
 import androidx.compose.runtime.changelist.Operation.EnsureGroupStarted
 import androidx.compose.runtime.changelist.Operation.EnsureRootGroupStarted
 import androidx.compose.runtime.changelist.Operation.InsertSlots
@@ -48,11 +50,13 @@
 import androidx.compose.runtime.changelist.Operation.MoveNode
 import androidx.compose.runtime.changelist.Operation.ReleaseMovableGroupAtCurrent
 import androidx.compose.runtime.changelist.Operation.Remember
+import androidx.compose.runtime.changelist.Operation.RememberPausingScope
 import androidx.compose.runtime.changelist.Operation.RemoveCurrentGroup
 import androidx.compose.runtime.changelist.Operation.RemoveNode
 import androidx.compose.runtime.changelist.Operation.ResetSlots
 import androidx.compose.runtime.changelist.Operation.SideEffect
 import androidx.compose.runtime.changelist.Operation.SkipToEndOfCurrentGroup
+import androidx.compose.runtime.changelist.Operation.StartResumingScope
 import androidx.compose.runtime.changelist.Operation.TrimParentValues
 import androidx.compose.runtime.changelist.Operation.UpdateAnchoredValue
 import androidx.compose.runtime.changelist.Operation.UpdateAuxData
@@ -87,6 +91,18 @@
         operations.push(Remember) { setObject(Remember.Value, value) }
     }
 
+    fun pushRememberPausingScope(scope: RecomposeScopeImpl) {
+        operations.push(RememberPausingScope) { setObject(RememberPausingScope.Scope, scope) }
+    }
+
+    fun pushStartResumingScope(scope: RecomposeScopeImpl) {
+        operations.push(StartResumingScope) { setObject(StartResumingScope.Scope, scope) }
+    }
+
+    fun pushEndResumingScope(scope: RecomposeScopeImpl) {
+        operations.push(EndResumingScope) { setObject(EndResumingScope.Scope, scope) }
+    }
+
     fun pushUpdateValue(value: Any?, groupSlotIndex: Int) {
         operations.push(UpdateValue) {
             setObject(UpdateValue.Value, value)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
index 74c7146..9b87d45 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/ComposerChangeListWriter.kt
@@ -25,6 +25,7 @@
 import androidx.compose.runtime.InternalComposeApi
 import androidx.compose.runtime.MovableContentState
 import androidx.compose.runtime.MovableContentStateReference
+import androidx.compose.runtime.RecomposeScopeImpl
 import androidx.compose.runtime.RememberObserver
 import androidx.compose.runtime.SlotReader
 import androidx.compose.runtime.SlotTable
@@ -192,6 +193,18 @@
         changeList.pushRemember(value)
     }
 
+    fun rememberPausingScope(scope: RecomposeScopeImpl) {
+        changeList.pushRememberPausingScope(scope)
+    }
+
+    fun startResumingScope(scope: RecomposeScopeImpl) {
+        changeList.pushStartResumingScope(scope)
+    }
+
+    fun endResumingScope(scope: RecomposeScopeImpl) {
+        changeList.pushEndResumingScope(scope)
+    }
+
     fun updateValue(value: Any?, groupSlotIndex: Int) {
         pushSlotTableOperationPreamble(useParentSlot = true)
         changeList.pushUpdateValue(value, groupSlotIndex)
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
index 894f3d47..15aa234 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/changelist/Operation.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.runtime.Anchor
 import androidx.compose.runtime.Applier
-import androidx.compose.runtime.ComposeNodeLifecycleCallback
 import androidx.compose.runtime.Composition
 import androidx.compose.runtime.CompositionContext
 import androidx.compose.runtime.ControlledComposition
@@ -171,6 +170,66 @@
         }
     }
 
+    object RememberPausingScope : Operation(objects = 1) {
+        inline val Scope
+            get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+        override fun objectParamName(parameter: ObjectParameter<*>): String =
+            when (parameter) {
+                Scope -> "scope"
+                else -> super.objectParamName(parameter)
+            }
+
+        override fun OperationArgContainer.execute(
+            applier: Applier<*>,
+            slots: SlotWriter,
+            rememberManager: RememberManager
+        ) {
+            val scope = getObject(Scope)
+            rememberManager.rememberPausingScope(scope)
+        }
+    }
+
+    object StartResumingScope : Operation(objects = 1) {
+        inline val Scope
+            get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+        override fun objectParamName(parameter: ObjectParameter<*>): String =
+            when (parameter) {
+                Scope -> "scope"
+                else -> super.objectParamName(parameter)
+            }
+
+        override fun OperationArgContainer.execute(
+            applier: Applier<*>,
+            slots: SlotWriter,
+            rememberManager: RememberManager
+        ) {
+            val scope = getObject(Scope)
+            rememberManager.startResumingScope(scope)
+        }
+    }
+
+    object EndResumingScope : Operation(objects = 1) {
+        inline val Scope
+            get() = ObjectParameter<RecomposeScopeImpl>(0)
+
+        override fun objectParamName(parameter: ObjectParameter<*>): String =
+            when (parameter) {
+                Scope -> "scope"
+                else -> super.objectParamName(parameter)
+            }
+
+        override fun OperationArgContainer.execute(
+            applier: Applier<*>,
+            slots: SlotWriter,
+            rememberManager: RememberManager
+        ) {
+            val scope = getObject(Scope)
+            rememberManager.endResumingScope(scope)
+        }
+    }
+
     object AppendValue : Operation(objects = 2) {
         inline val Anchor
             get() = ObjectParameter<Anchor>(0)
@@ -467,7 +526,7 @@
             slots: SlotWriter,
             rememberManager: RememberManager
         ) {
-            (applier.current as ComposeNodeLifecycleCallback).onReuse()
+            applier.reuse()
         }
     }
 
@@ -492,7 +551,7 @@
         ) {
             val value = getObject(Value)
             val block = getObject(Block)
-            applier.current.block(value)
+            applier.apply(block, value)
         }
     }
 
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
new file mode 100644
index 0000000..d9d78ea
--- /dev/null
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/internal/RememberEventDispatcher.kt
@@ -0,0 +1,301 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime.internal
+
+import androidx.collection.MutableIntList
+import androidx.collection.MutableScatterMap
+import androidx.collection.MutableScatterSet
+import androidx.collection.mutableScatterMapOf
+import androidx.collection.mutableScatterSetOf
+import androidx.compose.runtime.ComposeNodeLifecycleCallback
+import androidx.compose.runtime.RecomposeScopeImpl
+import androidx.compose.runtime.RememberManager
+import androidx.compose.runtime.RememberObserver
+import androidx.compose.runtime.Stack
+import androidx.compose.runtime.snapshots.fastForEach
+
+/**
+ * Used as a placeholder for paused compositions to ensure the remembers are dispatch in the correct
+ * order. While the paused composition is resuming all remembered objects are placed into the this
+ * classes list instead of the main list. As remembers are dispatched, this will dispatch remembers
+ * to the object remembered in the paused composition's content in the order that they would have
+ * been dispatched had the composition not been paused.
+ */
+internal class PausedCompositionRemembers(private val abandoning: MutableSet<RememberObserver>) :
+    RememberObserver {
+    val pausedRemembers = mutableListOf<RememberObserver>()
+
+    override fun onRemembered() {
+        pausedRemembers.fastForEach {
+            abandoning.remove(it)
+            it.onRemembered()
+        }
+    }
+
+    // These are never called
+    override fun onForgotten() {}
+
+    override fun onAbandoned() {}
+}
+
+/** Helper for collecting remember observers for later strictly ordered dispatch. */
+internal class RememberEventDispatcher(private val abandoning: MutableSet<RememberObserver>) :
+    RememberManager {
+    private val remembering = mutableListOf<RememberObserver>()
+    private var currentRememberingList = remembering
+    private val leaving = mutableListOf<Any>()
+    private val sideEffects = mutableListOf<() -> Unit>()
+    private var releasing: MutableScatterSet<ComposeNodeLifecycleCallback>? = null
+    private var pausedPlaceholders:
+        MutableScatterMap<RecomposeScopeImpl, PausedCompositionRemembers>? =
+        null
+    private val pending = mutableListOf<Any>()
+    private val priorities = MutableIntList()
+    private val afters = MutableIntList()
+    private var nestedRemembersLists: Stack<MutableList<RememberObserver>>? = null
+
+    override fun remembering(instance: RememberObserver) {
+        currentRememberingList.add(instance)
+    }
+
+    override fun forgetting(
+        instance: RememberObserver,
+        endRelativeOrder: Int,
+        priority: Int,
+        endRelativeAfter: Int
+    ) {
+        recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+    }
+
+    override fun sideEffect(effect: () -> Unit) {
+        sideEffects += effect
+    }
+
+    override fun deactivating(
+        instance: ComposeNodeLifecycleCallback,
+        endRelativeOrder: Int,
+        priority: Int,
+        endRelativeAfter: Int
+    ) {
+        recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+    }
+
+    override fun releasing(
+        instance: ComposeNodeLifecycleCallback,
+        endRelativeOrder: Int,
+        priority: Int,
+        endRelativeAfter: Int
+    ) {
+        val releasing =
+            releasing ?: mutableScatterSetOf<ComposeNodeLifecycleCallback>().also { releasing = it }
+
+        releasing += instance
+        recordLeaving(instance, endRelativeOrder, priority, endRelativeAfter)
+    }
+
+    override fun rememberPausingScope(scope: RecomposeScopeImpl) {
+        val pausedPlaceholder = PausedCompositionRemembers(abandoning)
+        (pausedPlaceholders
+            ?: mutableScatterMapOf<RecomposeScopeImpl, PausedCompositionRemembers>().also {
+                pausedPlaceholders = it
+            })[scope] = pausedPlaceholder
+        this.currentRememberingList.add(pausedPlaceholder)
+    }
+
+    override fun startResumingScope(scope: RecomposeScopeImpl) {
+        val placeholder = pausedPlaceholders?.get(scope)
+        if (placeholder != null) {
+            (nestedRemembersLists
+                    ?: Stack<MutableList<RememberObserver>>().also { nestedRemembersLists = it })
+                .push(currentRememberingList)
+            currentRememberingList = placeholder.pausedRemembers
+        }
+    }
+
+    override fun endResumingScope(scope: RecomposeScopeImpl) {
+        val pausedPlaceholders = pausedPlaceholders
+        if (pausedPlaceholders != null) {
+            val placeholder = pausedPlaceholders[scope]
+            if (placeholder != null) {
+                nestedRemembersLists?.pop()?.let { currentRememberingList = it }
+                pausedPlaceholders.remove(scope)
+            }
+        }
+    }
+
+    fun dispatchRememberObservers() {
+        // Add any pending out-of-order forgotten objects
+        processPendingLeaving(Int.MIN_VALUE)
+
+        // Send forgets and node callbacks
+        if (leaving.isNotEmpty()) {
+            trace("Compose:onForgotten") {
+                val releasing = releasing
+                for (i in leaving.size - 1 downTo 0) {
+                    val instance = leaving[i]
+                    if (instance is RememberObserver) {
+                        abandoning.remove(instance)
+                        instance.onForgotten()
+                    }
+                    if (instance is ComposeNodeLifecycleCallback) {
+                        // node callbacks are in the same queue as forgets to ensure ordering
+                        if (releasing != null && instance in releasing) {
+                            instance.onRelease()
+                        } else {
+                            instance.onDeactivate()
+                        }
+                    }
+                }
+            }
+        }
+
+        // Send remembers
+        if (remembering.isNotEmpty()) {
+            trace("Compose:onRemembered") { dispatchRememberList(remembering) }
+        }
+    }
+
+    private fun dispatchRememberList(list: List<RememberObserver>) {
+        list.fastForEach { instance ->
+            abandoning.remove(instance)
+            instance.onRemembered()
+        }
+    }
+
+    fun dispatchSideEffects() {
+        if (sideEffects.isNotEmpty()) {
+            trace("Compose:sideeffects") {
+                sideEffects.fastForEach { sideEffect -> sideEffect() }
+                sideEffects.clear()
+            }
+        }
+    }
+
+    fun dispatchAbandons() {
+        if (abandoning.isNotEmpty()) {
+            trace("Compose:abandons") {
+                val iterator = abandoning.iterator()
+                // remove elements one by one to ensure that abandons will not be dispatched
+                // second time in case [onAbandoned] throws.
+                while (iterator.hasNext()) {
+                    val instance = iterator.next()
+                    iterator.remove()
+                    instance.onAbandoned()
+                }
+            }
+        }
+    }
+
+    private fun recordLeaving(
+        instance: Any,
+        endRelativeOrder: Int,
+        priority: Int,
+        endRelativeAfter: Int
+    ) {
+        processPendingLeaving(endRelativeOrder)
+        if (endRelativeAfter in 0 until endRelativeOrder) {
+            pending.add(instance)
+            priorities.add(priority)
+            afters.add(endRelativeAfter)
+        } else {
+            leaving.add(instance)
+        }
+    }
+
+    private fun processPendingLeaving(endRelativeOrder: Int) {
+        if (pending.isNotEmpty()) {
+            var index = 0
+            var toAdd: MutableList<Any>? = null
+            var toAddAfter: MutableIntList? = null
+            var toAddPriority: MutableIntList? = null
+            while (index < afters.size) {
+                if (endRelativeOrder <= afters[index]) {
+                    val instance = pending.removeAt(index)
+                    val endRelativeAfter = afters.removeAt(index)
+                    val priority = priorities.removeAt(index)
+
+                    if (toAdd == null) {
+                        toAdd = mutableListOf(instance)
+                        toAddAfter = MutableIntList().also { it.add(endRelativeAfter) }
+                        toAddPriority = MutableIntList().also { it.add(priority) }
+                    } else {
+                        toAddPriority as MutableIntList
+                        toAddAfter as MutableIntList
+                        toAdd.add(instance)
+                        toAddAfter.add(endRelativeAfter)
+                        toAddPriority.add(priority)
+                    }
+                } else {
+                    index++
+                }
+            }
+            if (toAdd != null) {
+                toAddPriority as MutableIntList
+                toAddAfter as MutableIntList
+
+                // Sort the list into [after, -priority] order where it is ordered by after
+                // in ascending order as the primary key and priority in descending order as
+                // secondary key.
+
+                // For example if remember occurs after a child group it must be added after
+                // all the remembers of the child. This is reported with an after which is the
+                // slot index of the child's last slot. As this slot might be at the same
+                // location as where its parents ends this would be ambiguous which should
+                // first if both the two groups request a slot to be after the same slot.
+                // Priority is used to break the tie here which is the group index of the group
+                // which is leaving. Groups that are lower must be added before the parent's
+                // remember when they have the same after.
+
+                // The sort must be stable as as consecutive remembers in the same group after
+                // the same child will have the same after and priority.
+
+                // A selection sort is used here because it is stable and the groups are
+                // typically very short so this quickly exit list of one and not loop for
+                // for sizes of 2. As the information is split between three lists, to
+                // reduce allocations, [MutableList.sort] cannot be used as it doesn't have
+                // an option to supply a custom swap.
+                for (i in 0 until toAdd.size - 1) {
+                    for (j in i + 1 until toAdd.size) {
+                        val iAfter = toAddAfter[i]
+                        val jAfter = toAddAfter[j]
+                        if (
+                            iAfter < jAfter ||
+                                (jAfter == iAfter && toAddPriority[i] < toAddPriority[j])
+                        ) {
+                            toAdd.swap(i, j)
+                            toAddPriority.swap(i, j)
+                            toAddAfter.swap(i, j)
+                        }
+                    }
+                }
+                leaving.addAll(toAdd)
+            }
+        }
+    }
+}
+
+private fun <T> MutableList<T>.swap(a: Int, b: Int) {
+    val item = this[a]
+    this[a] = this[b]
+    this[b] = item
+}
+
+private fun MutableIntList.swap(a: Int, b: Int) {
+    val item = this[a]
+    this[a] = this[b]
+    this[b] = item
+}
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
index c2560dd..9328a61 100644
--- a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/CompositionTests.kt
@@ -4800,16 +4800,16 @@
 private val rob_reports_to_alice = Report("Rob", "Alice")
 private val clark_reports_to_lois = Report("Clark", "Lois")
 
-private interface Counted {
+internal interface Counted {
     val count: Int
 }
 
-private interface Ordered {
+internal interface Ordered {
     val rememberOrder: Int
     val forgetOrder: Int
 }
 
-private interface Named {
+internal interface Named {
     val name: String
 }
 
diff --git a/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
new file mode 100644
index 0000000..36a9747
--- /dev/null
+++ b/compose/runtime/runtime/src/nonEmulatorCommonTest/kotlin/androidx/compose/runtime/PausableCompositionTests.kt
@@ -0,0 +1,606 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.compose.runtime
+
+import androidx.compose.runtime.mock.EmptyApplier
+import androidx.compose.runtime.mock.Linear
+import androidx.compose.runtime.mock.MockViewValidator
+import androidx.compose.runtime.mock.Text
+import androidx.compose.runtime.mock.View
+import androidx.compose.runtime.mock.ViewApplier
+import androidx.compose.runtime.mock.compositionTest
+import androidx.compose.runtime.mock.validate
+import androidx.compose.runtime.mock.view
+import kotlin.coroutines.resume
+import kotlin.test.Ignore
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.test.runTest
+
+@Stable
+class PausableCompositionTests {
+    @Test
+    fun canCreateARootPausableComposition() = runTest {
+        val recomposer = Recomposer(coroutineContext)
+        val pausableComposition = PausableComposition(EmptyApplier(), recomposer)
+        pausableComposition.dispose()
+        recomposer.cancel()
+        recomposer.close()
+    }
+
+    @Test
+    fun canCreateANestedPausableComposition() = compositionTest {
+        compose {
+            val parent = rememberCompositionContext()
+            DisposableEffect(Unit) {
+                val pausableComposition = PausableComposition(EmptyApplier(), parent)
+                onDispose { pausableComposition.dispose() }
+            }
+        }
+    }
+
+    @Test
+    fun canRecordAComposition() = compositionTest {
+        // This just tests the recording mechanism used in the tests below.
+        val recording = recordTest {
+            compose { A() }
+
+            validate { this.A() }
+        }
+
+        // Legend for the recording:
+        //  +N: Enter N for functions A, B, C, D, (where A:1 is the first lambda in A())
+        //  -N: Exit N
+        //  *N: Calling N (e.g *B is recorded before B() is called).
+        //  ^n: calling remember for some value
+
+        // Here we expect the normal, synchronous, execution as the recorded composition is not
+        // pausable. That is if we see a *B that should immediately followed by a B+ its content and
+        // a B-.
+        assertEquals(
+            recording,
+            "+A, ^z, ^Y, *B, +B, *Linear, +A:1, *C, +C, ^x, *Text, -C, *D, +D, +D:1, *C, +C, " +
+                "^x, *Text, -C, *C, +C, ^x, *Text, -C, *C, +C, ^x, *Text, -C, -D:1, -D, -A:1, " +
+                "-B, -A"
+        )
+    }
+
+    @Test
+    @Ignore // Requires compiler support
+    fun canPauseContent() = compositionTest {
+        val awaiter = Awaiter()
+        var receivedIteration = 0
+        val recording = recordTest {
+            compose {
+                PausableContent(
+                    normalWorkflow {
+                        receivedIteration = iteration
+                        awaiter.done()
+                    }
+                ) {
+                    A()
+                }
+            }
+            awaiter.await()
+        }
+        validate { this.PausableContent { this.A() } }
+        assertEquals(10, receivedIteration)
+
+        // Same Legend as canRecordAComposition
+        // Here we expect all functions to exit before the content of the function is executed
+        // because the above will pause at every pause point. If we see a B* we should not receive
+        // a B+ until after the caller finishes. (e.g. A-).
+        assertEquals(
+            recording,
+            "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+                "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+                "-C, +C, ^x, *Text, -C"
+        )
+    }
+
+    @Test
+    @Ignore // Requires compiler support
+    fun canPauseReusableContent() = compositionTest {
+        val awaiter = Awaiter()
+        var receivedIteration = 0
+        val recording = recordTest {
+            compose {
+                PausableContent(
+                    reuseWorkflow {
+                        receivedIteration = iteration
+                        awaiter.done()
+                    }
+                ) {
+                    A()
+                }
+            }
+            awaiter.await()
+        }
+        validate { this.PausableContent { this.A() } }
+        assertEquals(10, receivedIteration)
+        // Same Legend as canRecordAComposition
+        // Here we expect the result to be the same as if we were inserting new content as in
+        // canPauseContent
+        assertEquals(
+            "+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+                "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+                "-C, +C, ^x, *Text, -C",
+            recording
+        )
+    }
+
+    @Test
+    @Ignore // Requires compiler support
+    fun canPauseReusingContent() = compositionTest {
+        val awaiter = Awaiter()
+        var recording = ""
+        val workflow: Workflow = {
+            // Create the content
+            setContentWithReuse()
+            resumeTillComplete { false }
+            apply()
+
+            // Reuse the content
+            recording = recordTest {
+                setContentWithReuse()
+                resumeTillComplete { true }
+                apply()
+            }
+            awaiter.done()
+        }
+
+        compose { PausableContent(workflow) { A() } }
+        awaiter.await()
+        // Same Legend as canRecordAComposition
+        // Here we expect the result to be the same as if we were inserting new content as in
+        // canPauseContent
+        assertArrayEquals(
+            ("+A, ^z, ^Y, *B, -A, +B, *Linear, -B, +A:1, *C, *D, -A:1, +C, " +
+                    "^x, *Text, -C, +D, -D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+                    "-C, +C, ^x, *Text, -C")
+                .splitRecording(),
+            recording.splitRecording()
+        )
+    }
+
+    @Test
+    fun applierOnlyCalledInApply() = compositionTest {
+        val awaiter = Awaiter()
+        var applier: ViewApplier? = null
+
+        val workflow = workflow {
+            setContent()
+
+            assertFalse(applier?.called == true, "Applier was called during set content")
+
+            resumeTillComplete { false }
+
+            assertFalse(applier?.called == true, "Applier was called during resume")
+
+            apply()
+
+            assertTrue(applier?.called == true, "Applier wasn't called")
+
+            awaiter.done()
+        }
+
+        compose {
+            PausableContent(workflow, { view -> ViewApplier(view).also { applier = it } }) { A() }
+        }
+        awaiter.await()
+    }
+
+    @Test
+    @Ignore // Requires compiler support
+    fun rememberOnlyCalledInApply() = compositionTest {
+        val awaiter = Awaiter()
+        var onRememberCalled = false
+
+        val workflow = workflow {
+            setContent()
+            assertFalse(onRememberCalled, "onRemember called during set content")
+
+            resumeTillComplete {
+                assertFalse(onRememberCalled, "onRemember called during resume")
+                true
+            }
+            assertFalse(onRememberCalled, "onRemember called before resume returned")
+
+            apply()
+
+            assertTrue(onRememberCalled, "onRemember was not called in apply")
+
+            awaiter.done()
+        }
+
+        fun rememberedObject(name: String) =
+            object : RememberObserver {
+                val name = name
+
+                override fun onRemembered() {
+                    onRememberCalled = true
+                    report("+$name")
+                }
+
+                override fun onForgotten() {
+                    report("-$name")
+                }
+
+                override fun onAbandoned() {
+                    report("!$name")
+                }
+            }
+
+        val recording = recordTest {
+            compose {
+                PausableContent(workflow) {
+                    val a = remember { rememberedObject("a") }
+                    report("C(${a.name})")
+                    B {
+                        val b = remember { rememberedObject("b") }
+                        report("C(${b.name})")
+                        B {
+                            val c = remember { rememberedObject("c") }
+                            report("C(${c.name})")
+                            C()
+                            val d = remember { rememberedObject("d") }
+                            report("C(${d.name})")
+                            D()
+                        }
+                    }
+                }
+            }
+
+            awaiter.await()
+        }
+        // Same Legend as canRecordAComposition except the addition of the C(N) added above and
+        // +a, +b, etc. which records when the remembered object are sent the on-remember. This
+        // ensures that all onRemember calls are made after the composition has completed.
+        assertEquals(
+            "C(a), +B, *Linear, -B, C(b), +B, *Linear, -B, C(c), C(d), +C, ^x, *Text, -C, +D, " +
+                "-D, +D:1, *C, *C, *C, -D:1, +C, ^x, *Text, -C, +C, ^x, *Text, -C, +C, ^x, *Text, " +
+                "-C, +a, +b, +c, +d",
+            recording
+        )
+    }
+
+    @Suppress("ListIterator")
+    @Test
+    fun pausable_testRemember_RememberForgetOrder() = compositionTest {
+        var order = 0
+        val objects = mutableListOf<Any>()
+        val newRememberObject = { name: String ->
+            object : RememberObserver, Counted, Ordered, Named {
+                    override var name = name
+                    override var count = 0
+                    override var rememberOrder = -1
+                    override var forgetOrder = -1
+
+                    override fun onRemembered() {
+                        assertEquals(-1, rememberOrder, "Only one call to onRemembered expected")
+                        rememberOrder = order++
+                        count++
+                    }
+
+                    override fun onForgotten() {
+                        assertEquals(-1, forgetOrder, "Only one call to onForgotten expected")
+                        forgetOrder = order++
+                        count--
+                    }
+
+                    override fun onAbandoned() {
+                        assertEquals(0, count, "onAbandoned called after onRemembered")
+                    }
+                }
+                .also { objects.add(it) }
+        }
+
+        @Suppress("UNUSED_PARAMETER") fun used(v: Any) {}
+
+        @Composable
+        fun Tree() {
+            used(remember { newRememberObject("L0B") })
+            Linear {
+                used(remember { newRememberObject("L1B") })
+                Linear {
+                    used(remember { newRememberObject("L2B") })
+                    Linear {
+                        used(remember { newRememberObject("L3B") })
+                        Linear { used(remember { newRememberObject("Leaf") }) }
+                        used(remember { newRememberObject("L3A") })
+                    }
+                    used(remember { newRememberObject("L2A") })
+                }
+                used(remember { newRememberObject("L1A") })
+            }
+            used(remember { newRememberObject("L0A") })
+        }
+
+        val awaiter = Awaiter()
+        val workFlow = normalWorkflow { awaiter.done() }
+
+        compose { PausableContent(workFlow) { Tree() } }
+        awaiter.await()
+
+        // Legend:
+        //   L<N><B|A>: where N is the nesting level and B is before the children and
+        //     A is after the children.
+        //   Leaf: the object remembered in the middle.
+        // This is asserting that the remember order is the same as it would have been had the
+        //   above composition was not paused.
+        assertEquals(
+            "L0B, L1B, L2B, L3B, Leaf, L3A, L2A, L1A, L0A",
+            objects
+                .mapNotNull { it as? Ordered }
+                .sortedBy { it.rememberOrder }
+                .joinToString { (it as Named).name },
+            "Expected enter order",
+        )
+    }
+}
+
+fun String.splitRecording() = split(", ")
+
+typealias Workflow = suspend PausableContentWorkflowScope.() -> Unit
+
+fun workflow(workflow: Workflow): Workflow = workflow
+
+fun reuseWorkflow(done: Workflow = {}) = workflow {
+    setContentWithReuse()
+    resumeTillComplete { true }
+    apply()
+    done()
+}
+
+fun normalWorkflow(done: Workflow = {}) = workflow {
+    setContent()
+    resumeTillComplete { true }
+    apply()
+    done()
+}
+
+private interface TestRecorder {
+    fun log(message: String)
+
+    fun logs(): String
+
+    fun clear()
+}
+
+private var recorder: TestRecorder =
+    object : TestRecorder {
+        override fun log(message: String) {}
+
+        override fun logs(): String = ""
+
+        override fun clear() {}
+    }
+
+private inline fun recordTest(block: () -> Unit): String {
+    val result = mutableListOf<String>()
+    val oldRecorder = recorder
+    recorder =
+        object : TestRecorder {
+            override fun log(message: String) {
+                result.add(message)
+            }
+
+            override fun logs() = result.joinToString()
+
+            override fun clear() {
+                result.clear()
+            }
+        }
+    block()
+    recorder = oldRecorder
+    return result.joinToString()
+}
+
+private fun report(message: String) {
+    synchronized(recorder) { recorder.log(message) }
+}
+
+private inline fun report(message: String, block: () -> Unit) {
+    report("+$message")
+    block()
+    report("-$message")
+}
+
+@Composable
+private fun A() {
+    report("A") {
+        report("^z")
+        val z = remember { 0 }
+        report("^Y")
+        val y = remember { 1 }
+        Text("A: $z $y")
+        report("*B")
+        B {
+            report("A:1") {
+                report("*C")
+                C()
+                report("*D")
+                D()
+            }
+        }
+    }
+}
+
+private fun MockViewValidator.PausableContent(content: MockViewValidator.() -> Unit) {
+    this.view("PausableContentHost") { this.view("PausableContent", content) }
+}
+
+private fun MockViewValidator.A() {
+    Text("A: 0 1")
+    this.B {
+        this.C()
+        this.D()
+    }
+}
+
+@Composable
+private fun B(content: @Composable () -> Unit) {
+    report("B") {
+        report("*Linear")
+        Linear(content)
+    }
+}
+
+private fun MockViewValidator.B(content: MockViewValidator.() -> Unit) {
+    this.Linear(content)
+}
+
+@Composable
+private fun C() {
+    report("C") {
+        report("^x")
+        val x = remember { 3 }
+        report("*Text")
+        Text("C: $x")
+    }
+}
+
+private fun MockViewValidator.C() {
+    this.Text("C: 3")
+}
+
+@Composable
+private fun D() {
+    report("D") {
+        Linear {
+            report("D:1") {
+                repeat(3) {
+                    report("*C")
+                    C()
+                }
+            }
+        }
+    }
+}
+
+private fun MockViewValidator.D() {
+    this.Linear { repeat(3) { this.C() } }
+}
+
+interface PausableContentWorkflowScope {
+    val iteration: Int
+    val applied: Boolean
+
+    fun setContent(): PausedComposition
+
+    fun setContentWithReuse(): PausedComposition
+
+    fun resumeTillComplete(shouldPause: () -> Boolean)
+
+    fun apply()
+}
+
+fun PausableContentWorkflowScope.run(shouldPause: () -> Boolean = { true }) {
+    setContent()
+    resumeTillComplete(shouldPause)
+    apply()
+}
+
+class PausableContentWorkflowDriver(
+    private val composition: PausableComposition,
+    private val content: @Composable () -> Unit,
+    private var host: View?,
+    private var contentView: View?
+) : PausableContentWorkflowScope {
+    private var pausedComposition: PausedComposition? = null
+    override var iteration = 0
+    override val applied: Boolean
+        get() = host == null && pausedComposition == null
+
+    override fun setContent(): PausedComposition {
+        checkPrecondition(pausedComposition == null)
+        return composition.setPausableContent(content).also { pausedComposition = it }
+    }
+
+    override fun setContentWithReuse(): PausedComposition {
+        checkPrecondition(pausedComposition == null)
+        return composition.setPausableContentWithReuse(content).also { pausedComposition = it }
+    }
+
+    override fun resumeTillComplete(shouldPause: () -> Boolean) {
+        val pausedComposition = pausedComposition
+        checkPrecondition(pausedComposition != null)
+        while (!pausedComposition.isComplete) {
+            pausedComposition.resume(shouldPause)
+            iteration++
+        }
+    }
+
+    override fun apply() {
+        val pausedComposition = pausedComposition
+        checkPrecondition(pausedComposition != null && pausedComposition.isComplete)
+        pausedComposition.apply()
+        this.pausedComposition = null
+        val host = host
+        val contentView = contentView
+        if (host != null && contentView != null) {
+            host.children.add(contentView)
+            this.host = null
+            this.contentView = null
+        }
+    }
+}
+
+@Composable
+private fun PausableContent(
+    workflow: suspend PausableContentWorkflowScope.() -> Unit = { run() },
+    createApplier: (view: View) -> Applier<View> = { ViewApplier(it) },
+    content: @Composable () -> Unit
+) {
+    val host = View().also { it.name = "PausableContentHost" }
+    val pausableContent = View().also { it.name = "PausableContent" }
+    ComposeNode<View, ViewApplier>(factory = { host }, update = {})
+    val parent = rememberCompositionContext()
+    val composition =
+        remember(parent) { PausableComposition(createApplier(pausableContent), parent) }
+    LaunchedEffect(content as Any) {
+        val scope = PausableContentWorkflowDriver(composition, content, host, pausableContent)
+        scope.workflow()
+    }
+    DisposableEffect(Unit) { onDispose { composition.dispose() } }
+}
+
+private class Awaiter {
+    private var continuation: CancellableContinuation<Unit>? = null
+    private var done = false
+
+    suspend fun await() {
+        if (!done) {
+            suspendCancellableCoroutine { continuation = it }
+        }
+    }
+
+    fun resume() {
+        val current = continuation
+        continuation = null
+        current?.resume(Unit)
+    }
+
+    fun done() {
+        done = true
+        resume()
+    }
+}
diff --git a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
index e1b9966..30b464c 100644
--- a/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
+++ b/compose/ui/ui-test/src/androidInstrumentedTest/kotlin/androidx/compose/ui/test/AccessibilityChecksTest.kt
@@ -111,7 +111,7 @@
     @Composable
     private fun BoxWithMissingContentDescription() {
         Box(
-            Modifier.size(20.dp).semantics {
+            Modifier.size(48.dp).semantics {
                 // The SemanticsModifier will make this node importantForAccessibility
                 // Having no content description is now a violation
                 this.contentDescription = ""
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -13,5 +13,9 @@
     Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
 
 
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+    Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
 RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
     Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index cc929b6..afed775 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -2338,20 +2338,20 @@
   }
 
   public interface MeasureResult {
-    method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+    method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
     method public int getHeight();
     method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
     method public int getWidth();
     method public void placeChildren();
-    property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+    property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
     property public abstract int height;
     property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
     property public abstract int width;
   }
 
   @androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
   }
 
   @kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 074fe37..8caaf36 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -13,5 +13,9 @@
     Removed method androidx.compose.ui.semantics.SemanticsProperties.getInvisibleToUser() from compatibility checked API surface
 
 
+ChangedType: androidx.compose.ui.layout.MeasureResult#getAlignmentLines():
+    Method androidx.compose.ui.layout.MeasureResult.getAlignmentLines has changed return type from java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> to java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer>
+
+
 RemovedMethod: androidx.compose.ui.layout.LayoutCoordinates#transformToScreen(float[]):
     Removed method androidx.compose.ui.layout.LayoutCoordinates.transformToScreen(float[])
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 2c9b7cf..29ad080 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -2341,20 +2341,20 @@
   }
 
   public interface MeasureResult {
-    method public java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
+    method public java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> getAlignmentLines();
     method public int getHeight();
     method public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? getRulers();
     method public int getWidth();
     method public void placeChildren();
-    property public abstract java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
+    property public abstract java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines;
     property public abstract int height;
     property public default kotlin.jvm.functions.Function1<androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers;
     property public abstract int width;
   }
 
   @androidx.compose.ui.layout.MeasureScopeMarker @kotlin.jvm.JvmDefaultWithCompatibility public interface MeasureScope extends androidx.compose.ui.layout.IntrinsicMeasureScope {
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
-    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
+    method public default androidx.compose.ui.layout.MeasureResult layout(int width, int height, optional java.util.Map<? extends androidx.compose.ui.layout.AlignmentLine,java.lang.Integer> alignmentLines, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.RulerScope,kotlin.Unit>? rulers, kotlin.jvm.functions.Function1<? super androidx.compose.ui.layout.Placeable.PlacementScope,kotlin.Unit> placementBlock);
   }
 
   @kotlin.DslMarker public @interface MeasureScopeMarker {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
index 2273b6a..3ea0bed 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitChildrenTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -104,6 +105,52 @@
         assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
     }
 
+    @Test
+    fun visitChildrenInOtherLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1).zIndex(10f))
+                Box(Modifier.elementOf(child2).zIndex(-10f))
+                Box(Modifier.elementOf(child3))
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitChildren(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+    }
+
+    @Test
+    fun visitChildrenInOtherLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) },
+                { Box(Modifier.elementOf(child3)) }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { node.visitChildren(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattachedLocalChild() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
index 63f82d9..6f1e610 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeIfTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -134,6 +135,58 @@
             .inOrder()
     }
 
+    @Test
+    fun visitsItemsAcrossLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1).zIndex(10f))
+                Box(Modifier.elementOf(child2).zIndex(-10f))
+                Box(Modifier.elementOf(child3))
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+                true
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child2, child3, child1).inOrder()
+    }
+
+    @Test
+    fun visitsItemsAcrossLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) },
+                { Box(Modifier.elementOf(child3)) }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtreeIf(Nodes.Any, zOrder = true) {
+                visitedChildren.add(it)
+                true
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattachedItems() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
index 59d8aed..c2b585b5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/ModifierNodeVisitSubtreeTest.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.layout.Box
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.zIndex
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.google.common.truth.Truth.assertThat
@@ -67,9 +68,6 @@
         assertThat(visitedChildren).containsExactly(localChild1, localChild2).inOrder()
     }
 
-    // TODO(ralu): I feel that this order of visiting children is incorrect, and we should
-    //  visit children in the order of composition. So instead of a stack, we probably need
-    //  to use a queue to hold the intermediate nodes.
     @Test
     fun differentLayoutNodes() {
         // Arrange.
@@ -79,10 +77,10 @@
         val visitedChildren = mutableListOf<Modifier.Node>()
         rule.setContent {
             Box(Modifier.elementOf(node).elementOf(child1).elementOf(child2)) {
-                Box(Modifier.elementOf(child5).elementOf(child6)) {
-                    Box(Modifier.elementOf(child7).elementOf(child8))
+                Box(Modifier.elementOf(child3).elementOf(child4)) {
+                    Box(Modifier.elementOf(child5).elementOf(child6))
                 }
-                Box { Box(Modifier.elementOf(child3).elementOf(child4)) }
+                Box { Box(Modifier.elementOf(child7).elementOf(child8)) }
             }
         }
 
@@ -95,6 +93,54 @@
             .inOrder()
     }
 
+    @Test
+    fun differentLayoutNodesInDrawOrder_zIndex() {
+        // Arrange.
+        abstract class TrackedNode : Modifier.Node()
+        val (node, child1, child2, child3, child4) = List(5) { object : TrackedNode() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            Box(Modifier.elementOf(node)) {
+                Box(Modifier.elementOf(child1))
+                Box(Modifier.elementOf(child2).zIndex(10f)) {
+                    Box(Modifier.elementOf(child3).zIndex(-10f))
+                }
+                Box { Box(Modifier.elementOf(child4)) }
+            }
+        }
+
+        // Act.
+        rule.runOnIdle {
+            node.visitSubtree(Nodes.Any, zOrder = true) {
+                @Suppress("KotlinConstantConditions") if (it is TrackedNode) visitedChildren.add(it)
+            }
+        }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child4, child2, child3).inOrder()
+    }
+
+    @Test
+    fun differentLayoutNodesInDrawOrder_subcompose() {
+        // Arrange.
+        val (node, child1, child2, child3, child4) = List(5) { object : Modifier.Node() {} }
+        val visitedChildren = mutableListOf<Modifier.Node>()
+        rule.setContent {
+            ReverseMeasureLayout(
+                Modifier.elementOf(node),
+                { Box(Modifier.elementOf(child1)) },
+                { Box(Modifier.elementOf(child2)) { Box(Modifier.elementOf(child3)) } },
+                { Box { Box(Modifier.elementOf(child4)) } }
+            )
+        }
+
+        // Act.
+        rule.runOnIdle { node.visitSubtree(Nodes.Any, zOrder = true) { visitedChildren.add(it) } }
+
+        // Assert.
+        assertThat(visitedChildren).containsExactly(child1, child2, child3, child4).inOrder()
+    }
+
     @Ignore("b/278765590")
     @Test
     fun skipsUnattached() {
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
deleted file mode 100644
index 74a3c0e..0000000
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NestedVectorStackTests.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import org.junit.Assert
-import org.junit.Test
-
-class NestedVectorStackTests {
-
-    @Test
-    fun testPushPopOrder() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-        stack.push(mutableVectorOf(4, 5, 6))
-        stack.push(mutableVectorOf())
-        stack.push(mutableVectorOf(7))
-        stack.push(mutableVectorOf(8, 9))
-        val result = buildString {
-            while (stack.isNotEmpty()) {
-                append(stack.pop())
-            }
-        }
-        Assert.assertEquals("987654321", result)
-    }
-
-    @Test
-    fun testPopInBetweenPushes() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3, 4))
-        stack.pop()
-        stack.push(mutableVectorOf(4, 5, 6))
-        stack.pop()
-        stack.pop()
-        stack.push(mutableVectorOf())
-        stack.push(mutableVectorOf(5, 6, 7))
-        stack.push(mutableVectorOf(8, 9))
-        val result = buildString {
-            while (stack.isNotEmpty()) {
-                append(stack.pop())
-            }
-        }
-        Assert.assertEquals("987654321", result)
-    }
-}
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
index 29b9c0a..e77e8d6 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/node/NodeUtils.kt
@@ -16,7 +16,10 @@
 
 package androidx.compose.ui.node
 
+import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.platform.InspectorInfo
 
 /**
@@ -38,3 +41,30 @@
         name = "testNode"
     }
 }
+
+@Composable
+internal fun ReverseMeasureLayout(modifier: Modifier, vararg contents: @Composable () -> Unit) =
+    SubcomposeLayout(modifier) { constraints ->
+        var layoutWidth = constraints.minWidth
+        var layoutHeight = constraints.minHeight
+        val subcomposes = mutableListOf<List<Placeable>>()
+
+        // Measure in reverse order
+        contents.reversed().forEachIndexed { index, content ->
+            subcomposes.add(
+                0,
+                subcompose(index, content).map {
+                    it.measure(constraints).also { placeable ->
+                        layoutWidth = maxOf(layoutWidth, placeable.width)
+                        layoutHeight = maxOf(layoutHeight, placeable.height)
+                    }
+                }
+            )
+        }
+
+        layout(layoutWidth, layoutHeight) {
+
+            // But place in direct order - it sets direct draw order
+            subcomposes.forEach { placeables -> placeables.forEach { it.place(0, 0) } }
+        }
+    }
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
index 5abbb34..5906b9f 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogTest.kt
@@ -15,8 +15,11 @@
  */
 package androidx.compose.ui.window
 
-import android.content.res.Configuration
+import android.util.DisplayMetrics
 import android.view.KeyEvent
+import android.view.MotionEvent.ACTION_DOWN
+import android.view.MotionEvent.ACTION_UP
+import android.view.View
 import androidx.activity.OnBackPressedDispatcher
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
@@ -35,8 +38,11 @@
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.gesture.MotionEvent
+import androidx.compose.ui.gesture.PointerProperties
+import androidx.compose.ui.input.pointer.PointerCoords
 import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.assertIsDisplayed
@@ -259,9 +265,9 @@
     fun canFillScreenWidth_dependingOnProperty() {
         var box1Width = 0
         var box2Width = 0
-        lateinit var configuration: Configuration
+        lateinit var displayMetrics: DisplayMetrics
         rule.setContent {
-            configuration = LocalConfiguration.current
+            displayMetrics = LocalView.current.context.resources.displayMetrics
             Dialog(
                 onDismissRequest = {},
                 properties = DialogProperties(usePlatformDefaultWidth = false)
@@ -272,7 +278,7 @@
                 Box(Modifier.fillMaxSize().onSizeChanged { box2Width = it.width })
             }
         }
-        val expectedWidth = with(rule.density) { configuration.screenWidthDp.dp.roundToPx() }
+        val expectedWidth = with(rule.density) { displayMetrics.widthPixels }
         assertThat(box1Width).isEqualTo(expectedWidth)
         assertThat(box2Width).isLessThan(box1Width)
     }
@@ -313,6 +319,75 @@
         }
     }
 
+    @Test
+    fun dismissWhenClickingOutsideContent() {
+        var dismissed = false
+        var clicked = false
+        lateinit var composeView: View
+        val clickBoxTag = "clickBox"
+        rule.setContent {
+            Dialog(
+                onDismissRequest = { dismissed = true },
+                properties =
+                    DialogProperties(
+                        usePlatformDefaultWidth = false,
+                        decorFitsSystemWindows = false
+                    )
+            ) {
+                composeView = LocalView.current
+                Box(Modifier.size(10.dp).testTag(clickBoxTag).clickable { clicked = true })
+            }
+        }
+
+        // click inside the compose view
+        rule.onNodeWithTag(clickBoxTag).performClick()
+
+        rule.waitForIdle()
+
+        assertThat(dismissed).isFalse()
+        assertThat(clicked).isTrue()
+
+        clicked = false
+
+        // click outside the compose view
+        rule.waitForIdle()
+        var root = composeView
+        while (root.parent is View) {
+            root = root.parent as View
+        }
+
+        rule.runOnIdle {
+            val x = root.width / 4f
+            val y = root.height / 4f
+            val down =
+                MotionEvent(
+                    eventTime = 0,
+                    action = ACTION_DOWN,
+                    numPointers = 1,
+                    actionIndex = 0,
+                    pointerProperties = arrayOf(PointerProperties(0)),
+                    pointerCoords = arrayOf(PointerCoords(x, y)),
+                    root
+                )
+            root.dispatchTouchEvent(down)
+            val up =
+                MotionEvent(
+                    eventTime = 10,
+                    action = ACTION_UP,
+                    numPointers = 1,
+                    actionIndex = 0,
+                    pointerProperties = arrayOf(PointerProperties(0)),
+                    pointerCoords = arrayOf(PointerCoords(x, y)),
+                    root
+                )
+            root.dispatchTouchEvent(up)
+        }
+        rule.waitForIdle()
+
+        assertThat(dismissed).isTrue()
+        assertThat(clicked).isFalse()
+    }
+
     private fun setupDialogTest(
         closeDialogOnDismiss: Boolean = true,
         dialogProperties: DialogProperties = DialogProperties(),
diff --git a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
index e85b14c..cde57e5 100644
--- a/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
+++ b/compose/ui/ui/src/androidInstrumentedTest/kotlin/androidx/compose/ui/window/DialogWithInsetsTest.kt
@@ -15,12 +15,16 @@
  */
 package androidx.compose.ui.window
 
+import android.content.res.Configuration
+import android.os.Build
 import android.view.View
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.ime
 import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.safeDrawingPadding
 import androidx.compose.material.TextField
 import androidx.compose.runtime.SideEffect
 import androidx.compose.ui.Alignment
@@ -29,10 +33,18 @@
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.onPlaced
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
 import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.requestFocus
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
 import androidx.core.graphics.Insets
@@ -41,8 +53,10 @@
 import androidx.core.view.WindowInsetsControllerCompat
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.CountDownLatch
 import java.util.concurrent.TimeUnit
+import kotlin.math.roundToInt
 import org.junit.Assert.assertNotEquals
 import org.junit.Rule
 import org.junit.Test
@@ -129,6 +143,90 @@
         assertNotEquals(Insets.NONE, imeInsets)
     }
 
+    @Test
+    fun dialogCanTakeEntireScreen() {
+        var size = IntSize.Zero
+        var displayWidth = 0
+        var displayHeight = 0
+        var insetsLeft = 0
+        var insetsTop = 0
+        var insetsRight = 0
+        var insetsBottom = 0
+        var textTop = 0
+        var controller: SoftwareKeyboardController? = null
+        rule.setContent {
+            val displayMetrics = LocalView.current.resources.displayMetrics
+            controller = LocalSoftwareKeyboardController.current
+            displayWidth = displayMetrics.widthPixels
+            displayHeight = displayMetrics.heightPixels
+            Box(Modifier.fillMaxSize()) {
+                Dialog(
+                    {},
+                    properties =
+                        DialogProperties(
+                            decorFitsSystemWindows = false,
+                            usePlatformDefaultWidth = false
+                        )
+                ) {
+                    val insets = WindowInsets.safeDrawing
+
+                    Box(
+                        Modifier.fillMaxSize()
+                            .layout { m, c ->
+                                val p = m.measure(c)
+                                size = IntSize(p.width, p.height)
+                                insetsTop = insets.getTop(this)
+                                insetsLeft = insets.getLeft(this, layoutDirection)
+                                insetsBottom = insets.getBottom(this)
+                                insetsRight = insets.getRight(this, layoutDirection)
+                                layout(p.width, p.height) { p.place(0, 0) }
+                            }
+                            .safeDrawingPadding()
+                    ) {
+                        TextField(
+                            value = "Hello",
+                            onValueChange = {},
+                            Modifier.align(Alignment.BottomStart).testTag("textField").onPlaced {
+                                layoutCoordinates ->
+                                textTop = layoutCoordinates.positionInRoot().y.roundToInt()
+                            }
+                        )
+                    }
+                }
+            }
+        }
+        rule.waitForIdle()
+
+        if (
+            Build.VERSION.SDK_INT >= 35 &&
+                rule.activity.applicationContext.applicationInfo.targetSdkVersion >= 35
+        ) {
+            // On SDK >= 35, the metrics is the size of the entire screen
+            assertThat(size.width).isEqualTo(displayWidth)
+            assertThat(size.height).isEqualTo(displayHeight)
+        } else {
+            // On SDK < 35, the metrics is the size of the screen with some insets removed
+            assertThat(size.width).isAtLeast(displayWidth)
+            assertThat(size.height).isAtLeast(displayHeight)
+        }
+        // There is going to be some insets
+        assertThat(maxOf(insetsLeft, insetsTop, insetsRight, insetsBottom)).isNotEqualTo(0)
+
+        val hardKeyboardHidden =
+            rule.runOnUiThread { rule.activity.resources.configuration.hardKeyboardHidden }
+        if (hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+            return // can't launch the IME when the hardware keyboard is up.
+        }
+        val bottomInsetsBeforeIme = insetsBottom
+        val textTopBeforeIme = textTop
+        rule.onNodeWithTag("textField").requestFocus()
+        rule.waitUntil {
+            controller?.show()
+            insetsBottom != bottomInsetsBeforeIme
+        }
+        rule.runOnIdle { assertThat(textTop).isLessThan(textTopBeforeIme) }
+    }
+
     private fun findDialogWindowProviderInParent(view: View): DialogWindowProvider? {
         if (view is DialogWindowProvider) {
             return view
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
index ef4b85e..2a1655b 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/node/UiApplier.android.kt
@@ -45,4 +45,8 @@
         super.onEndChanges()
         root.owner?.onEndApplyChanges()
     }
+
+    override fun reuse() {
+        current.onReuse()
+    }
 }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
index 83c30cd..475f5dba 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/window/AndroidDialog.android.kt
@@ -20,13 +20,16 @@
 import android.graphics.Outline
 import android.os.Build
 import android.view.ContextThemeWrapper
+import android.view.Gravity
 import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.View
+import android.view.View.OnLayoutChangeListener
 import android.view.ViewGroup
 import android.view.ViewOutlineProvider
 import android.view.Window
 import android.view.WindowManager
+import android.widget.FrameLayout
 import androidx.activity.ComponentDialog
 import androidx.activity.addCallback
 import androidx.compose.runtime.Composable
@@ -57,8 +60,11 @@
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.util.fastMaxBy
-import androidx.compose.ui.util.fastRoundToInt
+import androidx.core.view.OnApplyWindowInsetsListener
+import androidx.core.view.ViewCompat
 import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsAnimationCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.lifecycle.findViewTreeLifecycleOwner
 import androidx.lifecycle.findViewTreeViewModelStoreOwner
 import androidx.lifecycle.setViewTreeLifecycleOwner
@@ -77,16 +83,19 @@
  * @property securePolicy Policy for setting [WindowManager.LayoutParams.FLAG_SECURE] on the
  *   dialog's window.
  * @property usePlatformDefaultWidth Whether the width of the dialog's content should be limited to
- *   the platform default, which is smaller than the screen width.
+ *   the platform default, which is smaller than the screen width. It is recommended to use
+ *   [decorFitsSystemWindows] set to `false` when [usePlatformDefaultWidth] is false to support
+ *   using the entire screen and avoiding UI glitches on some devices when the IME animates in.
  * @property decorFitsSystemWindows Sets [WindowCompat.setDecorFitsSystemWindows] value. Set to
  *   `false` to use WindowInsets. If `false`, the
  *   [soft input mode][WindowManager.LayoutParams.softInputMode] will be changed to
  *   [WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE] and `android:windowIsFloating` is set to
- *   `false` for Android [R][Build.VERSION_CODES.R] and earlier.
+ *   `false` when [decorFitsSystemWindows] is false. When
+ *   `targetSdk` >= [Build.VERSION_CODES.VANILLA_ICE_CREAM], [decorFitsSystemWindows] can only be
+ *   `false` and this property doesn't have any effect.
  */
 @Immutable
-actual class DialogProperties
-constructor(
+actual class DialogProperties(
     actual val dismissOnBackPress: Boolean = true,
     actual val dismissOnClickOutside: Boolean = true,
     val securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit,
@@ -218,6 +227,7 @@
     private var content: @Composable () -> Unit by mutableStateOf({})
 
     var usePlatformDefaultWidth = false
+    var decorFitsSystemWindows = false
 
     override var shouldCreateCompositionOnAttachedToWindow: Boolean = false
         private set
@@ -229,50 +239,16 @@
         createComposition()
     }
 
-    override fun internalOnMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
-        if (usePlatformDefaultWidth) {
-            super.internalOnMeasure(widthMeasureSpec, heightMeasureSpec)
-        } else {
-            // usePlatformDefaultWidth false, so don't want to limit the dialog width to the Android
-            // platform default. Therefore, we create a new measure spec for width, which
-            // corresponds to the full screen width. We do the same for height, even if
-            // ViewRootImpl gives it to us from the first measure.
-            val displayWidthMeasureSpec =
-                MeasureSpec.makeMeasureSpec(displayWidth, MeasureSpec.AT_MOST)
-            val displayHeightMeasureSpec =
-                MeasureSpec.makeMeasureSpec(displayHeight, MeasureSpec.AT_MOST)
-            super.internalOnMeasure(displayWidthMeasureSpec, displayHeightMeasureSpec)
-        }
-    }
-
-    override fun internalOnLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
-        super.internalOnLayout(changed, left, top, right, bottom)
-        // Now set the content size as fixed layout params, such that ViewRootImpl knows
-        // the exact window size.
-        if (!usePlatformDefaultWidth) {
-            val child = getChildAt(0) ?: return
-            window.setLayout(child.measuredWidth, child.measuredHeight)
-        }
-    }
-
-    private val displayWidth: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenWidthDp * density).fastRoundToInt()
-        }
-
-    private val displayHeight: Int
-        get() {
-            val density = context.resources.displayMetrics.density
-            return (context.resources.configuration.screenHeightDp * density).fastRoundToInt()
-        }
-
     @Composable
     override fun Content() {
         content()
     }
 }
 
+private fun adjustedDecorFitsSystemWindows(dialogProperties: DialogProperties, context: Context) =
+    dialogProperties.decorFitsSystemWindows &&
+        context.applicationInfo.targetSdkVersion < Build.VERSION_CODES.VANILLA_ICE_CREAM
+
 private class DialogWrapper(
     private var onDismissRequest: () -> Unit,
     private var properties: DialogProperties,
@@ -288,16 +264,16 @@
          */
         ContextThemeWrapper(
             composeView.context,
-            if (
-                Build.VERSION.SDK_INT >= Build.VERSION_CODES.S || properties.decorFitsSystemWindows
-            ) {
+            if (adjustedDecorFitsSystemWindows(properties, composeView.context)) {
                 R.style.DialogWindowTheme
             } else {
                 R.style.FloatingDialogWindowTheme
             }
         )
     ),
-    ViewRootForInspector {
+    ViewRootForInspector,
+    OnApplyWindowInsetsListener,
+    OnLayoutChangeListener {
 
     private val dialogLayout: DialogLayout
 
@@ -308,15 +284,12 @@
     override val subCompositionView: AbstractComposeView
         get() = dialogLayout
 
-    private val defaultSoftInputMode: Int
-
     init {
         val window = window ?: error("Dialog has no window")
-        defaultSoftInputMode =
-            window.attributes.softInputMode and WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST
         window.requestFeature(Window.FEATURE_NO_TITLE)
         window.setBackgroundDrawableResource(android.R.color.transparent)
-        WindowCompat.setDecorFitsSystemWindows(window, properties.decorFitsSystemWindows)
+        val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+        WindowCompat.setDecorFitsSystemWindows(window, decorFitsSystemWindows)
         dialogLayout =
             DialogLayout(context, window).apply {
                 // Set unique id for AbstractComposeView. This allows state restoration for the
@@ -336,10 +309,8 @@
                         override fun getOutline(view: View, result: Outline) {
                             result.setRect(0, 0, view.width, view.height)
                             // We set alpha to 0 to hide the view's shadow and let the composable to
-                            // draw
-                            // its own shadow. This still enables us to get the extra space needed
-                            // in the
-                            // surface.
+                            // draw its own shadow. This still enables us to get the extra space
+                            // needed in the surface.
                             result.alpha = 0f
                         }
                     }
@@ -359,7 +330,38 @@
 
         // Turn of all clipping so shadows can be drawn outside the window
         (window.decorView as? ViewGroup)?.disableClipping()
-        setContentView(dialogLayout)
+        // Center the ComposeView in a FrameLayout
+        val frameLayout = FrameLayout(context)
+        frameLayout.addView(
+            dialogLayout,
+            FrameLayout.LayoutParams(
+                    FrameLayout.LayoutParams.WRAP_CONTENT,
+                    FrameLayout.LayoutParams.WRAP_CONTENT
+                )
+                .also { it.gravity = Gravity.CENTER }
+        )
+        frameLayout.setOnClickListener { onDismissRequest() }
+        ViewCompat.setOnApplyWindowInsetsListener(frameLayout, this)
+        ViewCompat.setWindowInsetsAnimationCallback(
+            frameLayout,
+            object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
+                override fun onProgress(
+                    insets: WindowInsetsCompat,
+                    runningAnimations: MutableList<WindowInsetsAnimationCompat>
+                ): WindowInsetsCompat {
+                    return insets.inset(
+                        dialogLayout.left,
+                        dialogLayout.top,
+                        frameLayout.width - dialogLayout.right,
+                        frameLayout.height - dialogLayout.bottom
+                    )
+                }
+            }
+        )
+        dialogLayout.addOnLayoutChangeListener(this)
+        frameLayout.addOnLayoutChangeListener(this)
+
+        setContentView(frameLayout)
         dialogLayout.setViewTreeLifecycleOwner(composeView.findViewTreeLifecycleOwner())
         dialogLayout.setViewTreeViewModelStoreOwner(composeView.findViewTreeViewModelStoreOwner())
         dialogLayout.setViewTreeSavedStateRegistryOwner(
@@ -430,21 +432,42 @@
         this.properties = properties
         setSecurePolicy(properties.securePolicy)
         setLayoutDirection(layoutDirection)
-        if (properties.usePlatformDefaultWidth && !dialogLayout.usePlatformDefaultWidth) {
-            // Undo fixed size in internalOnLayout, which would suppress size changes when
-            // usePlatformDefaultWidth is true.
-            window?.setLayout(
-                WindowManager.LayoutParams.WRAP_CONTENT,
-                WindowManager.LayoutParams.WRAP_CONTENT
-            )
-        }
         dialogLayout.usePlatformDefaultWidth = properties.usePlatformDefaultWidth
-        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
-            if (properties.decorFitsSystemWindows) {
-                window?.setSoftInputMode(defaultSoftInputMode)
-            } else {
-                @Suppress("DEPRECATION")
-                window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
+        val decorFitsSystemWindows = adjustedDecorFitsSystemWindows(properties, context)
+        dialogLayout.decorFitsSystemWindows = decorFitsSystemWindows
+        val window = window
+        if (window != null) {
+            val softInput =
+                when {
+                    decorFitsSystemWindows ->
+                        WindowManager.LayoutParams.SOFT_INPUT_ADJUST_UNSPECIFIED
+                    Build.VERSION.SDK_INT < Build.VERSION_CODES.S ->
+                        @Suppress("DEPRECATION") WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
+                    else -> WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING
+                }
+            window.setSoftInputMode(softInput)
+            val attrs = window.attributes
+            val measurementWidth =
+                if (properties.usePlatformDefaultWidth) {
+                    WindowManager.LayoutParams.WRAP_CONTENT
+                } else {
+                    WindowManager.LayoutParams.MATCH_PARENT
+                }
+            val measurementHeight =
+                if (properties.usePlatformDefaultWidth || decorFitsSystemWindows) {
+                    WindowManager.LayoutParams.WRAP_CONTENT
+                } else {
+                    WindowManager.LayoutParams.MATCH_PARENT
+                }
+            if (
+                attrs.width != measurementWidth ||
+                    attrs.height != measurementHeight ||
+                    attrs.gravity != Gravity.CENTER
+            ) {
+                attrs.width = measurementWidth
+                attrs.height = measurementHeight
+                attrs.gravity = Gravity.CENTER
+                window.attributes = attrs
             }
         }
     }
@@ -466,6 +489,28 @@
         // Prevents the dialog from dismissing itself
         return
     }
+
+    override fun onApplyWindowInsets(v: View, insets: WindowInsetsCompat): WindowInsetsCompat {
+        val left = dialogLayout.left
+        val top = dialogLayout.top
+        val right = v.width - dialogLayout.right
+        val bottom = v.height - dialogLayout.bottom
+        return insets.inset(left, top, right, bottom)
+    }
+
+    override fun onLayoutChange(
+        v: View,
+        left: Int,
+        top: Int,
+        right: Int,
+        bottom: Int,
+        oldLeft: Int,
+        oldTop: Int,
+        oldRight: Int,
+        oldBottom: Int
+    ) {
+        v.requestApplyInsets()
+    }
 }
 
 @Composable
diff --git a/compose/ui/ui/src/androidMain/res/values/styles.xml b/compose/ui/ui/src/androidMain/res/values/styles.xml
index e1211d4..d0e837b 100644
--- a/compose/ui/ui/src/androidMain/res/values/styles.xml
+++ b/compose/ui/ui/src/androidMain/res/values/styles.xml
@@ -19,11 +19,13 @@
     <style name="DialogWindowTheme">
         <item name="android:windowClipToOutline">false</item>
     </style>
-    <!-- Style for decorFitsSystemWindows = false on API 30 and earlier. WindowInsets won't
-     be set on Dialogs without android:windowIsFloating set to false. -->
+    <!-- Style for decorFitsSystemWindows = false -->
     <style name="FloatingDialogWindowTheme">
         <item name="android:windowClipToOutline">false</item>
         <item name="android:dialogTheme">@style/FloatingDialogTheme</item>
+        <item name="android:statusBarColor">@android:color/transparent</item>
+        <item name="android:navigationBarColor">@android:color/transparent</item>
+        <item name="android:backgroundDimEnabled">true</item>
     </style>
     <style name="FloatingDialogTheme">
         <item name="android:windowIsFloating">false</item>
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
index c1363c7..0328f65 100644
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
+++ b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/DelegatingNodeTest.kt
@@ -209,14 +209,14 @@
             layout(d)
         }
         val recorder = Recorder()
-        x.visitSubtree(Nodes.Draw, recorder)
+        x.visitSubtree(Nodes.Draw, block = recorder)
         assertThat(recorder.recorded)
             .isEqualTo(
                 listOf(
                     a.wrapped,
                     b,
-                    d,
                     c.wrapped,
+                    d,
                 )
             )
     }
diff --git a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt b/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
deleted file mode 100644
index 951cb3d..0000000
--- a/compose/ui/ui/src/androidUnitTest/kotlin/androidx/compose/ui/node/NestedVectorStackTest.kt
+++ /dev/null
@@ -1,76 +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.compose.ui.node
-
-import androidx.compose.runtime.collection.mutableVectorOf
-import com.google.common.truth.Truth
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class NestedVectorStackTest {
-
-    @Test
-    fun testEnumerationOrder() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(6, 5, 4, 3, 2, 1))
-    }
-
-    @Test
-    fun testEnumerationOrderPartiallyPoppingMiddleVectors() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-
-        Truth.assertThat(stack.pop()).isEqualTo(3)
-
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.pop()).isEqualTo(6)
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4, 2, 1))
-    }
-
-    @Test
-    fun testEnumerationOrderFullyPoppingMiddleVectors() {
-        val stack = NestedVectorStack<Int>()
-        stack.push(mutableVectorOf(1, 2, 3))
-
-        Truth.assertThat(stack.pop()).isEqualTo(3)
-        Truth.assertThat(stack.pop()).isEqualTo(2)
-        Truth.assertThat(stack.pop()).isEqualTo(1)
-
-        stack.push(mutableVectorOf(4, 5, 6))
-
-        Truth.assertThat(stack.pop()).isEqualTo(6)
-
-        Truth.assertThat(stack.enumerate()).isEqualTo(listOf(5, 4))
-    }
-}
-
-internal fun <T> NestedVectorStack<T>.enumerate(): List<T> {
-    val result = mutableListOf<T>()
-    var item: T? = pop()
-    while (item != null) {
-        result.add(item)
-        item = if (isNotEmpty()) pop() else null
-    }
-    return result
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
index 8010fb9..d64d81b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/ApproachMeasureScope.kt
@@ -90,7 +90,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
index dc853ae..1bdba48 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt
@@ -351,7 +351,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
@@ -365,7 +365,7 @@
             override val height: Int
                 get() = h
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
@@ -385,7 +385,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
@@ -399,7 +399,7 @@
             override val height: Int
                 get() = h
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
index a2b16cc..e7a3137 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureResult.kt
@@ -19,7 +19,7 @@
      * Alignment lines that can be used by parents to align this layout. This only includes the
      * alignment lines of this layout and not children.
      */
-    val alignmentLines: Map<AlignmentLine, Int>
+    val alignmentLines: Map<out AlignmentLine, Int>
 
     /**
      * An optional lambda function used to create [Ruler]s for child layout. This may be
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
index 36e7ea8..978165e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasureScope.kt
@@ -47,7 +47,7 @@
     fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+        alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
         placementBlock: Placeable.PlacementScope.() -> Unit
     ) = layout(width, height, alignmentLines, null, placementBlock)
 
@@ -69,7 +69,7 @@
     fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
+        alignmentLines: Map<out AlignmentLine, Int> = emptyMap(),
         rulers: (RulerScope.() -> Unit)? = null,
         placementBlock: Placeable.PlacementScope.() -> Unit
     ): MeasureResult {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
index 442dada..08a5f0c 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt
@@ -911,7 +911,7 @@
         override fun layout(
             width: Int,
             height: Int,
-            alignmentLines: Map<AlignmentLine, Int>,
+            alignmentLines: Map<out AlignmentLine, Int>,
             rulers: (RulerScope.() -> Unit)?,
             placementBlock: Placeable.PlacementScope.() -> Unit
         ): MeasureResult {
@@ -923,7 +923,7 @@
                 override val height: Int
                     get() = height
 
-                override val alignmentLines: Map<AlignmentLine, Int>
+                override val alignmentLines: Map<out AlignmentLine, Int>
                     get() = alignmentLines
 
                 override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
index ffc88f5..f967cd8 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/DelegatableNode.kt
@@ -99,48 +99,33 @@
     return null
 }
 
-internal inline fun DelegatableNode.visitSubtree(mask: Int, block: (Modifier.Node) -> Unit) {
-    // TODO(lmr): we might want to add some safety wheels to prevent this from being called
-    //  while one of the chains is being diffed / updated.
-    checkPrecondition(node.isAttached) { "visitSubtree called on an unattached node" }
-    var node: Modifier.Node? = node.child
-    var layout: LayoutNode? = requireLayoutNode()
-    // we use this bespoke data structure here specifically for traversing children. In the
-    // depth first traversal you would typically do a `stack.addAll(node.children)` type
-    // call, but to avoid enumerating the vector and moving into our stack, we simply keep
-    // a stack of vectors and keep track of where we are in each
-    val nodes = NestedVectorStack<LayoutNode>()
-    while (layout != null) {
-        // NOTE: the ?: is important here for the starting condition, since we are starting
-        // at THIS node, and not the head of this node chain.
-        node = node ?: layout.nodes.head
-        if (node.aggregateChildKindSet and mask != 0) {
-            while (node != null) {
-                if (node.kindSet and mask != 0) {
-                    block(node)
-                }
-                node = node.child
-            }
-        }
-        node = null
-        nodes.push(layout._children)
-        layout = if (nodes.isNotEmpty()) nodes.pop() else null
+private fun LayoutNode.getChildren(zOrder: Boolean) =
+    if (zOrder) {
+        zSortedChildren
+    } else {
+        _children
     }
+
+private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(
+    node: Modifier.Node,
+    zOrder: Boolean,
+) {
+    node.requireLayoutNode().getChildren(zOrder).forEachReversed { add(it.nodes.head) }
 }
 
-private fun MutableVector<Modifier.Node>.addLayoutNodeChildren(node: Modifier.Node) {
-    node.requireLayoutNode()._children.forEachReversed { add(it.nodes.head) }
-}
-
-internal inline fun DelegatableNode.visitChildren(mask: Int, block: (Modifier.Node) -> Unit) {
+internal inline fun DelegatableNode.visitChildren(
+    mask: Int,
+    zOrder: Boolean,
+    block: (Modifier.Node) -> Unit
+) {
     check(node.isAttached) { "visitChildren called on an unattached node" }
     val branches = mutableVectorOf<Modifier.Node>()
     val child = node.child
-    if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+    if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
     while (branches.isNotEmpty()) {
         val branch = branches.removeAt(branches.lastIndex)
         if (branch.aggregateChildKindSet and mask == 0) {
-            branches.addLayoutNodeChildren(branch)
+            branches.addLayoutNodeChildren(branch, zOrder)
             // none of these nodes match the mask, so don't bother traversing them
             continue
         }
@@ -159,11 +144,15 @@
  * visit the shallow tree of children of a given mask, but if block returns true, we will continue
  * traversing below it
  */
-internal inline fun DelegatableNode.visitSubtreeIf(mask: Int, block: (Modifier.Node) -> Boolean) {
+internal inline fun DelegatableNode.visitSubtreeIf(
+    mask: Int,
+    zOrder: Boolean,
+    block: (Modifier.Node) -> Boolean
+) {
     checkPrecondition(node.isAttached) { "visitSubtreeIf called on an unattached node" }
     val branches = mutableVectorOf<Modifier.Node>()
     val child = node.child
-    if (child == null) branches.addLayoutNodeChildren(node) else branches.add(child)
+    if (child == null) branches.addLayoutNodeChildren(node, zOrder) else branches.add(child)
     outer@ while (branches.isNotEmpty()) {
         val branch = branches.removeAt(branches.size - 1)
         if (branch.aggregateChildKindSet and mask != 0) {
@@ -176,7 +165,7 @@
                 node = node.child
             }
         }
-        branches.addLayoutNodeChildren(branch)
+        branches.addLayoutNodeChildren(branch, zOrder)
     }
 }
 
@@ -264,33 +253,41 @@
     return null
 }
 
-internal inline fun <reified T> DelegatableNode.visitSubtree(
-    type: NodeKind<T>,
-    block: (T) -> Unit
-) = visitSubtree(type.mask) { it.dispatchForKind(type, block) }
-
 internal inline fun <reified T> DelegatableNode.visitChildren(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Unit
-) = visitChildren(type.mask) { it.dispatchForKind(type, block) }
+) = visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
 
 internal inline fun <reified T> DelegatableNode.visitSelfAndChildren(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Unit
 ) {
     node.dispatchForKind(type, block)
-    visitChildren(type.mask) { it.dispatchForKind(type, block) }
+    visitChildren(type.mask, zOrder) { it.dispatchForKind(type, block) }
 }
 
 internal inline fun <reified T> DelegatableNode.visitSubtreeIf(
     type: NodeKind<T>,
+    zOrder: Boolean = false,
     block: (T) -> Boolean
 ) =
-    visitSubtreeIf(type.mask) foo@{ node ->
+    visitSubtreeIf(type.mask, zOrder) foo@{ node ->
         node.dispatchForKind(type) { if (!block(it)) return@foo false }
         true
     }
 
+internal inline fun <reified T> DelegatableNode.visitSubtree(
+    type: NodeKind<T>,
+    zOrder: Boolean = false,
+    block: (T) -> Unit
+) =
+    visitSubtreeIf(type.mask, zOrder) {
+        it.dispatchForKind(type, block)
+        true
+    }
+
 internal fun DelegatableNode.has(type: NodeKind<*>): Boolean =
     node.aggregateChildKindSet and type.mask != 0
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
index a4d667fb..fe22dff 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeAlignmentLines.kt
@@ -94,9 +94,9 @@
     /** The alignment lines of this layout, inherited + intrinsic */
     private val alignmentLineMap: MutableMap<AlignmentLine, Int> = hashMapOf()
 
-    fun getLastCalculation(): Map<AlignmentLine, Int> = alignmentLineMap
+    fun getLastCalculation(): Map<out AlignmentLine, Int> = alignmentLineMap
 
-    protected abstract val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    protected abstract val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
 
     protected abstract fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int
 
@@ -201,7 +201,7 @@
 internal class LayoutNodeAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
     AlignmentLines(alignmentLinesOwner) {
 
-    override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
         get() = measureResult.alignmentLines
 
     override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
@@ -215,7 +215,7 @@
 internal class LookaheadAlignmentLines(alignmentLinesOwner: AlignmentLinesOwner) :
     AlignmentLines(alignmentLinesOwner) {
 
-    override val NodeCoordinator.alignmentLinesMap: Map<AlignmentLine, Int>
+    override val NodeCoordinator.alignmentLinesMap: Map<out AlignmentLine, Int>
         get() = lookaheadDelegate!!.measureResult.alignmentLines
 
     override fun NodeCoordinator.getPositionFor(alignmentLine: AlignmentLine): Int =
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
index 3df4e68..bc03b51 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeLayoutDelegate.kt
@@ -950,7 +950,7 @@
             return true
         }
 
-        override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+        override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
             if (!duringAlignmentLinesQuery) {
                 // Mark alignments used by modifier
                 if (layoutState == LayoutState.Measuring) {
@@ -1278,7 +1278,7 @@
             }
         }
 
-        override fun calculateAlignmentLines(): Map<AlignmentLine, Int> {
+        override fun calculateAlignmentLines(): Map<out AlignmentLine, Int> {
             if (!duringAlignmentLinesQuery) {
                 if (layoutState == LayoutState.LookaheadMeasuring) {
                     // Mark alignments used by modifier
@@ -1894,7 +1894,7 @@
     fun layoutChildren()
 
     /** Recalculate the alignment lines if dirty, and layout children as needed. */
-    fun calculateAlignmentLines(): Map<AlignmentLine, Int>
+    fun calculateAlignmentLines(): Map<out AlignmentLine, Int>
 
     /**
      * Parent [AlignmentLinesOwner]. This will be the AlignmentLinesOwner for the same pass but for
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
index f5e4439..8bf8d59 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LookaheadDelegate.kt
@@ -203,7 +203,7 @@
     override fun layout(
         width: Int,
         height: Int,
-        alignmentLines: Map<AlignmentLine, Int>,
+        alignmentLines: Map<out AlignmentLine, Int>,
         rulers: (RulerScope.() -> Unit)?,
         placementBlock: PlacementScope.() -> Unit
     ): MeasureResult {
@@ -215,7 +215,7 @@
             override val height: Int
                 get() = height
 
-            override val alignmentLines: Map<AlignmentLine, Int>
+            override val alignmentLines: Map<out AlignmentLine, Int>
                 get() = alignmentLines
 
             override val rulers: (RulerScope.() -> Unit)?
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
deleted file mode 100644
index 7f93d07..0000000
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NestedVectorStack.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.node
-
-import androidx.compose.runtime.collection.MutableVector
-
-internal class NestedVectorStack<T> {
-    // number of vectors in the stack
-    private var size = 0
-    // holds the current "top" index for each vector
-    private var currentIndexes = IntArray(16)
-    private var vectors = arrayOfNulls<MutableVector<T>>(16)
-
-    fun isNotEmpty(): Boolean {
-        return size > 0 && currentIndexes[size - 1] >= 0
-    }
-
-    fun pop(): T {
-        check(size > 0) { "Cannot call pop() on an empty stack. Guard with a call to isNotEmpty()" }
-        val indexOfVector = size - 1
-        val indexOfItem = currentIndexes[indexOfVector]
-        val vector = vectors[indexOfVector]!!
-        if (indexOfItem > 0) currentIndexes[indexOfVector]--
-        else if (indexOfItem == 0) {
-            vectors[indexOfVector] = null
-            size--
-        }
-        return vector[indexOfItem]
-    }
-
-    fun push(vector: MutableVector<T>) {
-        // if the vector is empty there is no reason for us to add it
-        if (vector.isEmpty()) return
-        val nextIndex = size
-        // check to see that we have capacity to add another vector
-        if (nextIndex >= currentIndexes.size) {
-            currentIndexes = currentIndexes.copyOf(currentIndexes.size * 2)
-            vectors = vectors.copyOf(vectors.size * 2)
-        }
-        currentIndexes[nextIndex] = vector.size - 1
-        vectors[nextIndex] = vector
-        size++
-    }
-}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
index b522a01..9a1ba69 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/NodeCoordinator.kt
@@ -1407,7 +1407,7 @@
 @Suppress("PrimitiveInCollection")
 private fun compareEquals(
     a: MutableObjectIntMap<AlignmentLine>?,
-    b: Map<AlignmentLine, Int>
+    b: Map<out AlignmentLine, Int>
 ): Boolean {
     if (a == null) return false
     if (a.size != b.size) return false
diff --git a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
index ed0ed4d..007491b 100644
--- a/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
+++ b/core/core/src/androidTest/java/androidx/core/app/JobIntentServiceTest.java
@@ -37,6 +37,7 @@
 import androidx.test.filters.MediumTest;
 
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -365,6 +366,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueOne() throws Throwable {
         initStatics();
 
@@ -386,6 +388,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueMultiple() throws Throwable {
         initStatics();
 
@@ -410,6 +413,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     public void testEnqueueSubWork() throws Throwable {
         initStatics();
 
@@ -439,6 +443,7 @@
      */
     @MediumTest
     @Test
+    @Ignore("JobIntentService is deprecated and no longer maintained")
     @RequiresApi(26)
     public void testStopWhileWorking() throws Throwable {
         if (Build.VERSION.SDK_INT < 26) {
diff --git a/development/update_studio.sh b/development/update_studio.sh
index cdd888e..3fa7ebb 100755
--- a/development/update_studio.sh
+++ b/development/update_studio.sh
@@ -48,7 +48,7 @@
 sed -i "s/androidStudio = .*/androidStudio = \"$STUDIO_VERSION\"/g" gradle/libs.versions.toml
 
 # update settings.gradle
-sed -i "s/com.android.settings:com.android.settings.gradle.plugin:.*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
+sed -i "s/com.android.settings:com.android.settings.gradle.plugin:[0-9a-z\.\-]*/com.android.settings:com.android.settings.gradle.plugin:$AGP_VERSION\")/g" settings.gradle
 
 # Pull all UTP artifacts for ADT version
 ADT_VERSION=${3:-$LINT_VERSION}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5165817..99e5b63 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -67,7 +67,7 @@
 spdxGradlePlugin = "0.6.0"
 sqldelight = "1.3.0"
 retrofit = "2.7.2"
-wire = "4.9.7"
+wire = "5.0.0"
 core = "1.12.0"
 xmlApis = "1.4.01"
 yarn = "1.22.17"
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index efdf962..4a0d0b9 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -3,7 +3,7 @@
 
 sub    CF771F914C2A4A73
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBE2fCWARBAC3v9wYo5kmynmVP+43ccamidflSLQjjpsXpSDLPFokGxeuw0OC
 QJy46m8b5ACoCqRlfwnRRcEHxiSlaBATJA6hi7NRO41R39C62JXsIxNJR16JNQ5k
@@ -33,7 +33,7 @@
 
 pub    82B5574242C20D6F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFC1VWUBDADZwqBEEmSjwy2JADG0qCpvVQzC5KszL0CjzqTLPMBmLKNuc/36
 26MU4yI8Y+pcCTnC3LN9hrI0hxiP4zFFFyLYKkUWCZRAwj4OQlnyTDKa9frKBMed
@@ -52,7 +52,7 @@
 
 sub    43115D7B115DB0C0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFSR0DQBCADw8XL+xgFg9WVPknAIqqb0sUIZ3yNNr8LkuNtwQXnwAcSJkHSt
 C1k2CIKwRPPfcLsb51l3SpxFTs/s5yhyiknDfjqP8IFtLocBSsn3kD4VRjcxFQhc
@@ -80,7 +80,7 @@
 uid    The Legion of the Bouncy Castle Inc. (Maven Repository Artifact Signer) <[email protected]>
 
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGR/8HUBDADJ+V5VgTXFG4xVI/1r07a/pTXoAQhHyJMkVdFScGARsps07VXI
 IsYgPsifOFU55E7uRMZPTLAx5F1uxoZAWGtXIz0d4ISKhobFquH8jZe7TnsJBJNV
@@ -101,7 +101,7 @@
 
 sub    594E23256A36A392
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEqQOcwBEACdPSfBAkHm1b2GdOjB3gGerx/JDn3zYNnNpcQrM8Do0bxDwlfT
 qwLA0P9ju4mzTfHU5kEvm2lrXz8QCZPLe9eY6GxzzSbeXtt+4fP84/YGmsK6DQTy
@@ -146,7 +146,7 @@
 
 sub    8B2A34A7D4A9B8B3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFrKW9IBEACkqUvM7hU1WqOOeb1gZ7pUsRliHuoUvYIrd+hdp+qhPmJ0NG0W
 YhZK5UtJBmqvtHKRkbwYxUuya9zlBmCfQFf0GpFKJ65JSrPSkZADI3aZ4aUkxIUw
@@ -191,7 +191,7 @@
 
 sub    8832A83FA3060393
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBD9AzmcRBACMqgb7IFvC/nLxw7mUAgHENeZXY3JOQJ8wVBevIbbMEeFvzHE2
 diFydqUXocPexduYr0ahkf033WvWdAiNqDLfVW/HFOsc1TpjbHkqPUHtJ62Ya5tg
@@ -220,7 +220,7 @@
 
 sub    51F5B36C761AA122
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFoQh54BEADOuivAfgGKc4/zDwx+AwJdctjTT0znL9knRTYG6ediv2Eq+CXm
 gBM9m5twl+qhUB1NtrdHb4BH49VY9/gHr3JDyo5ewu96qkbeQl4pxW0zmHg/yJx7
@@ -263,7 +263,7 @@
 pub    86FDC7E2A11262CB
 sub    59BA7BFEAD3D7F94
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE2kzuwBCACYV+G9yxNkSjAKSji0B5ipMGM74JAL1Ogtcu+993pLHHYsdXri
 WWXi37x9PLjeHxw63mN26SFyrbMJ4A8erLB03PDjw0DEzAwiu9P2vSvL/RFxGBbk
@@ -291,7 +291,7 @@
 
 sub    1AFEC329B615D06C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEdddbQRBADRgstdUZq7ceq3NYcR5kpoU2tN2Zvg1vptE9FxpDbL73gdLWnI
 C7IAx+NNjdG7Ncdg+u10UZv6OSmhWAd8ubWcD9JxKtS4UXkNPHxhHFHqVPHuCwsQ
@@ -322,7 +322,7 @@
 pub    88BB19A33A18445F
 sub    FF59C22B07640A16
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE//SjoBCADao3lh/I96fWIY2ZU49ljtHR4Vnzmifm3URFNuv/c8McWGxxCy
 Y1+oolgVuJcy4hCqcgbkwTiAfBhjZSmsC1QK/2Vs1awFzGccPcgTBakFw/TUav12
@@ -350,7 +350,7 @@
 
 sub    5E9AEEBA28836032
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGUVRogBEAChVh0t3YAJIdreb6SP/lf4x097IRpOiJ7Ww+DDtXFUhKJBwgfC
 4T10TBGP835tV6TfkEeCPGWABoxaD88zUlSHs7k7v/SfedwfOKbOE3c+oR43JL7P
@@ -395,7 +395,7 @@
 
 sub    E98008460EB9BB34
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF8kuOUBCACo8/VYVfmglgTgmai5FvmNzKi9XIJIK4fHCA1r+t47aGkGy36E
 dSOlApDjqbtuodnyH4jiyBvT599yeMA0O/Pr+zL+dOwdT1kYL/owvT0U9oczvwUj
@@ -422,7 +422,7 @@
 pub    8E3F0DE7AE354651
 sub    D3047B0BA4452AE1
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFMnpeABCAC+vckg+AqDG5Sg+GKbA5t2knu72aD000Qle1X//SjTvPHz0L1v
 rUNzwrqlmah17usczZHOoOCaGjSUFl3nPmBEOlLBh6L4+e2Av8PSbP0qUneaQVgi
@@ -450,7 +450,7 @@
 
 sub    37AE8263DA3084E5
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFu8+5UBDAC74QfHuMgQVUqSmwgE+zWX1YKY4w9a0vKrj7E4tRY8JXaX6GtH
 TWnOkAndsxK3kpUyRx8S7f4HL4Sxf05Tar22nrNkuiQddKjLsdlH7VIolGW1eFm2
@@ -487,7 +487,7 @@
 
 sub    9D149DAC4AC24632
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFPzzfABCADK/wEIRhUCUTj00TcBOxGTPs5ad8jn5D01P7P5ILpLOgmnUp1I
 E3EYy54PQYjDIeOFvEmEywvwMRV8yCVhhYGpOPqbegKwcebXoiMGhJjuRf2nPbdZ
@@ -516,7 +516,7 @@
 
 sub    E04D6BFB21395F43
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF5ZgzYBEADQvBgzh4vKJqO3amYjIUJ85OPCjJdK5G0xSH/nqOGZbo78DjLx
 3PosyuYqV6sIfaCx+NWv+pYnpKdQHbAnQygggjOTByIQJtpmMT80dUsXTxAk6Aim
@@ -561,7 +561,7 @@
 
 sub    F57552EA2A2B5F3F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFUITeIBCADHIijQBuGmC+Oo/XE5qIXxzZ2cK26uD0tlDqaPhRLWt5RP3EbU
 b6X8ZLE2AlmawFzU0IqndrCDxSyuo9+ZFQRYT+stf+qHFjtvVQJh2+4L2LpcPrnf
@@ -590,7 +590,7 @@
 
 sub    684EB33FB007E676
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQSuBEwVyy4RDAC9hprQuF4fCPCYdtMlb0Mfb+6G2TqerT1MebLm8/KHCRnPbFLg
 PwGgcyynLX5R2nXUb6oBZQByDN/Dal0UMuC19KeZX83LTcFE9vr516BMXLXXKmM9
@@ -643,7 +643,7 @@
 pub    971B04F56669B805
 sub    D3664677F6280E44
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBEzZjwMBCAC0ecfE/qkdgq8uJv1c1ZlzegeWH/lxW0W3SWK2RwaHx7LrfpiN
 WhxLkXbK6fkf86FN4579W1+9Qef2yjZCwTfLe6bqj3zZGQWSu7HPw1mmhf9lbhJ9
@@ -671,7 +671,7 @@
 
 sub    CC3328A2F49A80C8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFhlXQUBCACoN2nTeSRVZnGoktKHyiCgeYQ/hEKKKDDAbWubnnQwonCTILaN
 Qw3GmIT6plmi9iy4rl+rJprSzDeQDZngQCx1KPYcXCrrc0pnjERDaogw9fC3c3z2
@@ -700,7 +700,7 @@
 
 sub    C327DD2B96A50E1C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF6WyHgBEADOrbvGGDYVckFcUofqKiYrBneClFJH1ANheF+KIekmnFV2SH1Z
 RS2rw12IbpCjwqjhFTMWH2UTLF6pAsSGIufTrSVUAF2WxHw84Y60KmwuYayJCVd3
@@ -745,7 +745,7 @@
 
 sub    B89991D171A02F5C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF9amNkBEADKyJj5snYd8bZpONpu1QHf7c/TK9HxcMzGZaIv9QzViX6CtEHb
 2Q2x6ejXQ2frECMrvns5JAJd21B6215EhlOqrHSMkTrQ6fvOIfWd0huZ0QHr4FME
@@ -791,7 +791,7 @@
 
 sub    80CFA7C482552DC3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGJGMxoBDADF9xkWwxwN72wRh0al9ARzTTIHpcVBIjDij1Xr768zMMRdKOsQ
 aEHRTBKArAfGl6Xt6CfYnu3wMgEDUfh50s9NPOKvhpKtqdIlUxZLEJ807ebW3MD+
@@ -829,7 +829,7 @@
 sub    6C907406A9482E08
 sub    B2581403B6FA2318
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEJDQwIRBAD8GFadoCUDLBvFZaR/xu2KS+k8dgfqtYKXpEQ2CH05lpFWrTXo
 C6h9koiHcsMKtgLFE0LG6nHTUbLs2W7gBCaCk9HzMmsFI5D7RDbyga0wvvg96y4d
@@ -877,7 +877,7 @@
 
 sub    D66472CF54179CC4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFKD+PgBEAC8IkWujQlmU0/7+QPZFsc/z/rXgg7BQyo330QK4HeMzeCK6WHa
 SWzVDM9h6nFDs6Xln6YexbZUjLsxS/a/Ox2i26Qg8B+NghgiratbdJsByRrU/3la
@@ -920,7 +920,7 @@
 pub    9A2C7A98E457C53D
 sub    92AECA6AC21DB816
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFjN6bABCADHL68BhnXVXyJhOA9kO9cBwJXKmav2RftpcpXfaeHJTy+CMQa4
 rFxokx5W7E1IPlLg0qJfKSMeWhimVLOsLhY1MZV8Mb4fkK+SlDz/ah+5ej6dzOs3
@@ -948,7 +948,7 @@
 
 sub    2F3C9EEB05D1D1E3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEzH8KcBEADyHAdW2cHj2SfvmdAG3yG0NIlfdSWXG06k7BGUatjNfaIGHVSv
 r0U3WlGlUowiLqPhZfQf3v/tvd7yDKZ1Tk3p3A3rEVEZQ26u/o66QgTNjl15YmaR
@@ -993,7 +993,7 @@
 
 sub    458AF764D812A037
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEWjofgRBACePEiXmSvjcjUgWkNAFQ/w7w2VSEqe1vuTCrta+ER9JsvhwipP
 2/BEHigFf99TlU0p1UC591LMeYP2UXfQnb3jiyEPKxA06aj1fTGGMoNMAilymvgd
@@ -1026,7 +1026,7 @@
 
 sub    32E3DF6FC5E91334
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEzDDl0BEADHvJW2uff8vfxbfy0IvNOK4aytU+HVEvKEmuSqYEzC8i3BF6RT
 LOxTeRFlu92rYz5ypD0mdNCzQaH0xbkcjialP6FpPCByrM9fFv6hmxZFSY71rvqz
@@ -1071,7 +1071,7 @@
 
 sub    81176177BB514041
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF3xPHMBCAC7OXU5uXXKttUU/BwWm6q08NBC3ybk0fNIfoITWiFA1RtxO7S3
 K4ijImBnLLb7ivjpTtIWzUwFAfSZHc3LgS/TBQJQ2PGsO4/AdaMAcs69irgfoPYY
@@ -1100,7 +1100,7 @@
 
 sub    923C08F9417B222D
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFKws7QBEADEy9+PqF0cjeS1yG4xMRBV+teFNsS+WZW1ATDBl5ETASqMZT7R
 zFWjMWq8Kf3iTMfmPlKVCPIFH1FG+SgMvWpQEEcLCOmUkJR7UYtn2y3vaXXYqawz
@@ -1145,7 +1145,7 @@
 
 sub    E3F6790A5A167F5A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGHDIagBEADpzdCwVjVlHuo8qpu9HtmqNpEW4TB7y6+NX7Q39mj8w+iVskE1
 sL0+BOCdP6ZMiQziWbOQ2FxCd3mD0ixZ7v1i7+0jowySPacJbVNaPPECP38gDte4
@@ -1190,7 +1190,7 @@
 
 sub    BA6D22590B3F9BEA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE4waOEBCADHDHNTq1NRR5TSooIrKY0BTQnaLfjKZfcJOwp+btBJrOUO7+e/
 V3M4DZQclj/e8SBiVmRPK8Oyrv6i5B5+Ee/qNlLjWiO10AJ/PLRjYdoW1V6PlTm7
@@ -1219,7 +1219,7 @@
 
 sub    6366592024774157
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBDsSIk4RBADSCj6rUjV64tYCGT1DYKYR7GthyWpNdGHSYLbETBcDatAe1dzQ
 5NsCgfrlybfyeY+y1lxr3T9bqf6zJWDw/718wff96qmmv1qzexSYtmIrj+h53V82
@@ -1248,7 +1248,7 @@
 
 sub    6A2038967E03726F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFRdA40BCAC0zSALsOjfjr+gO8q+HV4qPWuIRB8S4z//jCEpKypyCRR9sA0W
 IDHG6OqG5fO1bP6VsHvSx32E8YUf0bi8eGgpKj5gJ9jmausRvRHtUHJ0pvZRBw51
@@ -1277,7 +1277,7 @@
 
 sub    8183E80D264EE073
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBE8YNGIBEADEgcfvs8TL3X2Ql62HJ6SrXWAOoHw5CquJxUQkvBGesIT1Hk24
 exiPwrlNE1qUjbVlef1Cwk9ZfwMOpJdfP2MQQbx0nxxqv+JtsoeXUy9bTSvZYBUL
@@ -1322,7 +1322,7 @@
 
 sub    21200D723F53CE38
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFy+swoBCADGyV4k02OjVCrziziYIvIO+qDm8Yqxt4KVd+ISw2DvmKVcP7lx
 z5WVGvxVdAl+Xy7FdcrIJYFCsYfFFxPz+BM6+np2c477HkdIcDwBWiHEoOqMehax
@@ -1351,7 +1351,7 @@
 
 sub    9C4C23E6FFE405BD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE+xZxIBCACzKctn4ez8xOC0pGThhAwjYWGkzcwK4HNaC1usHThBFz3/t8JN
 OqUXRixLyi5wELN6GHlsGVUQS3IfB4JtuhScsieSB8PTree68/knMq6JI08mJqZr
@@ -1379,7 +1379,7 @@
 uid    Tobias Warneke (for development purposes) <[email protected]>
 
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
 xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
@@ -1397,7 +1397,7 @@
 pub    A730529CA355A63E
 sub    D5A25EF82542C54A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEUQYOcRBADsCu4zTVaB4TOhV7NyTvHhG1bqN+3Va5t4vpGQJg4M4U0Yu0ut
 4bCZP8I6rlXGj+TqDKVUx9kfGpIKX6Kw2TvZUYbHIDWh3UhQO1hD4xy4b8rOak1w
@@ -1427,7 +1427,7 @@
 pub    A7764F502A938C99
 sub    F20DB7FEF61CE1E8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFc7oMQBCADaIPEUzMrwF9gnEC+PRn2cSPG8OV4RxXxa88TZm0L7NF7D+F5N
 MNUAZ58oVqFUW+ytgb5iey3X7KjlJXZnuqES4m2Id4N7FlnvrmpeOg7MUc9VmNkt
@@ -1455,7 +1455,7 @@
 
 sub    FB4C179C9305F3B1
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGJZox0BDAC/pjQlGW0w4nlUz/pJo69HlaFXNcTw8B6oGwIAhzer/iJIYaPM
 OYM44uifatxD16n4eFk3ZLHkIYbU+2wfprLlfsMhBuh+esY5qIHqFlhos0yQATGE
@@ -1492,7 +1492,7 @@
 
 sub    4044EDF1BB73EFEA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFELz9cBCAC1cr1c5jWUreRdPYYvk6DK7DwF6dgt7iN4rN2QT75M6ob9Yxow
 QeO709C7V0JXpVCOJ+7gCxnllmktpchRpj7hj3iDdvhVuKMEF4pl+tDyoyzK4Xvh
@@ -1527,7 +1527,7 @@
 
 sub    D36DB5C489BAAC5B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGBoC2ABDACyCWLqqAo9NeThE90hBoYomtgLci5I8+7PxSYeQfzUYjXzZcnh
 6d/zHaeC0zxGhT2LNe5i3p2e36xSeFDobjG2Il/nv+4jFCgbn3TZ2hEingPuPsg5
@@ -1565,7 +1565,7 @@
 
 sub    1A94B14C6A03458D
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGI8r9sBEACZJBV2TNUSsLRo89uC4lfmQxfNDqkE0uZghfFY/p0fr6fkBybO
 WDkPFskAPD32fzrWxZd2kkyCRyUrOmAUC22q8hw96t28+RqZymvetIa0f8GQGgkO
@@ -1610,7 +1610,7 @@
 
 sub    3737A3AACF645E77
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGZPgeABEACy60J6lwPVmvj7LnAwkaggbBVA2kZ3P+YxWLV/hhdMQplYGJKR
 mwd3bnBdR8duAwyEh+VlEsw7+FP14bCV6DihTOhzKwVliprV8Jkt6cog4ccFIjHI
@@ -1656,7 +1656,7 @@
 sub    501B5ADEF57CE6A3
 sub    5D9FFE7B8E3DEA8B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF0YzcYBCADmNIEEzvSsnJnxH0u89Hb5vCCkl+45dWHyCMsCLNty8yL214LV
 B35gnU+6BvRXN3DmTpreCV8/wgI2h1eq83dTO2AsnJTxTjvYpiwAtWhONxWxCU1Y
@@ -1704,7 +1704,7 @@
 
 sub    7B92B768F9D37337
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGHu5IUBEAC5appY0S1OLTgUnwbM49Y5Km/pL0SWE1nLwGPQKG/YBpcVaKhE
 zn1w7/3gtqrfQr811OpMVjrV0LAKh+gPg25m4GIYpqtqgO1u3T7e5Za5dq8f0fAP
@@ -1749,7 +1749,7 @@
 
 sub    3F7EB3ADB58CF1E0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEYlrX6hYJKwYBBAHaRw8BAQdAuMmfV7N5GrH0mrA6JbgOFXi5Qx4V+DN6DiEJ
 yQWXOcu0JlBpZXJyZS1ZdmVzIFJpY2F1IDxweS5yaWNhdUBnbWFpbC5jb20+uDgE
@@ -1765,7 +1765,7 @@
 
 sub    38185785755267BD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQMuBFKTz1wRCADOdMCDOKXlBuQpG7mnQ/5rppqhS0SXdKvNZ5pYrJKib1LLtlS/
 LOeABja3E1ky+znvTqnEEtai7fNhw36zPdUjhPKE0TZwn2aK5fyctkcfqBFsja3E
@@ -1806,7 +1806,7 @@
 
 sub    C707929E5065E0BC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGJm9OEBEAClTz80QmRmi9bpX4m77aas5Q+x+gRtlEg6IWU6QfrGdazVO/3S
 brF3KmsEnxW8fjqv5drswed8FmUVdEsTcco31jxeD+fiBFCAU8BnrpL/+iIALMRY
@@ -1851,7 +1851,7 @@
 
 sub    7892707E9657EBD4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFdbSfIBCACrFI0ai/abnV2U2Wa9QQZwGk3Fegc8laiuTKc0GoYdyptd83/H
 hD5S61ppdkOugBjVTHdgda3xJ7zBZdnwjZvV/TyayQltbh6hU+BMlEolzXLgyvY7
@@ -1878,7 +1878,7 @@
 pub    B16698A4ADF4D638
 sub    32784D4F004B405B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFM1v9ABCADD0KoXq2ZKlUHeIVovQy3gFmW9oFAaraV48ouv8cYvqdf+s91H
 NyqeyNPT/ihFeNqZJUAMyPdwN5xrWD6gxMrOCR7BFhA5kLmAKz4HfFCQ05ViyQdI
@@ -1904,7 +1904,7 @@
 pub    B341DDB020FCB6AB
 sub    315693699F8D102F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEowbDsRBAD2jx/Q2jNuCkgiS3fzIj6EzDP+2kipIKH2LEnpnTiBlds2PFYM
 xYibVab/grgQODxTdDnAKifbJA/4h1/T7ba+OV+xIUoSI5MbgaF3USidiDHPX0pY
@@ -1934,7 +1934,7 @@
 pub    B57BD58EF6D0A713
 sub    781D1F35916E0113
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFsZf3oBDADUgeJsq9asQLaUajkGON9KmxKBtJS+IbGa0jgvx37T4LDigKS/
 wh4axvdJ0mE31uXKitBVDkr5TptyxA0jojYwlt5YLXsotnskdHrIg35Q8xpMp72K
@@ -1968,7 +1968,7 @@
 pub    B5A9E81B565E89E0
 sub    28FA4026A9B24A91
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFIsmpIBEACzV3plLr6UEdvMiarCYzoK3W0Gzzd6BWtEuQdOsDkR/XCGOEkY
 hNQ9sB7QdA3ysFdRGf9IFcd7E4Y9dQABFXDlLEDGewPdZ1ahMTz9kK5k6R/1mxeu
@@ -2010,7 +2010,7 @@
 pub    B7C3B43D18EAA8B7
 sub    02A4A6FB70018AD9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQQNBFT3aMQBIACl/07e2aAdqLGTocp3J694BSGxjH1M4T8BevXH0UTRTXbge0l2
 3IONp63KF1tmHg0skzUu/1Ybau6Zw7k+jRFN+9VmslRprk4fjHjgxmT5U8p1ualk
@@ -2084,7 +2084,7 @@
 pub    BAC30622339994C4
 sub    FC9BDC25FB378008
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFlMExYBCACmdTDSXPwSJeYbfYvHoDl5C7vx/0+LOTunDGJN38pNQHYQAZnv
 Gyoc9ZmChrhLoim7z4ILqmNo8eegknepQ3dGdUij4NVIhR+m+8irayTbsNHvo3UG
@@ -2110,7 +2110,7 @@
 pub    BB2914C1FA0811C3
 sub    7AEAF265B448E2F3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBFHwyNYRBACkCXpipiMx0lCEccXXzv0bE7LHHbcQYtb1vT/o9WXYoP8JMChJ
 cvuAe8Tvg+s7EUjKHJRhu7I7kie+IJ2wtH5uVARkYxoP2OslYN6MSXa/bmwU8fwQ
@@ -2142,7 +2142,7 @@
 
 sub    C9F04E6E2DC4F7F8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFKneXIBCACtnX3ZQmPujf6ocvdnhsBheze71DSl34TfebyW2Qt+g9NhMxo4
 DaJy+iFNnsaMwLZRr6k/qf+ISE3A4opWAQlbk+Wb5s6DPPA2cHH6W4GdkxtuJzqt
@@ -2171,7 +2171,7 @@
 
 sub    E05A9780475FAB55
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFFGZXQBCADeZK9xuCrDwJ7v37y8RITlchzBfJEWv7cSbrSIBlFNAsUUoshW
 Y8U6xYKe0GdiLVta2F8bzs0Si4LcDeglQNi9Fxvh3/jfs0MEJUfSeZ4z1Mn5WY35
@@ -2199,7 +2199,7 @@
 pub    BEABCFBEE059E4E5
 sub    6579F3D193AD0019
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFOv78cBCACj4w72ksYDdLAY3GzwpRa1fo6S4aF7r96PitlETY83ct7AVF7j
 XaBGk5ylNAZXan3vlsSAKtxlI7skZOE5iKjqDo7SUfohs1WXdmL765mUNsSmkbG+
@@ -2227,7 +2227,7 @@
 
 sub    4BE257B370130000
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFv1EEwBDAC61jyEM99KH18hI3zlfuqvGoNjTLIh0wge5vXAH8VxMR0ndOID
 HYSBT2+L6OeiqKlyhCgF1km48F/dMzyJdTASkNO1Ni+B2Ric1sBxjsSPufkjl4en
@@ -2264,7 +2264,7 @@
 
 sub    C163B490C5CDC967
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQQNBFT3VuYBIADPQxdM6fJajMSyeiKbfpSjllBkGA16DE9IFJ76B6281k8sfya2
 k6UOAKNIprxY3JCRulbnkn3BcdbY1vZDhaf/fbdkvJ+o/XVzrxojq1jS3tvSq95L
@@ -2340,7 +2340,7 @@
 pub    BF984B4145EA13F7
 sub    84761D363E7B0FC4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF7rgogBCADU9OwoEFdIgN5U0JU5pI7s3T1T1LeDMzAQ8l2Hq4jFrhnrjcEA
 ieDSut1YIv5NTBoZv4CrklaKvvQNUXPvKrFImA4OKhBodKV3wsq2efCATDGa1JAw
@@ -2366,7 +2366,7 @@
 pub    BFFC9B54721244AD
 sub    788E173C196BC673
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFRRGVMBDADAQcmG+x0mHZwJ3uKgODjUZXkGRkuz7aP/qRmuQVn93tl8DmA1
 lgvXndvChUjzYt4DJnQhRsapAXEmP5/YYIkWOzuk9EpXGtqUieocylvNXP9eDF9y
@@ -2402,7 +2402,7 @@
 
 sub    3F078B16810B4EA4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGRmS+sBEADKHnDWmf5NP1/WPGmBLTEDv/mSGZx7jpfjbaEcCFH3hiGbspbK
 3wgGE1OzFf6JRBurs8GS0gD4aXoQFz8saVASPHlKK/LYc7f6vYAAWj6Tlm1j2qwe
@@ -2448,7 +2448,7 @@
 
 sub    606CC6C4533E81A2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGAic/4BDACtIv4a32pL+84jJNhJ1yb6GFgoWknJSJ6IELIL0Z7m+FYsymRs
 lTJ/QwBgjZlgS3HS7IBhEl5o+kEt2/U5lPkz/krP8By8EvRv18PpfBzmXNT8rGqc
@@ -2485,7 +2485,7 @@
 
 sub    97149CA7141687A7
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFwVgzkBEADF3gGO9iBXW9g7+yRjwTKuadaSW/32gDyREjKNSa7NA0HSCtnU
 dKapw6AaCFpznhfjPQL+bZX/YJUdrIXrSJ9iL//2Ay/JET7UhYBsHxaMm8VURpIK
@@ -2529,7 +2529,7 @@
 pub    C3BAB45F4AF71FAB
 sub    34FEB51E33761BEA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFkeN88BCAC4rvR3Dc6nDYhbXUC5IQ6SJWvV98+tvZ117J/VD07el7dicryY
 H3OAWl62iLjHJFP/+AEra1plpiWbPioDlzjOWC2AJjUCtqVLHdyVbY0Gv3sSRZXJ
@@ -2555,7 +2555,7 @@
 pub    C4C8CB73B1435348
 sub    EA2A558279B36E6B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFSwGboBEADoHgtdw+OVEAIF1SiRju8QDuhePZbpSgRLrt25AmowHJhOQUI1
 EP7+RWoCWW9gWAGas5mGDBxhPw8NgFv1nMUWFAsj0rkViuRD4qpJbChvlqw7YkOq
@@ -2597,7 +2597,7 @@
 pub    C51E6CBC7FF46F0B
 sub    4006CBA6D352F1FC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFbgSbABCADGGENSy3oWLjW7zfYMSsR0pm3l3eMA7ptyU5C0U/MoIYjbXwyX
 XtlGwKnNgngATh1SMpX4WDbD8tn6vdeP4uHQsDb40t0XN7/HISFcLhV5pCgz2wNR
@@ -2623,7 +2623,7 @@
 pub    C71FB765CD9DE313
 sub    DFF2D25E2CD6139E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGOV0eQBEADTe/ljLAoBp+z84NkWHDBqbBmEsBxcGa0VDQxGsaMMi2f6wkO2
 VDkRFNzNQbmw5xFqLisZ9ywzuVc9xmZ6qoMWLJaYs9RdsJSgD9+4hL5IkmjClxc9
@@ -2667,7 +2667,7 @@
 
 sub    29E792953D515FC5
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF8pVB0BDADcwRGpJUDe8eVSlJ0yPQl/CyeYc0RWq2f1seUMQO0xFW1xPIeL
 IE68D9VdgarA88qDLYesfBqzn57/r/ztj2aLEKt8IRunJzd0w0G2rrgSCZQ8RmzL
@@ -2705,7 +2705,7 @@
 
 sub    96123FA2B8E17FF9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGU//oQBDADDJ42aKLuJyYL2ZfG0ob/XxdYRZhP7/OO64Jf4WJtX7vgoVUif
 iSytAikRT707EshXRMtw8tx3H/jM2O7o/gJl592IQ2gYppMh4boNO7lc7dd9F6gv
@@ -2742,7 +2742,7 @@
 
 sub    7679164AA2590985
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBErg1IARBACVbmwMwp4p0ldolUYSkGl7XFJHwtEWmuikGcM4lp72h/YhAXpf
 RVsKE3aCy6HSTt7KJrcUuOL8BB67riZXLOIZtA9kDyC+0EUbnW2EbVfJXskPLP5X
@@ -2787,7 +2787,7 @@
 
 sub    64863FF4D1BF1809
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEdUhrIRBADCU9cuKc92CWQlZxwtRuSIV/36Qmj264YD+Lix+r1Qe1PqRr1I
 /MObOo83ulorWigSkx1k81Mnr56NwmIeo2bMhjmgRgf7EG6XEbKdRKfJcJRR1lDV
@@ -2820,7 +2820,7 @@
 
 sub    AFF3E378166B1F0F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFeWvEwBCAC7oSQ7XqcGDc6YL4KAGvDVZYigcJmv0y5hWT4wv9ABP4Jhzr1H
 NDmmGyWzhzTeMxwuZnc9vhxCQRwyxj3gGI5lYPEARswbi2fWk//78/3Wk+YMHJw3
@@ -2849,7 +2849,7 @@
 
 sub    B2D8461AB7A7DF27
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEYo/OhhYJKwYBBAHaRw8BAQdAStj5losyChV0W0clNh6HwDgaGgmypqqVICtN
 K+Vy0oy0JlBpZXJyZSBZdmVzIFJpY2F1IDxweS5yaWNhdUBnbWFpbC5jb20+uDgE
@@ -2863,7 +2863,7 @@
 pub    CB43338E060CF9FA
 sub    C59D5D06CF8D0E01
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBE0NT+kBEAD1hzO+dXStXYJj8M6FBn9fxw+grddjM9rqaEgJ2omSdpZZOPBs
 DRor7v0Rm23Ec17y/7Dd6oR1CvyAeQwhJvNBaAW4LQmUcvvqep4hfkWDhlRvh/QS
@@ -2907,7 +2907,7 @@
 
 sub    BE04F93C75A3B493
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFRIQyEBCADYOc8Y4bOkLGh5NFwQ1JJwGzPY/mV9kndWy2tudEs89Poo4cQD
 A/wndJqO2PrdvDvt+kxKQGra0RzUNW3Te5gaePo7+3H297BAWar8+KiX8RRu3uB1
@@ -2934,7 +2934,7 @@
 pub    CE8B1D1D2530EDC5
 sub    7ECBD740FF06AEB5
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFuX5CkBEADkTgn4nzuq0lWR+7kFGYLKvmPLjes4j2nmygIafUjVbNmD70gY
 DPpbSP02HxgicM6xSSqzZuBVxpbcffqjMPXf8LkVX4iWKZtyzLpf34yaojigU3qF
@@ -2986,7 +2986,7 @@
 
 pub    CF9F3090CE4CB752
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE7E4m4BCADCkqre+MJRRn+yBa8PqDHFIpfxOk8lQeueZTrU0Hw14wMkkOW6
 XFBb4hDeezStNNP6s2TS7bf5YRXZwqOwwgg33WYVVH4jPldaP1m+Z3GtYSLKEjTl
@@ -3002,7 +3002,7 @@
 
 sub    57CE36BB68F1BC57
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEYYx3eRYJKwYBBAHaRw8BAQdAV7zh1T+xL7mD2O63rTIvRfQ9kwL2Gvq/Q6PD
 9apCe2K0LkpldEJyYWlucyBDb21wb3NlIFRlYW0gPGNvbXBvc2VAamV0YnJhaW5z
@@ -3018,7 +3018,7 @@
 
 sub    5199F3DAE89C332D
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGCtdhoBDADdopjDt4eUNEqLJSw1ZICSR0oq09SOVtJSaSYdF8UiXjBfL1Ds
 fhTDqSv5pT2a2gLj0OU3tFhWHvINLaKKCjQnHVcFXi2LTxt+XBOjRYkFjHVisbaZ
@@ -3053,7 +3053,7 @@
 pub    D364ABAA39A47320
 sub    3F606403DCA455C8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr
 c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD
@@ -3097,7 +3097,7 @@
 
 sub    D826E3935EE9DC71
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGETEF0BEADoVhSwI5d3PZTca1W/1HvIf5UiTJrSlZby9xRdSbfJ0dj7V0QG
 aY1tsOcLLuIkj+/wDJuATokYx6IiGnntorQcLg3b0XMoPqzTVDl4lnKcNIsh/kxD
@@ -3140,7 +3140,7 @@
 pub    D57506CD188FD842
 sub    63F72A7A8658D653
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFj2NXwBCADPJcGfWz4Zsfa/UEUF6a4aAIjqCy+rNmLf9Vs3HD6B5p1r7VkC
 e0HhxrfbkDkQu6aEmAwV6GwYiwWBf/LQYNdKm1FYZFhKLhyuTPiirFqIouEFqiK2
@@ -3174,7 +3174,7 @@
 
 sub    9D49CFE20A7A3EE7
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF7rvIMBEACkH8bOlnIXAH9nQYFcihkcJvv73pw66YMz4aMPJe5PzaJU6kkV
 2lbEgEOnfoFLqgnJVY/KsPf00BXaP5uMzqNfJTK+HO9I7m3BTqmjLBgUegQig4K/
@@ -3219,7 +3219,7 @@
 
 sub    A23FC45C6F9E2F57
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF0uFrIBDADbJkwrWs0qPrv4bNmPZMWHcryANAwodvFF4f51Z6S3pBkuBxx0
 vW8ZKC9/scJiAzSqJRf4im70GPNE3MZjNyfuRdaedXw2rFc4Ip7lBsCtklYmTWmC
@@ -3256,7 +3256,7 @@
 
 sub    B4C70893B62BABE8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFMvQKsBEAC3/wuVMv4ia132SA1Y/KnuZYkSNDaRH/Ie1WTAX9X0KrWA5fx2
 WmzKfaLNyBHU5aI0BjoE9DW3zkZcLEcL/cxRzoXoavUGRhRsaHbj4PhQkEqV35L1
@@ -3357,7 +3357,7 @@
 pub    D945E643368FEF62
 sub    A8D88140C35897AD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFzJyTIBDADO8siKg1NQb8jNPo2DPC5CpPwYDPUjlX7Nq/FMBYeY51JlxKLD
 jmH/R5u6LuY0v7gSodrJqE0FUjz8LgN9+Yp1f1szqxeYHLsAVahO4cafG/sITYvr
@@ -3393,7 +3393,7 @@
 
 sub    9121AD263441EEDD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFrjUQUBDADTMQL/4d9EyVhsO4XBH9wbGWxcEJvsu/HvppN5fY8hpMV0+Cr9
 wjAeJ7d9zdFJVB8vPLN7bb5dm6SNyK3KiOugqVgZrQ+ZPTvCCgFbFyEXuZwDiOa1
@@ -3431,7 +3431,7 @@
 
 sub    66A2CBDE49E8A25D
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBGAwdRsBCADCXfWdHhywp8Rcgt834W/Z3MFEAxYdxjAJOTQhc/In1SJfIqi/
 xD7OKHA2fbwzRnS/UmXkmElTK7JI3/1gWRm8kEaaHTnlI63Z9MZV0DHMpJMgvpFM
@@ -3460,7 +3460,7 @@
 
 sub    50C6CC55C6F24FB1
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF8tnmMBCADROe7j1ZvgiMgfsQKqCSuSqgMkfMT2DEXwZKdHqkj0gfx8MPQg
 OP1pmMgpIwIXKr5kZ9KMGiGULNnS+WU2SNqjyKeq3MlnSYW5Di52MoAD7W4cHmry
@@ -3489,7 +3489,7 @@
 
 sub    A947A3FCB1697B4F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF7H/6gBCACbEuIbxWAfHEYViPqdpwxDYauxsYwk6FgA9sSO1nS95KRwx+Cs
 X6F8nRGnfLtbo6Ffcp6r58fNi9RvY7ueRGiL0kQd6c5GYx6dH1b91Q1qrdVOeEdj
@@ -3518,7 +3518,7 @@
 
 sub    E8D0C72FC5A02B28
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGAlt80BEACftpFzUCGm2u5sV4UgAysobdqZywkUKP147toek4ULQRYpADig
 AI9J3BCmHbcApLek1U7vj8geB6T7V0c4ELLFPQ+4lQlCPC8Siv5c2gDaZvoMzTlw
@@ -3561,7 +3561,7 @@
 pub    DEE12B9896F97E34
 sub    9A716F957BC42546
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFAxQKwBCADJGPv6pmFEq0SDwAKESEgCdnXycbR0bNXpNa/3VGboNto1xKgd
 AQ/sI5x+CmN0hpUjklEwff6QIt3MlofEMkAzSfRmTobhJTK9W7r4+p5DuhJpi5Wz
@@ -3593,7 +3593,7 @@
 pub    E0130A3ED5A2079E
 sub    0AE7BBD7FEE66E0C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFlMSXMBCADcgN0/57D/gU5cDobPiRuDT6qAxb/NWhQiqwAocKd274r4gPJm
 RbffUEZEgKhjH6l0CQfilC4R4x2QtU9sNC9kB/D6zumoS1uI0Hmx1pC4UseUy55r
@@ -3621,7 +3621,7 @@
 
 sub    F3DBCE882C3A01AA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFsNoY0BCADIvRrJEX3k7UeuT6zt+F4++xH+5Qo7QzdicjFhhyb22PLPyIsI
 Ema+T4QqiPDegUv8yKKTTBmHNw/vSUHTPX9ZUpglckopuOgdfnuQjTKEOEzrN7V/
@@ -3650,7 +3650,7 @@
 
 sub    5A34A5E06B936F93
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFF/4bYBCADTeOLZiVGNbjlPrwG7UcMl+yXmEqpf9dB1A9cuicH3PWXj0WOb
 LSzHjzoRvRekEqSUmgoveey1lPuA2qjOUkXY6Kiyx+oLiG0/ObJHUQW2O+tjSQ0R
@@ -3680,7 +3680,7 @@
 sub    60EB70DDAAC2EC21
 sub    3D5839A2262CBBFB
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF/RX/MBDADSqelDQKobURExWUKALq86yTPMxMasxmDlccKFpk5xjWrryL7z
 qg4Fnb7IK5fKDtcnTANtOv2hlIli1h131+SmjJdD3qhfly7QoszOpr5izDS+FOCj
@@ -3761,7 +3761,7 @@
 
 sub    4697DFC8F2696A57
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEzdTPIBEADki1HMFzssqhU2l3jJr0zNE/gyPohjzI5ugw1dNWUd/ht6oUnm
 2StYcsRnFHlY7aIp56v6cZtAKYDZTlEArIurH5xyQXQ3PLfxQZPVS6HDUghaa0rJ
@@ -3806,7 +3806,7 @@
 
 sub    52410ED7B05AD2E9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGKRNiwBDAC56nNMaU1QEHCpOnvOHK1rjDKGDolxSyx9rgoTTWpaI9y7JbUT
 iajEkzrtTsqjrabCltAY6QGQUz/TdS9MikCPUZM+l9EYKoBACDeKrYMcApHj4eVw
@@ -3842,7 +3842,7 @@
 uid    Rolf Lear (JDOM) (Used to sign JDOM Packages) <[email protected]>
 
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFCPD00BCAC4tY8wMQTsCKyII/mMkUDAkXA2cLM47fY1Wn+iohtgtalUdA0v
 AhGvTdFU6/St35rOKNoyLC7Sy30FBYpAEfMB/x9j/CaQtdtGhaQU0hCvtWGhhS3J
@@ -3859,7 +3859,7 @@
 
 sub    BB09D73166EEF1AD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFEqVnEBEADZhnnAV62dwYvq5CxvEO9N7m7vrYMosc8PCEafxJqrDMbWWfv2
 tD3EaHAERt/UFVEo2U5FV1hELUvFISPhh/DpOWYuc7pwA75do7ul6dhwgi5FcyjR
@@ -3913,7 +3913,7 @@
 pub    ECDFEA3CB4493B94
 sub    3BD211F725778C36
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBERFeVERBACjfASThn15ynIICr0Gu8quGCl2rSSRar8TsjrbiwYB2MTW35Rg
 NjLU6MN5Nq4d5G8D5aMeoyGODstIHH8zA52sDGeHOMKfDaAraL+lGzElbpmaqP2s
@@ -3943,7 +3943,7 @@
 pub    EE92349AD86DE446
 sub    E68665C8F91BDE69
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBGO91akBCADDDpIrW/IohUSJNDu9VOUlnfEOm5VS49uqM0uucLi0BeAhy1Fo
 P6Yg1cJkcK66DtnUoTM/JJLyDzJRlKnniLrYCkw8ScvtPdA5cQKJTY5ecn+9ouR2
@@ -3969,7 +3969,7 @@
 pub    EE9E7DC9D92FC896
 sub    3B7272A25F20140F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE/oyDcBCACgYsHtmWmtUzqyr/JN+orfJaTl2363qiS+NJ1lt2CNxUWOqldc
 VcIGyjmzokxTRpGdCFmT1Lh/hzZhcDPLjrtxf+f6njIibt80OiEbX39gjwZRIikd
@@ -3997,7 +3997,7 @@
 
 sub    AE7B5A78012824FE
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGFUnmoBDADItKvcmnwP6xsF7EnS+gKxUBU+M+x1sdzLJGyOL4laakwgUx3m
 RhKwDfT6tIQjTAVpHpORa2LNYikoYYodIHshTuwN9Gba/pybeRdazWguOv4pizTx
@@ -4035,7 +4035,7 @@
 
 sub    EF375EEBBDEFD775
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFS2J+0BCADZI8RYk32YeO9gnEkY9RN+4dKb+H1AR4v+IGxmy0UYy+O8bo4m
 YzkQHTlPpEPGe10/quKk1embDifEfNa9mwcSJl+XUPFlTrSA97SR31mdyK/Ua146
@@ -4064,7 +4064,7 @@
 
 sub    1C9F436B883DCCF6
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGAhOxEBEADdB5Jy2sSOndOMCTyk8IFIJYPogjXtN7CnyIlqr4jEB5G87TJf
 m7OxB95aIVS1vSA5ghCm88N1mKtW6jyYjgLFQbbyD9/X3ShVZjh8B2R4atL93SSK
@@ -4108,7 +4108,7 @@
 pub    F406F31BC1468EBA
 sub    4BB1ED965FF68B71
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFmnALcBCAD1KazT9eswNXzML5+M72qhdIX4VlJrrOzeiQtTW9vbXj7DZUnw
 U8m2bNmKHtpnyXQ3Vl7FE/e8CKGUVKmB854VJGDSyjToeAnt8A0Lg4smaSfgbEim
@@ -4136,7 +4136,7 @@
 
 sub    6064B04A9DC688E0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEtsF2oRBACcai1CJgjBfgteTh61OuTg4dxFwvLSxXy8uM1ouJw5sMx+OKR9
 Uq6pAZ1+NAUckUrha9J6qhQ+WQtaO5PI1Cz2f9rY+FBRx3O+jeTaCgGxM8mGUM5e
@@ -4169,7 +4169,7 @@
 pub    F6D4A1D411E9D1AE
 sub    B5CB27F94F97173B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE89LqsBCAC/C7QToaRF8eZgGOxcvp9aG+mFFCMjaRAb4Mh59OYdmUb6ZjfO
 9388HPebGbPNR8SHYs0dBIuWY4ZJ7oUTYPswasL8vB0iPFdyHhvkCca+yk0b8ZBM
@@ -4195,7 +4195,7 @@
 pub    F800DD0933ECF7F7
 sub    592C39141EB02A78
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQMuBEvQhhQRCADQ2MH2FpQD7pbCTDJ4uvPSeaOz0IUhkX9bK4sKvIISx8MbHhR4
 k4sXi+vVkLngWCMUV4nB4WcCibk2S184SzL0TstTDrudxe4eJFVbmZw0GrgASugQ
@@ -4236,7 +4236,7 @@
 
 sub    012F07EDD27CB2E2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEZI8otBYJKwYBBAHaRw8BAQdAL5VNS8O2NJbsTZaMphHGdxsSaLUj0tZLI6+R
 /pve51q0HlNlYW4gTGVhcnkgPHN0bGVhcnlAZ21haWwuY29tPrg4BGSPKLQSCisG
@@ -4252,7 +4252,7 @@
 
 sub    5F68B9B2F1725F16
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFFCLwoBCADxtcGi0nfolr1kGWe3jQ7n18roJFwBs4Q52nx0h4+a8ZGr7/1E
 1brakrz3t/cTSZIrhfru8kirP8cJtXBxpd/nCeRrB/4ZtXPUJiGwKx6sVGr0ix6U
@@ -4279,7 +4279,7 @@
 pub    012579464D01C06A
 sub    CB6D56B72FDDF8AA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFgnlA8BCACVtx3oLXcanfvwtMRwal6pLQ8IVMG9+fr4xGdbSHXCRNbosDa5
 agU7WeQMPhusSxJGaA3w7NOdjAwD/LeHADhDPeI6llJg1Fb3EyqH0NZaODKU/Or/
@@ -4307,7 +4307,7 @@
 
 sub    C753427AB202DB9B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFBdqooBEADuV8IhDi4Xvs1oYAnTXQz9MW+bU5uaxQyQcFzUwxacSdgAv+pj
 dZRFli8qs31HsddRmW6qCkCua/QXNQWCOcylcwAKmumct1Z/ZumYTRVGbsagneBa
@@ -4350,7 +4350,7 @@
 pub    02216ED811210DAA
 sub    8C40458A5F28CF7B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGADx6IBDADoHin1LGQ8dhnlhfNCBZ3IyXS2NpR1VjmYtHSlh1hGsPcmHuwo
 1mLA6JzXF7NuK3Y52pbTr6vz9bAap8Ysjq/3UJeiDbf7FvmO5xAEVUhrpc7AEY7G
@@ -4386,7 +4386,7 @@
 
 sub    7CD1B9BD808646B7
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFqzjCgBEADfFggdskGls5KqMnhvePTtS4Bn/2t9Rl+Wg3ylXgy4IFd4bnI2
 9f82dVM/nobNqAnhOp0wEaAcw+57xBx3rjjKQbrMzUweWeL3uJdTwtPWoyzzsUP0
@@ -4431,7 +4431,7 @@
 sub    0181B45EA58677BC
 sub    944EC8D1A08CF77A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEYiljShYJKwYBBAHaRw8BAQdA4ativA3OtR15B4YnoRwpm9rRgHdd0A0lzJ4u
 6q7gsMO4MwRiKWQYFgkrBgEEAdpHDwEBB0A8fHRuwUbuvdnDexkJzQZVUg+nFrcA
@@ -4456,7 +4456,7 @@
 
 sub    F2E4DE8FA750E060
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEmoKU8RBADEN0Q6AuEWEeddjARAzNXcjEx1WfTbLxW5abiiy7zLEht63mhF
 kBlbyxEIRnHCSrPLUqY5ROWdyey8MJw+bsQn005RZmSvq2rniXz3MpcyAcYPVPWx
@@ -4489,7 +4489,7 @@
 
 sub    9757C89E39C828B7
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEZUJpBRYJKwYBBAHaRw8BAQdAMUi1X0odyTiUuXIgDEYZZ4Pf4FQifp2UgYln
 s/XkBjO0KFViZXIgT3BlbiBTb3VyY2UgUHJvZ3JhbSA8b3Nwb0B1YmVyLmNvbT64
@@ -4503,7 +4503,7 @@
 pub    049FE94F2D5DAD9D
 sub    953E02E4F573B46F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFxlMc4BEADbWFmOqHBqUUAcO9nPRSqtrmIdjBCzqsRosPk80n3Nd+jWc44T
 /O5TObVbn4NxCmbLxklWpIU7eTEo3u5LnwhkgcsxMykWYdq6DqyzENO9PeE/McrN
@@ -4559,7 +4559,7 @@
 
 sub    DECB4AA7ECD68C0E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEoo3BYRBACXE2oGRA58Ml6s+kvfk6n/AJ+5OFeRT/Xelco/cpdxOVF5LkRk
 yd+vR2+F9ldBlH7CSTCmrdZIN3M3zrcWndrk/OQkCxNWVnE/a1li7L3G9nYr011k
@@ -4586,7 +4586,7 @@
 sub    6A0975F8B1127B83
 sub    3FF44D37464BBB7E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFzy4ngBDAC4mz6ELMWjfJ8GZtolq3E96T7qjfp4J9FxGVxdbJxkEDnn6MTg
 V8zhD7yeSZcUSvwzPiDlB/b4RYnh+5LjzKHTsrtr9ja0SupuCkVGkMGWeHhpIGV9
@@ -4667,7 +4667,7 @@
 
 sub    11F4CE313A637CC1
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF3HgdMBCAC3ET5ipFXdZ9GGMbtsCQ3HGT40saajsNDOdov2nMJxzKkVe3wk
 sN3bpgbsqBU9ykVkIhX8zV5+v8DOBzkV0pJ2eLjFa9jBPvNjV+KoK2BAI5pzNzYg
@@ -4696,7 +4696,7 @@
 
 sub    8118B3BCDB1A5000
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFu1EwUBEADAXapH49L1Lwt28iK737X/+4bRDE+lkMxehnUZ7QJs5zkFz5Sh
 9K2rQO0PpvoMSdadGplFyhKdDP/iEUpzxTTbqMs5UjbJr0MoFfE957Vz59mNf9WY
@@ -4753,7 +4753,7 @@
 
 sub    5FB8CE6F93DDEB23
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGAjI8sBEACtiX+/sDZDNo9M42xWgMDEUUBGkObE0opLPe9N15OHQt8Ve2yJ
 VW5yW0X0hcBIkaxAG7F/NpjVRN1bLGM3J3URR+XD+Ubq2KJkKW/39RHcP0m60tL2
@@ -4798,7 +4798,7 @@
 
 sub    A9E4161147556D82
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF+EGtgBCAC/KXNQAl1rz3VBbqm6ssjzR+5Su1QWHI7oYDS+YHCLOaqfE3jO
 zQd+8iNgniVNtX2n7bt1hido5B94VmaqD+zjjSu2UV/eZoYhCOQ5NgvxIr7WZe9t
@@ -4825,7 +4825,7 @@
 pub    0D3B328562A119A7
 sub    C45D01093DCFC371
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBE4rG7gBEADo5n849j3hlKrvFzt6y65grIxTlbLDXEB7+6sw0Xwuh4NrK/Zg
 0+eF0vvCCZrl3lHE2duD2ng9ZXz8EvUSNfwKMQz+cwF0klhP92u6mykKJ3/DZ4yo
@@ -4902,7 +4902,7 @@
 pub    0DA8A5EC02D11EAD
 sub    71499A87DC1FF84B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBE3LMfMRBAD90h69D8yyPWaSoAyh2mOOOZ/XH0isuBpDZCWptemlMHgImqdQ
 2sXLXYT1bJKmSaMw+yKjp8J/NYk69EbmSK1C2nypLQtWhUmXXd3XVYw6hrG/dGvi
@@ -4932,7 +4932,7 @@
 pub    0E91C2DE43B72BB1
 sub    83552A552A0D431C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFBIm/wBCACgqvegptBhfKbyBXZiW+7XchIJCOpwq0/9QgSehKMwELbUKqNM
 sIVrywANqYn32S9hNRvBiKGm/KY7VwN9p1Cr6Ey3XuGSbRo/xN6tqfV/rV5YClL5
@@ -4958,7 +4958,7 @@
 pub    0F9FE62F88E938D8
 sub    BF6D15D3F1BF7BCF
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGGNmd8BDADSpbdIfqzkUNAeYlP0nUw/HFU/v+/aydtjUioAi/KxYt2FOMi6
 gk1LOJzHBubv8bF79mlN6sXrnq2lV/MuqvN9DrTAQ4u4Dh0pgbLK6jbxDWPGrYIo
@@ -4992,7 +4992,7 @@
 pub    10066A9707090CF9
 sub    2B9F5DBAEAB53FE8
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFGKp5ABCADTyMhDq+7Kcv2wXOpOmZgp++JNO1erNUjVqFX7n9bT77DciEML
 LNxWVF1tkNqgkn0ughZTXK5EGdjUfZaJaDDfG4BIsox/ng4nDvIp4CtXqHbWqrlc
@@ -5024,7 +5024,7 @@
 pub    102E05D8DA6C286D
 sub    7680B2343D1CF013
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFOZyw0BCADj6eDnIjaug0RJQCi/HLw5jJ2kORPaegxFuE5IhpN9pZCPASax
 aTROfUSnys7cbxZxh3Sri3spQ0j+ejod0MhVX9ajTg508YAJUaCBbM7CGZJZtVFL
@@ -5051,7 +5051,7 @@
 uid    Thai Duong <[email protected]>
 
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFrY3D4BDADSiDX16IC+236IeUiqi7Nbt2wlsBS0zqqaXi43QwXwcf7aYn4+
 qrn+4JvsyMrDgkRgOElz134B1i5OSzP/32w2JCnj90XUjO5N1KD0QqoSops7NLhZ
@@ -5070,7 +5070,7 @@
 
 sub    DEDF3A7EB400D53A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFnu01oBEADvITy7wT3dfEh6GKbW58giiB+JM3ikYNsK6LWaOa9Pi4/ZPpBT
 ZxNfY90xp7U8lklmiOZ80XzXfKdnQySdW0GlGkRnzL8c3FayN97TlmMeRouRo64q
@@ -5115,7 +5115,7 @@
 
 sub    0888B86856F9D71A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF2hcBkBCAC2H5WcFoeByKBUAjRDjmP+5P6FRsZjLe6c1wy7G1ha6/EQUVK4
 gZUZYE9W7l/4QKHvAu4PvFWdD+5FXGZuB/2kw348CtabAlJTehm1QlPt5//ODkxB
@@ -5143,7 +5143,7 @@
 sub    F2EA967B5B8FD0FC
 sub    F860F86A8AA8521B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFolWewBCACurWoOCed1W8Ut0tmqkSTpaz1AvPrYvZxmNqSVbxGjd8S/Bpxm
 uypKQ/KIF88a8QbePYa6e/I9g8HiuA2Bg91T9khc1mztXvutkiFNaldecg2rFHZK
@@ -5201,7 +5201,7 @@
 
 sub    8B794AD8CE1926C6
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF8LXXYBCACuy3HnrpWl7boi98G4wG1ZrhBiYImyfQd1M+dvH3GF3Vqt2NYv
 Nv8vryhUkMi8uu233KrYx2/kVK0RomMYWtUrSbQIdykytd0/VsoEk82ysN21ld9P
@@ -5230,7 +5230,7 @@
 
 sub    0190A8A50D88C2C9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF1wCjUBDADAQDQrGd1ul3QLVj5zbl72zNWVNsRVF98JLSXYMmxsY/A0YNzT
 B8LR58QCNF/xcjDyFt6+9jDEVjkKnJTHduzxddF/cQ9pw+0BOOwyfIkC2ryHzGUH
@@ -5265,7 +5265,7 @@
 pub    15C71C0A4E0B8EDD
 sub    891E4C2D471515FE
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFcyNOoBEACj0zTN3GkRNAY3jihHZdGvi70i4R8mUfcQUwWGRsGGlzSwyJfe
 20qNOHqwHaxVCAIp4e5paNf9cEKepOv5IqMkmaRdiC2W+BHDxcJgBot/IrC81ube
@@ -5314,7 +5314,7 @@
 sub    D101F7899D41F3C3
 sub    E074D16EB6FF4DE3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFf0j5oBEADS6cItqCbf4lOLICohq2aHqM5I1jsz3DC4ddIU5ONbKXP1t0wk
 FEUPRzd6m80cTo7Q02Bw7enh4J6HvM5XVBSSGKENP6XAsiOZnY9nkXlcQAPFRnCn
@@ -5865,7 +5865,7 @@
 
 sub    EFE8086F9E93774E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFPU8TIBCADGNvExYTJpVuNGCF9NuWw+IkitjAD7WzF7QkvFCSw9VftzgTUZ
 3PYrThRiaDdmHQAke4Sp+nYyAJ7iUcQqg/5/ONiMdzXEv5Kwy5WJN8+o2aXSunIT
@@ -5900,7 +5900,7 @@
 
 sub    5F6BA89D4B0869B9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF3TQCcBDAD177B+Btl8XBEkBQ5jFSezFrpEl4arwCEa7htCp6T3h55HvYwz
 P7Y9zWYXfhAC8XJlPQJYpqaQiiYtdlmOrOS4wbp5Lr+z/0XpFlJFzdKglxKYcdfP
@@ -5935,7 +5935,7 @@
 pub    1861C322C56014B2
 sub    9A347756830C4541
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEGVK0ERBADwhGhmOMvSgvGaqHW3ial0NS80ZXyE1EeNL6ke/WrXHB4dT6if
 inoAuUgRz3v9Na4rjSQ8YVFjn3NaZq1i8RM2KJOUU8ZkJ2AsrH6fqStjofLTd5ng
@@ -5967,7 +5967,7 @@
 
 sub    D068F0D7B6A63980
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFHNxM8BCADYmt+HKkEwu89KQbwV7XIbgwZSfWc7y1HvA2YJpJRXJQsU/Pzv
 BhsHnm9ZIScBLIlgE5OUnMNz8ktPDdsFg3j/L0HREXOAqkOFxWx2kANsRo2HmkM3
@@ -5996,7 +5996,7 @@
 
 sub    A3F393B5D034A0A3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBEzxj6sBCADGV4szLvjBwrAOKYWw3efASDI2yo5Aq4oevm9cUB4G9G/D/fuR
 XhodLaG2smZLd8sNafWTSbPHswsZtMAjHGzka9Uj4Ow0etl3+kTh0DE6Loezkj7s
@@ -6079,7 +6079,7 @@
 pub    1B2718089CE964B8
 sub    A182F48D9C2C0825
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQMuBE4CPoURCACWBMGV/j1pioJPWkD9K9NdeRvld8sBorFBZo99DF3mcJvrXo/t
 We7gmvcx2n/8P5lL27sYPuj6WSRgtVBlSMXllJm3NL3Hu/7XRILfJEKVeLLTdxc/
@@ -6123,7 +6123,7 @@
 sub    7999BEFBA1039E8B
 sub    A7E989B0634097AC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF3Ep5QBEADZfs6o1IpZbZ1qlBkoJ7oWL0vFCcdPUgF/PRFXWKlsuFHVVV/N
 oZF9SDiCJxfvsVXmI+IHTVMR2SszU2xDF2SlScRfZQwrLhBsDP9nv9N1eGIoA5Ny
@@ -6202,7 +6202,7 @@
 sub    C2148900BCD3C2AF
 sub    CFF46EE3C17E53E9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGBP58sBDADYRZmxLOkqrz0QZ/yESRpv7IeHGLqDE1a8QfFtFb14MJCLSAAS
 3nMD6Szi9mEjEqYdJURRcMjbUBhePgbhzGa3FYkjAB8lj6IKbu+ogCwVm1S8+caZ
@@ -6283,7 +6283,7 @@
 
 sub    B7D9C5C3EEC4A9A9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFkyw7oBCACtGFos6g11ycruiWMuXwrE4+XbU85+1jR99AN5PcKjgXo/J3T9
 XaZLjJ+oTWCVgEHu5PTxAftbkq9+lmDAUEWZ1Q8dKrnVgBLsFNn+G2pcvVschorz
@@ -6312,7 +6312,7 @@
 
 sub    8DC6F3D0ABDBD017
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFAJOeEBEACn8aGYTnhyLS9SNi+SAdRU+pMPiqxdpxDMZczVee50y3LiRnCX
 biWqZyhzuHZTccgV9IMYFwxD490BioH8M80escHrMh2C50FCFglVYsZQG93jYJJR
@@ -6368,7 +6368,7 @@
 
 sub    AD9CEBA0521B1945
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEry8yoBEADnhvT3m/zzzuiUKyAeIfnN9CeN0ilQx4P0kFMhyZchRR4Ekb41
 iKw7tDL9q+g7xSo3yUT9dKjDWJ3yhDpdAhp6d4y8GAuWqlOu8CQdEHJOKK0yxTzX
@@ -6413,7 +6413,7 @@
 
 sub    B4A1D8D630480593
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFI6WiwBEAD+kkswnsY8eaqvYkS+ZB5MJr7juWrv9Lw9OGsIXFlTvD1XK01c
 E8k4+uA2sOtaXQ5wTMdc5N3YzAXqFxplWuafQgEvhyTTq37M5YCxvtYEZy/EHQYT
@@ -6456,7 +6456,7 @@
 pub    218FA0F6A941A037
 sub    9FF24F51B06DCC19
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFjDR2UBCADDfzBacBg664xalF55ghVkhxxwLaTNUdvTJ9o0IH+cGTRj7EYr
 7QvLVa68PKigj0q7SVVwhPT7fzBLDosGjHef0UWap10ynqACBoAYSFocT9m3Www1
@@ -6484,7 +6484,7 @@
 
 sub    A98BD25BE464EA45
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFxmwqABEADNTTxqFiBcLLQwARbc0bmPUlxFl0A0Di9dTycUEjn0wTGS2xgF
 dFxWomZd8R4b/lVb9jHf0r+AEul7U7sBoKinjwk0EuPDAZK5PEy3P8ILcAulwQqW
@@ -6529,7 +6529,7 @@
 
 sub    D658968EFD5E9F85
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQQNBFMPOkYBIACdXZi+34dvl+8q0IGIjLzFP7JvUH8ail4vrf2zwliW/QZskB/7
 pFXCpV2/hX+0n+kJz0eqenl1l/+lT6p0MQ1TMCtiMccnX7WseQM+xSv4ug82nAwa
@@ -6607,7 +6607,7 @@
 
 sub    BFE9E301CD277BAF
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFpqN94BCACaAb8Afmng1QPu5k5uzLoA1FJnF6Wf31ZU1FzDxHFHLNUYSWN2
 Bg6k95QH5ruZ+Z/QOJSoIB+b3htDklyxd8m+G2KsMIqnQs0BaTN18hb3PFyMIknM
@@ -6636,7 +6636,7 @@
 
 sub    EDB3D937B0C94C3E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBDwczzwRBADyR8BVt1SUMHxjSG1AAekABO0YQHJG/XwEHYk7zPH3aU14/ocf
 g6M8gxZXumM2f3oCCkmOpnW6uKxqTclQX44GyaMDETcAU5/bjWenWNj4INDlTjFS
@@ -6668,7 +6668,7 @@
 
 sub    B4E75C15C3C701AE
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFPsGJIBCADOxQoTLxpZVRIbLaRfsHa2y/TEIGvxLP7TgqTwspZYnwBd0cOW
 OHAvF8yGfdk5gvkGTlQ/xchwu2Ix05FO2c+fBoOgIG1Gn2Q+PwheZklS7S+V+GFk
@@ -6694,7 +6694,7 @@
 
 pub    280D66A55F5316C5
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFOOGVgBCACiDwUZOc6943aBGUrxikkfUnsyZfHtF9jihYmA1pSgfsye+JxR
 oG9QWW9+3qx4L/d4ZEqBftTWpsjyrY7NyMaeXtJEjE0vhiWNehgXB1z4XTJ66zCX
@@ -6708,7 +6708,7 @@
 pub    29579F18FA8FD93B
 sub    9DF7F2349731D55B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFYFiMABCADYpblWssqGxbjTwsyroPh48BwdSKl59zbFKoEHDw87NeWq7fik
 h95RkbdeWsQSvduXWgQZsUDq9cLOkuS/ChAMkAAd3MPp1NMdFmAqS7BX5wU5s5I7
@@ -6736,7 +6736,7 @@
 
 sub    D95ECEC170500D9F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBFsHC7gBDADlkoJglNVbX9MShcAm6jvS5atCZwWT63gSasObXFxswsJQd1NK
 qryHNcj9tKBfLbSpMOoHeyyIKDdwdxN+6+N9Hi4hf0j1Ub6deJyI8ace8VERWaxF
@@ -6773,7 +6773,7 @@
 
 sub    74C249541619FF0B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQMuBGJIi4URCADFspeHyziASBuPXpLpikWjmC3D6VtTaDT17ogOyGLf6/sjsQUz
 0KS3PzWBuPoqRGRpTtZxJ5yr10apr8mJF9Po5LFkrtcexaiYmUWAZAik894OhKt1
@@ -6815,7 +6815,7 @@
 
 sub    673B436865B87E35
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGKVyb8BEAC+qG+3tDrZkCJlJwiU72OrX/R+cKQ8Jvp2lzwgJg2Sw/S0xXAz
 KqoxvfkcM/egEWbxUsbuYVVXlAuGwTJeg8QtiuqIVXyoEEmUoWIqjOsCcNDbQ8Of
@@ -6859,7 +6859,7 @@
 pub    2BE5D98F751F4136
 sub    C7FDDD147FA73F44
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBEwMV84BCAD0u42clJ3hghKlMGwFA8PPlPgSEZjyvs2dRCF+dKWBaPUnR88K
 kGfWB66jX6PBtHzeiVRa078lL002S1lSth2A+s1UfYGS5wVbE938wO6PCMwgoXJ6
@@ -6885,7 +6885,7 @@
 pub    2C7B12F2A511E325
 sub    10DA72CD7FBFA159
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE+ZO+EBCAC3fZOOuYKthr0GcUge0PH2bh18sbM9XUmPKQz/W15l1NA/2ARS
 2gUXM0R+SunMlun9KsqjnojJ2ObVPvbm1Hg/66JSRgR3JWfIpSlJxLicpfu8rCfN
@@ -6911,7 +6911,7 @@
 pub    2D0E1FB8FE4B68B4
 sub    FCF74AFDF5947ABA
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFYVT4EBEACqm1qKc6Twp2Iw0tjUqr3hrZ7mjZMWg5MemH9ZiQ9iVIqV4Lee
 KmgjVWk5jnTslriymDilDIMk0YaT67JokhgSdqMIavI29tJ6quOp0K7Rj/rNBc6p
@@ -6953,7 +6953,7 @@
 pub    2E2010F8A7FF4A41
 sub    E4D15F24364C7906
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEnOgPURBADYutfvXAtNgf67BQ2gWTI6+nKfILIwMPzCbQPMd7pykzF5nPMu
 Nswt3E7efo5IP1Zsv6DRrLafAW0OJSmL/oo8/ta0AfqcxCCbJ6CUyViifRZ5T4nU
@@ -6985,7 +6985,7 @@
 
 sub    C4725C965E0455E9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFPSiQsBCADcgBiaKkIG5jVFbQ0NyG//y18S84/OT1X1I82OwtTryxNqxT9A
 q6HuTJqRPi5Qd0BwmQB6dG0mug9AEp58L8W5udiDysHeUvBKY6zTOprSSFvFg/Y8
@@ -7012,7 +7012,7 @@
 pub    30E6F80434A72A7F
 sub    C30F4CB428DDFC28
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEaNL+QRBACYhfwZdDNXVeU9G5/XsxrUgQGKkhfOaB1CyPHAd02Jyc5oHR0a
 nu7dHb6QBlY8b47pX8ii+uTCOX2yyFlJt2cuKYqN1TwHrMspDTC9K1x8WJMmKdM5
@@ -7044,7 +7044,7 @@
 
 sub    D79E291A1BF549DC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFZ1ptUBEADVzx4LjDmWHK4gY03VBGRh/A+1CAjwdDtcrHPnoFYCYC0uoe8m
 z/iESYlAHRqVo0nMItZgjqGTPD6GhQvJn/fzXTjIpYIDLZgPMXxImHCSRAFnduI6
@@ -7089,7 +7089,7 @@
 
 sub    5CE9BCD2ED28F793
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF0vfHYBDADEDPY9ub98c7jQe4yMbPke3A/sxNHnn0WuA9JN880DPs3L7lrv
 9VHTOlFXslDNBPYSbgFXH5YlMGg8ZY8bhngjc+Z3dtrCX1cAjUXOnibi7fBFomLB
@@ -7126,7 +7126,7 @@
 
 sub    7494750BDF4F8FAE
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE1/v9YBCADaUiBecDzwU5g9Gmn3T9pAa17OlUl2iH0zn8tNTUg++bW/A9m3
 lWykQBlvPOi32lqZ5q7yewSNBGHl/pHRRVsIE6hhkVigNQbMztRFPshKCU/0RvKu
@@ -7155,7 +7155,7 @@
 
 sub    B8EB751F2C19011D
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGA2h7oBDADtWcow8HEnabHf+poCBJR+MG8JybFpgOQ5ns1e6b3xnD51kzqv
 0I1orkmIfhCVU4nPGp2jy0JHQUvf3NDIDobt/O/C7+3BvNanfw7sJeHXrCy90o3I
@@ -7192,7 +7192,7 @@
 
 sub    FE694B892910DD22
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBErygmoBEADbs8zVUn5ZwbsG3tqT4x6U7SZYOtd3WXOtHjuu9Cyp74rZ19Pi
 XNbYwIAoCgOI/nXVWwuOrNJH0pHaQ73slbNzLxo2ahQIkw9PbK4V3YXLai1r/W6T
@@ -7232,10 +7232,47 @@
 =n4Zz
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    36AB7ACFFF2027B1
+uid    krzema12 (Piotr Krzeminski) <[email protected]>
+
+sub    958D552911BCFB32
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQGNBGBM2rMBDAC/vuwIsVD0yQhNK5GF6eFRSxQxfG/XPXBQldx/+xTJaUtuItZe
+qxBTjmkwDP8m8jMm4iNtjHNOyfbN698nRC7QRRIheAw6MvqcTJNw+6WJku2vsxh4
+jceeGogIgAXRVQmOGApcLkD1+tYxbTcSU7wXrD8dxspAy14Fh1ZmyxhmhrbhxdKp
+4Z5rQPnQdDmzu/CuaN7qQyz5JDcwql5DGvXbbBVxBdYb34rkF2WvxZMEMuHIYoLP
+BWcHWsmNrY07+46O6oudiKk0f6mIyMbHbqAyskarmeXUQrWPMmHT7Ni6KDANRZLB
+m49VJV/3mYpmIZZq4QmWrIM/kg2lw94nwtn1WCseeH4kt2TSB1UX0FnukRYVoSM4
+d2SwDoBsW/HedO8A4nlNCYqrURTLkcgoy1EyegdlmRn2q/FAnCIUjjm4hIeoy2vk
+QeaaQg7lCljHAZtvzWjtN7WBRG34XwNGAXiBXdITeMo335tzqxruDCKGdkRnWFIF
+URf4O+WsZ3cc/kcAEQEAAbQ0a3J6ZW1hMTIgKFBpb3RyIEtyemVtaW5za2kpIDxz
+b25hdHlwZUBrcnplbWluc2tpLml0PrkBjQRgTNqzAQwArw0OrUg0it4NxA4JqEQ3
+Y4cVwc9FPSpCpR79k+CUXPmmrwlO35Q2VvZ/nxGYL4M1CnLfrt46nnylUL+oX5Gy
+dAu2cyqCL1rH3OLrk+q0KEexkulWhLJfaOUXV3PZBkYoFRleZHvFn8Qfhlo7eK67
+LQ6vWLTBCqWwNbE5kbmZTiwBLkL0ajZOXP4rMP0o3aBkiMj0Qiq5DEzrYjpjGtUm
+CZZwf4X9c86tkbSS+i/VgHQfR7MUwg2zXswJu/t8cc+PPW7qpyfmyRhnKBFK4DqH
+PsqDVFAnWUqyV+Mp51wiUbTH8AvlKCk+xNAGhNH93/m2pBWLHjDFQFNwqiTwLdj7
+OVLaokE4kjRXYow7TrjbeSfY4VI1ZWkIFw5MdDQ4VnzRS7lRGPn3Y9Ortpu0wRFR
+gygnRF75AWqBtoqSjtsdUL2X2kx8UleOE0NAVF7MwdlsUruBbnh/TFcudJFjAEdU
+YmfJhoL2PyUpsK2itHZTArXFLooEwRCQGWWxUYqFZ7NzABEBAAGJAbYEGAEKACAW
+IQSCR/iJM8LVb4MK9zQ2q3rP/yAnsQUCYEzaswIbDAAKCRA2q3rP/yAnsf/qC/wO
+Ou/ZIQGNZe7ebemklst9m2PHngB9TGljixu3M8QHhDaH1eIYOhzhMMNwtJQmDBHy
+EWuYurwEJVV62vgyl2/S7SmxXDE7Kp4DRuwJAvMI08vCKLNpwHBp2HogCON6nVAv
+BOedrDhFNfeBXMx5Ig5CPcGb3DWBKCnZiebtyRMQyXq9z0wC9ArS6FNmzXOfZgBA
+xSAuoIgGOHBOF6nOQAoQdAqHpIKJWa0kvBEyRU5vzTJHwVCNtrCDMtgfgYWXouVs
+cljqKyXTSeO0wdiIfDelnZXS0gU3kNgxW8kFumlHiubOnzn8ClLdL8piGFxOXbcm
+cMWh0+SndYzZ9SbnUbUKEbR7GDH39uqrZ8r3YugBwwUut71uWtu6EEhvLAS+jRo7
+qKx+doc5xpCPMyHPpo539DTLeowcp5O/tdkaqypfeSPf+ZOzLWBbMj9zsq48mSWV
+4NzrdvDj2rIvs02a2YMsdpNyt/kQbDbZiOrPuFWwdIw7pNuwCpt66Zu3V6d9dCQ=
+=B5lt
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    36D4E9618F3ADAB5
 sub    C4935FA8AC763C70
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGGiftwBDAC94Yhhh/5yO8jYFkg01MPnooXKZEPwxAbAg9wn5iM0tHxhEpkU
 zJVYZ+JYq013+Ldp8Of7A/d6hKTtZ0xwSeY7S/WFykIk6tc0P5j0sfFS3pGPDk+W
@@ -7269,7 +7306,7 @@
 pub    379CE192D401AB61
 sub    0CFE993CDBE1D0A2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFTi8JIBEACcN1ucQ1uCOZ1owTELQV/6i4q7NbYdJ5wf7yPYfEugSo3yfbo3
 Pw/XEvlnpDZmT155sGNOkteZtZMdcm5XhFbdtquLlrkjAcUGatq5rAt3eLAlvU7u
@@ -7313,7 +7350,7 @@
 
 sub    D908A43FB7EC07AC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFu07rsBEADYizNlY0FYNZ6q2wx7AmWLw6PHje55uFhYM8Saqtwg/rm1tl78
 j28E/coP2zMFf/ec+zqKsfYi4DMmLZ9ESIngMUOIE7mY0Pp4WN7oYFRtvU0ARWyp
@@ -7370,7 +7407,7 @@
 
 sub    9B2A1B698A113AAD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFzwo60BEACg1rgL5jUtKkFE5DiwqJwxzJyJDH00TBSN6ZT+nXh1UxgC9q2h
 olF9V+2+LV1Jcmnc946xzIMiWLG33QB0NKVCdU5jNuLahOcViQQjNfGXwNzYoNCR
@@ -7413,7 +7450,7 @@
 pub    3C0A8F4744F37328
 sub    D17266C6E05F9993
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFSQ6LEBCADnoAfQsg2uDYMnEPqt7tlnZxzyLVKiHXdJzT6OHA0FUdsB9H/9
 vWI863v20dsk4+tf1pXLa1AWBusInf7FM1JBCQBc/By3fR3JRhJU0QSoEcwtOQSa
@@ -7439,7 +7476,7 @@
 pub    3C27D97B0C83A85C
 sub    4BC7B9A81C39EBA0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGEdX1MBDACuRDzoPMh3CyUHQydFo363R6OdXqMZ8mJQMdysIJCXOXZGRwUC
 uyPOUfH6uSG24RU2zvD72D2SGAehQKLXLQeN6XCt9PRAszP18dJADm10xgkXJm+G
@@ -7475,7 +7512,7 @@
 
 sub    575D6C921D84AC76
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGL4BxIBEAC+lX44fd/zrVQPzdKygarBd/X0bBpGakT++Kfk4UBGl3q+wd2G
 R9puB9R377ds8hU7U3To8sHguUZo6DbD9Gb/is/WajSb9g92z+rMow3KbqfCYqWr
@@ -7521,7 +7558,7 @@
 
 sub    7ECD484BE871E4BC
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFhV2aoBCACyHcEuTUn5nVo1ODvWvgBgV8b6Aju4cVAhMNIvAdcOYf+N9Rgo
 Y/669/P371uN2hc4SxJeORBjHyzkAX2sJZQj+FwdvGl60YX9Zv/NQaTzC1WFMRp2
@@ -7549,7 +7586,7 @@
 
 sub    6B7EF7B18190F4A9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF2KLsIBEADgVw/j0Loslv+pBDEfYemeObeKCWBhEdAiGznT23XFb4eOa4oL
 Yk8FTL5SYV+Ylm5Pv4zUGV1JUggzb4mS5+/k0kl2OHzZpJTLz45E9Qe4KI5vk6jT
@@ -7592,7 +7629,7 @@
 pub    3F36885C24DF4B75
 sub    97859F2FE8EAEB26
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFmfSwYBCADdZEuR8cs2ejLLW3+Glxiq15rVbHbxaWmmZApGNijFro/LzFrR
 z+99N1mnA5+Ar/yKmn8lsCiTWukGQzWbdH/QSRUdyHtzxbCSeONdMhdKl3sJY1h2
@@ -7618,7 +7655,7 @@
 pub    3FAAD2CD5ECBB314
 sub    3260CB2DEF74135B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFhqdSMBEACmveOOsQrTky8b5M+Cq6lbhqRB4+INnfigxr7+EMpswo4AxYuA
 Op/YG+G7NU5h6EK6Tj2dVfXga90GYFkehtFRZgOUJUGKPU/53upsbnsWS8qjJD8g
@@ -7662,7 +7699,7 @@
 
 sub    C0B9C2CC3DD97C16
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE5zrtcBCADFfU0ugIGUCM44fqPJKrsB3TaDu5EpauvFfYqUfyookzMHSKtB
 4YqBSKzBEiZ1rFB/KCn7XJTh5epoCau4DsG4U0XZjsx+esDR4ZtL42LEzeMTuluV
@@ -7691,7 +7728,7 @@
 
 sub    01F3A913FB698736
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFLmWO0BCADfxkkFnwj5uOALP07g8yArQpu6zbNr+5dtDvJe8Y51V1leb74U
 Eh4U1IeosCRdKUCg0XlAjDmjrUkG6W/5AMUZM676JVHL5tVG1F+dsKhCrFOZoMHj
@@ -7721,7 +7758,7 @@
 
 sub    47624A56526BF2F2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFAZ8yMBCAD+elPZR4dx7RHLErbQadUXmxxh15JTZ7A/OmARW0ZA1kbkRven
 4b3rXQKtWhZqxHh9Vb1FMgOnrbOi9984J3REJzLWEFM+REB6GJ3/ZAQvaAmrjDtV
@@ -7750,7 +7787,7 @@
 
 sub    8A57131A07E0911E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGB980QBEADLBOfY981RbUf7zI9AoXcxGignXkYbeSvxIMML9vAbnhmuHwa6
 h+81ZTY2XK7Rz211y129YidPykkiLX9mY+OWvJsj7dTyVTcIm6MU5ETDvovfmKWg
@@ -7795,7 +7832,7 @@
 
 sub    1364C5E2DF3E99C5
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF1Vn08BEADgfOupXhJxyb3t1kzDNa595spJptjF5ViyXuEJtlMQlmobPP9L
 2gZH83gNe7Ro1TsLesgWTtin3hGANSKITdi/wVH4ET6lPInv1k/8hXe0zlF11Zmi
@@ -7840,7 +7877,7 @@
 
 sub    2C8E4A350000730C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFQMn6cBEACn5RegEd6pYnrIwFMpf/SKP1aIp+rF657o4zP2eQtCyU2Kxiyd
 VXyvUqIN9kv8exnNUOHnjQzUyVFmcaYaQTxf6D+DVkSlusHk4yq+6I4K7g42Ghvw
@@ -7883,7 +7920,7 @@
 pub    44CE7BF2825EA2CD
 sub    E01173141D06B1BF
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBEzQQMUBCACbwbw7tuTWgwPsDAdQTWGO355jP75oBLHwGgEwV+OCKtxkNXNw
 wrJqXst83vmD1dEJyHflQww+d+Olj90IefQGfR+K7O005C2nky7eNGIomxaP52Y/
@@ -7910,7 +7947,7 @@
 sub    8067ECAA8D58321C
 sub    750F9A735EECF640
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFkgff4BEADQW10I1gEirYflEkNU9ukvBD/UFzsNxtKKxiDB58O1j9/o8bJN
 uM56B/skfFg1V4Gkpmnf9sJyakI8jHIvZ720dPHB8nVRBKV+sUD7hoI2QYVJMJMV
@@ -7977,7 +8014,7 @@
 
 sub    D74B959DFA1D84F2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFkW7RoBCAC7NMtr/e27nrUuIcEZEJBZS3TbZYId80UNQXgYmqPhy/sfCyMc
 87eKzOalauwLbr5+VGuKqhvKrihV1WCt2+FUjOtnCf1GutpAUH9plfSs8IpRog0h
@@ -8006,7 +8043,7 @@
 
 sub    4CE6E05D128BCFAD
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFE0soEBCADAy/iIRT/lpb+vfDjWs/k1XQNU3mzXoMm1mmS/Z8VOc0jF7sVB
 A5z2pC6u2OmEr1oJkhWefX+mU//7kXs6VvUCReE4uheGBlisg/ELEXkTm342TcwS
@@ -8035,7 +8072,7 @@
 
 sub    868FF6CCEF26A83C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF5CDMYBCADC1/aWU6ZbGZEphRbmjUPNfqh3N5goSnDCou97mmQ9Uq8iBuKS
 UXJnGSOHudXK56f+Drx5lGZdLAzveZdqaqb1o3yLFO3PJxwj3Ulhab3O3uTG2eR0
@@ -8064,7 +8101,7 @@
 
 sub    FCB1A11865F6A17A
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFVB0KABCAC8YRgcTIomAMw865DHxS/tbFgqN9i7M+tgpih1ETJbb4enhIBj
 Upeq+MoFCtxN86zGu2gsA4DOMEXVCReJ4O5n0F8E03+NUraCnJjbXLW9eEyRQRaU
@@ -8093,7 +8130,7 @@
 
 sub    3EA98BD451E4B457
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF36fYEBCADU+1b8TH2AhJuJXebg5D3UbR9rk8/9kEfiF7ifbb3nCB9tnF5M
 7NnNocEdAq3XezNuSj9LtEpWUu6P4JdpXcfZiQO6wrobzSJRUWDc7X8D8NyhGpd8
@@ -8122,7 +8159,7 @@
 
 sub    726F4E5C34CFD750
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF8QwXwBDADKNLAHhjWUqnLYiO+ws3Hy1du6tMvkR3nfsnIDqpCvSjb+3/rI
 OHSyq8TbaGLLuHOM4K/KvrKgjhTbXQxvx1WR5IpoylcINzI959yAbaywBj6gVQB3
@@ -8159,7 +8196,7 @@
 
 sub    5686B45C142551D3
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF1EtnUBCAChtyYd/4eMAxTz5DVmO+8QOrTA1cf9bprQhtXD5pVbw8/IGKN+
 EqXmvt7AGy+4O633g7ec5iyirwCfEP+4YDv8k1LOvY9C5+tOwfK+FxAPRVc1AAB5
@@ -8188,7 +8225,7 @@
 
 sub    A568CCD291175902
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFUpCooBCACj6t0qOEGqMpX3puhk5W1TXZ1ewSXPS1yaoiFD2rysxjVWmXxG
 wvon31ed6PaZqtv+CUCIjbCjJN50dQF6g1I4FLvDcpF8LuLGriYtFW43lJ/GW//G
@@ -8218,7 +8255,7 @@
 
 sub    25EB2A6CB1459233
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFBm+zUBCACsrBpO6mOsZ/B6PdPPV/Hj87m2GHeEYEHt2o2l8X2BdbZKbVW1
 FIKnpYe3+TsFCe/qNxlR6vk0Jpy3ChD3nW/J0rmU0ju1SZnS7rdSMj3AI5M5xxpy
@@ -8247,7 +8284,7 @@
 
 sub    BE0F021FCB5F68A0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGBFmccBDADIusjFY82nMHFXYxY1b5eIWtyaXTQxv/bXfjR2Yb16dURgFjai
 OeuYzapF7vVqNV8/H7Sya0W9z4OWf0ZttWhtQFcmhF90586OArXEikKcFgO8EL+l
@@ -8281,7 +8318,7 @@
 
 pub    55C7E5E701832382
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mI0EVdDLQQEEAJMtYCaTA56YsP5RzQzPvqVTaR2nZ27qRk36blHB9WmXK+NHpGeH
 PHgq59mLPVueo2/M5k/fFrCe36jHePP31gYpFtueeYDfsofHwod0WhsHyC7JfG8d
@@ -8294,7 +8331,7 @@
 sub    D89D05374952262B
 sub    B5681E477AD61C38
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF+7lwIBDACcXIXAwFDoWvCCWn+OImyyJQvSnnte93Mc1ZJtlArkrjeGU7Mu
 5giUH+FOyiXlj7CU4G9RTnAzDgM8XPncWOERgRG2dXtO03Li7iUEX4Z8PCUGsTxP
@@ -8372,7 +8409,7 @@
 
 pub    571A5291E827E1C7
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBE9iFawRBACJb4OMk3zqMDNvSJKYZ8fGYrPq7yCcf/ykKDkGb2dtPnAZGkSp
 3mmNlTsU6s9ARn7BtkhIuM5TdbLs+z+okX62h3F0WW3h+CpfIXyKSgl7uWbhZ5G8
@@ -8389,7 +8426,7 @@
 pub    5796E91EE6619C69
 sub    153E7A3C2B4E5118
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFri3Q8BEAC90D8TTu6C05m/eq6HbU8gOHFc+2VJriVmnoyODTlEk/LAsT6h
 BRok7nzY0LpNUzUREjJy/w80YTOjLs25IFhnqA6mq8BGLjFwjhBPA4piCyhW/Elh
@@ -8444,7 +8481,7 @@
 
 sub    2E74CACB6918A897
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBE1VSkkRBACkCgvt26sMi+0X+EOJDMqdK0Sziy06k47LJf1jOg4tTZ2T9QtP
 OZ8fD+va/O5+q8Kna993jzcO5n0Nv+R/K3+MvUqSmdITshCIjBt3cC0n6FWndGyl
@@ -8477,7 +8514,7 @@
 
 sub    92BD2D0B5B21ABA2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFUBG7QBCADRWXf0Fw05qRhM4cRnGKlOW1ecue1DCxHAtFwoqmAXyTCO+tI0
 MEW5SyXUkX6FsWLl6A2y+KgOs669ogzfQ0rnZMEt4HisRp8wpgk3GWR1/9aKYz/c
@@ -8505,7 +8542,7 @@
 
 sub    990B862E2E89C087
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBGGugdABCACl8r69cybA9Xv4mVUXH2UErQutkOsCxXJCb6KcL5Ucn7+0RGxc
 PRdw+sw2VBJsBctdUeCvNA6o1126O0gSgbQPTojvdyYtq4J9OAjS11RoiiQ269Zc
@@ -8534,7 +8571,7 @@
 
 sub    8857595B73BFD468
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mDMEYuRVGhYJKwYBBAHaRw8BAQdA2Dp4m1Yhtb1g94pQzzL24FuP6b9KXF8lP9Dh
 hZnynhe0M1Rhcm8gTC4gU2FpdG8gKEZvciBHaXRIdWIgQWN0aW9ucykgPGxlb0B4
@@ -8550,7 +8587,7 @@
 
 sub    4E5C59DBFF7DACF9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGPcwwABEADTw/gqmHh4LTSDsBP0KMoXFtFQnv7xmVPPrPjt0NxGn3w2WIou
 7UaLUTViKkgm92h72gyM7N9JfNBLcYrqVf9ed75MPdGQgzIhkVg3SLWZGFoIQUJ4
@@ -8594,7 +8631,7 @@
 pub    5B05CCDE140C2876
 sub    9D29AE4A6B50E01F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q
 YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs
@@ -8633,7 +8670,7 @@
 pub    5D67BFFCBA1F9A39
 sub    DBE749136BF76809
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFUHdtcBCAC5xFdAcSc5qQsPkujcRdzeldrESZBo1/SfGwFV0T+lgp99QJuI
 LDwZ1OEG/lQck59J0JRdAgxlUj1um5LzNYexIJSdxRz2DffQ/z9R+hw4DF2h0fyP
@@ -8661,7 +8698,7 @@
 
 sub    A7CC6488427379A4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFgRFtYBCADud9fmvTI8Dbs+9GcZUIVzxkL84QYHSDxI9fF+sxfAviq1U+YJ
 a+ZLIW7HsXx8vpn3hqIqAbDxHjrb6MEJ3OWD5Ks7O9Lq7HOhtqAT/mpV3fZmf6pF
@@ -8689,7 +8726,7 @@
 uid    Sebastiano Vigna <[email protected]>
 
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFijpzMBCACxAT3jijwXbI6b7LIF/k8oSGyM8ZNJpb6AQvPqKIqCzxNFXzow
 EBCasKMhIWgGy+293Tpt/DY4btJie4u+igMBS86iXrF8CUnOLPgTlAIyil/oREGJ
@@ -8706,7 +8743,7 @@
 
 sub    91A4BA316974A467
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGHvIbQBDACpPdbz5UIVIWR4cfXyyZEMOG0ayCzJQPsT4eq8XR0o5Y9egfAq
 dRXC8paInsaF/iVL8BJY6CNq4B3dUfJwKDcJiCiPbiQgknqF1HDBqQtCb4akW8f4
@@ -8741,7 +8778,7 @@
 pub    5ED22F661BBF0ACC
 sub    31ADCD8BFCB760B4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBExyNhsRBAC/W5cMapoP7NUn8S22iWG5bPw0bconApJHP4kQdT17gT2JgNJz
 BmuGWV59ZOGQkc6woeFKc1s6twlsgIL51jMeVOtgLJRGTS4So2hthNqDcgO4j8Lm
@@ -8773,7 +8810,7 @@
 
 sub    0440006D577EAE4B
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE7JURcBCADO+9Dc4/JnB+wX+fq+Fr2zUGSPOT6/qjE5kXL4FEbJKsqDSAKG
 VnbtRrsIUdmNIFQmz71bBDFhRBbrSrkz927k8eUPhYtxE2NmmWSuKgrjF4qviPQv
@@ -8802,7 +8839,7 @@
 
 sub    73F7734B17EC71F4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGEVsM0BEADiZwFLiyjeOLeGS0jAso0pOwUigT9PpwQq7JFAuJP2i9C4Eunc
 J2HWRdMhnAY12C2MVetSwhI/4QID+rIreB7ooC4xv8sz1PIC30t2oSYtXF4w5DYh
@@ -8847,7 +8884,7 @@
 
 sub    FD2D3AEF63B97A64
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF/kpOgBDADKuK/xrCb39AAmyzVkFTP03ZNCAVhDnmx/1bSHTwvXFWQ2topE
 IgqlMpKmjuEH03gfOP2ibbgeJ3WOJcijqfeHNZ7wGDcslbKOnFVrcN7DuJx9LDYc
@@ -8882,7 +8919,7 @@
 pub    62C82E50836EB3EE
 sub    2AC7BF2F3349DE80
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFeOGY0BEADIr99yL4ahwgM3KB7zMVzDk/PEkzUWpm1BSxqUxuQtzWArFj13
 Y3Zi6g1tw5jKESfxtmpXx7j7xR3qVdJbsYJMU0zQi+FehwnKox3Go3UnIKt7kydz
@@ -8926,7 +8963,7 @@
 
 sub    D547B4A01F74AC1E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBE3XFIUBCADcj1zw8m1evCgEMqxgOfl6L8y1tsYWsX7tVPvHEkYlXHrdcpkB
 fGuWPrauvhBmB9sBkFfxzU98Ilz3Xk9pfISYiaMUk9Mk1ZxsCoYPVhxvOSvk5LgS
@@ -8954,7 +8991,7 @@
 sub    D3DBC823BE4819ED
 sub    0162FE0CF6E18BD4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBE7sdQQBEACsLaqrIiSlsJIWpalL9i+i6x8Yg6l+bw8qaH/i7kjZKFLf6Xrq
 PFHo9dpF3LPOguvPLP5fs04KIShl0IhJuArSxvwfH8GnqPAaM0TZpfJQ9uqAcvxk
@@ -9033,7 +9070,7 @@
 
 sub    1E8F1D57A4450BCB
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFbqsT0BCADwERe1Rc9qNWwXOvwZHsjauVDy0TpqNVY8I3S+OYm4rX1dkjyh
 +6bTEH1ys6bKevvR+PLhYzTGKboHnMT0RIINY/DQQSzHr/GRyCiiRlRvULbt9Fnz
@@ -9062,7 +9099,7 @@
 
 sub    DCF4B49B4D5845D2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEmhev8RBACz56FVQ9l701+PE7Nr6+6Lsoy5tK6wmV89pEvUDgDjT0VTs4EI
 dupAk4a0dLn8Lu87AloEYuSzbCxv5cH5vyDcvLDK6g3/sRC1LPQPydD+UlCvG8LI
@@ -9095,7 +9132,7 @@
 
 sub    0AC07D0BBD11498C
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGBVUWMBDACXALXWXSrB2V95lR1L+i+sQsTQt8tCIgX0iX9UZ7Vw2K/lLnLw
 WYtM3oTxYox4OdgkK9tK6771EdCH5wQtRdUQJjlsBfZDPMiGqmh1jrAxAugEkFyC
@@ -9129,7 +9166,7 @@
 
 pub    66B50994442D2D40
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBGDoYisBEACqUDZnT4h6ma6XIzdC6KR++uDbR2VKdhCuv0Og/sHEKkm6ZbG0
 OFB8tAaQx/WlsoQyf3DlLfUEOGDai875Aqor3fbM+E1hrZbQNfsOySKEE52k7PYe
@@ -9150,7 +9187,7 @@
 
 sub    A1766BE5F812AC2E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mJMEYvEGpBMFK4EEACMEIwQA6knc/2gtbqDhPh5EzrymR4Hwi1Xf2S0aqMopA1zg
 IeZzBgSfL+4fEfpXL4eAzvrk29jIXSizDEOgFpw3PW3Om1gASxub4Jo6EQrRgOdd
@@ -9171,7 +9208,7 @@
 
 sub    CA7AE93399B1ED99
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFqHCi8BCACgRpCaVCiJ2MccCN01SbHYowmM255nSYKOnfItBmXYAMtc4rL9
 n1y1qFtc4LBbkIrPH8CO2zpEImUTZel4W93BQkluPOO3EX/hLCTCFfXrO89L1u4V
@@ -9200,7 +9237,7 @@
 
 sub    C0058C509A81C102
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGAofm8BDADhvXfCdHebhi2I1nd+n+1cTk0Kfv8bq4BQ1T2O85XlFpp1jaIR
 70GAm2MOt8+eEXt/TuPkVBWnJovDpBbkUfYWxSIpPxJzcxWV+4WJi/25fBOq2EuP
@@ -9237,7 +9274,7 @@
 
 sub    4083687620E57086
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFfAYzkBCADRFsmQXLC6UbFNjCKrwy+AwiMNUchJdsPkbFrvueWKq0nPB6Rh
 D2YGNdCdLlkeybHHaSjYi/Xdxv7Vgfp4d32tzoqQJe8Q8oYYW7KbTkfzwH7TNLcw
@@ -9267,7 +9304,7 @@
 sub    EA8543C570FAF804
 sub    CA890A5FA09CFD80
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFgMcBMBEAC/xcIVVOOh+F7S0OTzBlFH34s5fDbi6Zto469tZyW1peyWtXAZ
 m+2jzFfeTCHaUQO3YjoTy2fPygS4tVD+ew4EAzMG5Uti4kwWZw0PYKz2JO/gl1JY
@@ -9333,7 +9370,7 @@
 pub    6A97BB242496B68A
 sub    374A2ECC99F4A7A0
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGK88f8BDACqAfnTaZazrzbO9vM+3nAdmcW1QR84zwUKneFML/I45kihIW2t
 zhcx5JIwl7gK6q9kzRGClMCkSGhq0y9Q8UGR+wAmLJ8bexS998c3rtFfg2/c1zBC
@@ -9369,7 +9406,7 @@
 
 sub    FA6831EE37606774
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFTDM4oBCAC9cUMAjkP1dD7tt0JUI5kVORKagn4/zG6+Y2MUwGgJs481xsFC
 jXPuNZMucAVtXmw5Sl7FbsfSxR/9jJ2pnbXL918eRFbUqY4LnuOTZjcgNWo8PWPc
@@ -9396,7 +9433,7 @@
 pub    6CCC36CC6C69FC17
 sub    C694465FAACEE66F
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBEtrDFABCADLXGAhjPxdh+naC6XU5kficZYEVAURNRa8MTnaMKr+31v2zcAk
 nyqyjihcXGQBCeaNsz2mQkc/MrKdnFNVSwp715JcmcqDJGfR9aIDMUs9PvoNkkqv
@@ -9422,7 +9459,7 @@
 pub    6ED0F678B90EB06E
 sub    3605922A9B0C4A82
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFTCZ/YBCAC/AX5Uve8E97kKLWumtArjcouxHzENzNez3/wjiuIdCTchm/G6
 fUKHNTqo9sdcnvAO4mfJbysXh1tqXl6zxjw1QdQGCyy8klGRlpEiper0eS5heDhV
@@ -9457,7 +9494,7 @@
 
 sub    E2F840B227D3C024
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF2ClL8BCADs2bbaF1ZMiMkTUUb59NTlyAbOOVWoIh7cnKeNjMWBUTP0kLFI
 XpoKiyccQLP4rFdbP2yI6h+LJR0Kj/lJmKpCaAooNlooxfIyPUX5TMvDTRutzwBO
@@ -9484,7 +9521,7 @@
 pub    72385FF0AF338D52
 sub    458AAC45B5189772
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEr8kngBEACvK2oDnKTCGQWUEMxCgQPYTTaWVHzaRFZCn8po/DnKMh8llPuU
 GRdi5O7ChLjsg7qlNJKhi//ZoSnNBdPfT7EGNaKxUO13BVNBvXDiNNbUTWGBY2W7
@@ -9526,7 +9563,7 @@
 pub    7457CA33C3CE9E15
 sub    ABE9F3126BB741C1
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFIXyRQBCADe285y3Pu7KzoKyP6wqeNXtvvuwMatAmPm5x/i+S8MlryqzsYa
 x6twUmXV1yKjjtGrO+9fHvTOWBfSSP+fP9KTaTQYSasoJq2Mw4cQDy1i0zrxNZUw
@@ -9562,7 +9599,7 @@
 sub    6494C6D6997C215E
 sub    E88979FB9B30ACF2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBFcMjNMBEAC6Wr5QuLIFgz1V1EFPlg8ty2TsjQEl4VWftUAqWlMevJFWvYEx
 BOsOZ6kNFfBfjAxgJNWTkxZrHzDl74R7KW/nUx6X57bpFjUyRaB8F3/NpWKSeIGS
@@ -9755,7 +9792,7 @@
 
 sub    FA84183FDD6A6B98
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBF6RvwcBCADIVU7oxOiljoWxNTkZ00PKVwyqhahYpN/4lamULtECCS+HAF+J
 DsNy/6QCl7lKAGrSyn9dvsI56KEkGvUJfpQrpRlg+uIQDMxS8JF7p9n49DNc8Q88
@@ -9784,7 +9821,7 @@
 
 sub    DBC5123E2E98FEFE
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBGSsZCsBDADJZoPoHGJNAB3sn/kFQ3zlj+vZ7OY5aWoH2nL3tHQYZvN/pJRs
 8wu4Cw1ApatqLIaur6S6LR+s4xB7HxnMvpiF3NMwr6ZeZBUUTGEJbRgFhY9TqZam
@@ -9821,7 +9858,7 @@
 
 sub    AC9F6F1991913E30
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEqXMWkRBACnsxVroe9ojc2AnRn/85KJi/Ntsbku5iJ5z72B6I+VGn/b1Xln
 kuvRJ41RLG13lKVmHtSTq2pajjmAr9jY5gS8nJ3JUES9bG3yKNN1IDswXExfAUJp
@@ -9864,7 +9901,7 @@
 
 sub    9F7335D63326E7F9
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFWdcSoBCADK8j+0eVZKUGctZo/VaJ/K2Wppx4jEFgih8xiIWREQ9B3QEugJ
 mJMWZHhrnHB+sjVx5No482ch6sVhYmC+VMyTdzepItZ8beYa0pnNGJnrFT+HcTOS
@@ -9893,7 +9930,7 @@
 
 sub    C3E640F38D845FA2
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFZUsiQBCADGmoidvh3VvXWGdwbAtHPtDPKEebE/MfFVO+QTRbjJxphzKwAt
 mxHruikafaSTnC9FWizj99e/Yc45YZHcnt5Htmy0a7DSOQXL37rrnieZxg86tYmC
@@ -9922,7 +9959,7 @@
 
 sub    C189C86B813330C4
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBEvxja8BEADAzZOup1X0B12zJsNsDvXVIwmM6bB+uhEsUHoFTvmsEVwRoZtn
 i7Q0WSFoY+LDxbvC4Bg1+urCrUrstRJYRyF/pMqPYq/HokRlPjtrli/i3mUSd0zN
@@ -9967,7 +10004,7 @@
 
 sub    926DFB2EDB329089
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEPonucRBACtbhYckAoyz1tuSXYX4XiqGa5390gIMcxe2hJ+Ncx9o3zX09Im
 f8PW27BnMrz7EIydgB2wphhjfK4vkNNtm5ZDWH/zJStsk1Fe7lNuuxs8XorX1+8D
@@ -10001,7 +10038,7 @@
 sub    3967D4EDA591B991
 sub    0588BC69A286FF16
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGNBF+TCd4BDACbIA94MfIWL0SpvZwBddXgx36Lp9GYOWNgGoQCWSvk9vaMrLaI
 rEll0xnoP98CfBQYrVSAmHDMhSLBCjNB3V1Sdz8GRdOG7HUffF7Cqwbm3Fxo3H/h
@@ -10083,7 +10120,7 @@
 
 sub    9842FE565AA0601E
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQGiBEvsZw4RBADH20nX+H1xvMBYmXRj1Aae4dRr6Y6qI7QRWHO6Z7/dxr9bk/NN
 Yjq5KsVOQxZzloVdtqx75rznT7fZq98g7Nq9IeEtB6k4tnh6XQLhljJMk0a3mzdt
@@ -10116,7 +10153,7 @@
 
 sub    D7913335BFA51814
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQINBF99kCMBEADE22W+h7w4R9fHyqEQV1NpgY2qI1S1HlwSQLQnf1pt1k3ZvMzX
 Eh2q9lNAn96O5TqpiMhHm2ZEDORqXqAx6hEVpA5iy+7NrIaSpxcbM0crUxSqoceW
@@ -10156,12 +10193,82 @@
 =oANj
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    7FAC222BF1FC0C12
+uid    Erik C. Thauvin <[email protected]>
+
+sub    776702A6A2DA330E
+sub    3565C12F190E4CB2
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v@RELEASE_NAME@
+
+mQGiBDZlGC4RBAD71K/vOcX69tHOv4e6nuIGqbqooKxURWJhe1OvoK9Z1pgadt5D
+Ct0HDuSGydkA91cAM3a2QzZmGepGS8r3s3ldTf83uBfsriYM+yXmalmHYNdMVrPL
+CDFUNMH7A+2rnKvhQ7phmMVBdeEtRcrncfXMHmFvfq0h2y2V/NQ6VfJq5wCg/xd4
+gARVHy7gIE9VtVMg3W9D6xsEAOxmB+PxSjD0y5/NN81EMeOYc3INakbQtjkQ0KoO
+jlkWot6OidFBpUA/ssLRUpmBGN4nTztXiESjz4n79cW3bRAG3b1XtOxbCkNltlrD
+DKqpy4FwBsnslKFEAOIwttWJKXkebtWIw5Jm+HHIENH+IzoE43JFmKhtTS/427Rn
+FIi6A/4l/WesI6bUZDVXuicWh4KYrArARpuK59Cycz17lZmAES7nramBCK7uNEi1
+XlZwWhKz/tYL8o2y2cwuUv45EOl+mv7sF88z65Vt60N9u6HXu1STFyl85q09g2Vs
+il+/P8Hlh71t53yrTtmAwkugBB4mbdi6uvVFlWN0kkxQhtXGlrQiRXJpayBDLiBU
+aGF1dmluIDxlcmlrQHRoYXV2aW4ubmV0PrkCDQRZDPiYARAAtzgweuKZHqTHK8mZ
+2TC0ySaUYTRskl4L8JetdXpnnYVXb6qcyYHSyyzxyR2SrSj2sjdyK2svU3FSxYbc
+OswyFlBjJsapQydE0ZcWrzhQbMgm/3hhQdzzCB49W3nl6m7pjFxtZTZvrxvjxPIF
+iLQUIbphrMyH8ZoxjS/nfMc/k0q29B4qT6heICZQJoOEZ/kgPC8SNKoFRhuVSp5A
+fBRMGe04dQYqxUYGnub/ik0dKXKJLapk6OVDgTr0daDH/awpc2RWM17Ct4qzmnWF
+BFDHcUNlkvYoQYf3xEoLA0hiTJc/gT0IB22Bush4cSoPv+JHV8QnY56Nnliv3Kdl
+4YYdiFLIAfFwjP41QsNbEJsY74e9sDuh1ZofGQh7g5ejDBDGo7frHSEM5QC/nIWZ
+b5XzPO6uL027NwqHn6ILQVlSo6pZcBH3I8wPGX/lXX0oVELBUJiCGSvDFaK6qzkQ
+QdljUiNiq7RW+EuNr3ut+8zviruzTFWXs1psnQK5TenxIjS6ewb57byJv3jhq/G9
+hHqNlmaaSA2FQU4oN0lgyAVC8N6dIgIQt0nNJQKfIgAhDFgpmBhCk1WgNL554jr3
+CkHdmjFf+hFTJzGdE8g2pIDWh5QTBL0sy47mfInfnA0TDhUnpb/luZHBPrKa6arU
+c2Gk0Axdski8T3mQDqmGY8cGC8EAEQEAAYkCaAQYEQIACQUCWQz4mAIbAgIpCRB/
+rCIr8fwMEsFdIAQZAQgABgUCWQz4mAAKCRB3ZwKmotozDoOjEACpSTVTZKmuEcOJ
+tD1RQIKIxVIPu4tatD5nU0IzhsLYPB98CZJTbDm1zuHpjEB5qRRVwsbBDvQ2cZnt
+Sz4PKqALT+xx+kxrEjO+Ubv7JLaA+z/InwiMVppipJ9F7WpRRqhSOCcgkFjzKtsq
+Qa4V5SUq4fy5piHimj367u/aV6/Psz2mNkS6cFWYuKSwDy/uGtm3m4xvUs1y+YpX
+7zzdeWaMRcdNAbkfTk0tbc6Ij+20l/jDbADO+5KGuIlah7GZcT8k3uNzO+ZRLyQN
+mlbgupyH2NvRNGhc3loYgcwfDtmM5B0Wd6clkWH5Yc6uPt+6N5a037/4pkTngzUR
+L8rQcY7seC5um6ouVCXdVkCeazFAHNQg/Q0zqN2Gr25uDpzP3s8y+A2ucOcNSQgM
+YIPy6CAeLnE9CpnudlNGbRCVWT9B+pIjKLI3AZo/9geaW0JLc9S9+HXGQn0RO00W
+EYX7zfkuX8KtDznjw5JnyDbxRAPiKJjoIoz16UehtqzhBWJzmk9+kqH+IfQ6IIsi
+HJRqJD4veGHem9ZN9+5XdVOgsiSx0BB7UrjqXBu15JTQRwg2gmy6N5XPuV5yM36h
+paRG8FOlYCmOOnFmpzzEBJv4Z8oT5PNWdhhwTugaxv1yWlTzFDbg/va5/NFhwtDi
+v3XnqjFTesbBFwMWIhRRMpCwl+J5oM2lAJ4p7U6+8andiWS6tuMns/mDTJ3LJwCf
+dgb+Mx8gFFGCQpvy4DTYfDtXbkCJAmgEGBECAAkFAlkM+JgCGwICKQkQf6wiK/H8
+DBLBXSAEGQEIAAYFAlkM+JgACgkQd2cCpqLaMw6DoxAAqUk1U2SprhHDibQ9UUCC
+iMVSD7uLWrQ+Z1NCM4bC2DwffAmSU2w5tc7h6YxAeakUVcLGwQ70NnGZ7Us+Dyqg
+C0/scfpMaxIzvlG7+yS2gPs/yJ8IjFaaYqSfRe1qUUaoUjgnIJBY8yrbKkGuFeUl
+KuH8uaYh4po9+u7v2levz7M9pjZEunBVmLiksA8v7hrZt5uMb1LNcvmKV+883Xlm
+jEXHTQG5H05NLW3OiI/ttJf4w2wAzvuShriJWoexmXE/JN7jczvmUS8kDZpW4Lqc
+h9jb0TRoXN5aGIHMHw7ZjOQdFnenJZFh+WHOrj7fujeWtN+/+KZE54M1ES/K0HGO
+7HgubpuqLlQl3VZAnmsxQBzUIP0NM6jdhq9ubg6cz97PMvgNrnDnDUkIDGCD8ugg
+Hi5xPQqZ7nZTRm0QlVk/QfqSIyiyNwGaP/YHmltCS3PUvfh1xkJ9ETtNFhGF+835
+Ll/CrQ8548OSZ8g28UQD4iiY6CKM9elHobas4QVic5pPfpKh/iH0OiCLIhyUaiQ+
+L3hh3pvWTffuV3VToLIksdAQe1K46lwbteSU0EcINoJsujeVz7lecjN+oaWkRvBT
+pWApjjpxZqc8xASb+GfKE+TzVnYYcE7oGsb9clpU8xQ24P72ufzRYcLQ4r9156ox
+U3rGwRcDFiIUUTKQsJfieaDNpQCg2uWEeylyeb7fxS8xwZJNvwLabfcAoN9rHTJ7
+Z3ZeNKUlYL7uHGivlZh/uQINBDZlGC8QCAD2Qle3CH8IF3KiutapQvMF6PlTETlP
+tvFuuUs4INoBp1ajFOmPQFXz0AfGy0OplK33TGSGSfgMg71l6RfUodNQ+PVZX9x2
+Uk89PY3bzpnhV5JZzf24rnRPxfx2vIPFRzBhznzJZv8V+bv9kV7HAarTW56NoKVy
+OtQa8L9GAFgr5fSI/VhOSdvNILSd5JEHNmszbDgNRR0PfIizHHxbLY7288kjwEPw
+pVsYjY67VYy4XTjTNP18F1dDox0YbN4zISy1Kv884bEpQBgRjXyEpwpy1obEAxnI
+Byl6ypUM2Zafq9AKUJsCRtMIPWakXUGfnHy9iUsiGSa6q6Jew1XpMgs7AAICB/9I
+2phs8YFrOxKv9lwdRFT1DLg5AlK3aOhLeDMfe4oPdcfu2YBslX2xJaZK8tq5J0YB
+JjOz+FPGlSLFdBYgcsf+Wf7fm8xhYYoaZpFbBkBwNZUNOTg0QCpJwYUqeiwjIzqo
+FciFKL+esSRdRLur6C80Knepl4LHuOZAcjKeukHzUxjISZICIvSNzJe/FWoa4p7J
+oJuG2ScUXWhae0OF2ulWHmN0lw/c3NDkysTuwaVHl3jm9xL/8aLXAblzQqAuuOr/
+7V1/wvU92Ir31b2+BIRb6SLSjKdZfoyCQd19KBP5DjJtV3/mMLEvT0eMhMp9NdaC
+Z/DQtQ0jYYgDwvv2yGogiEYEGBECAAYFAjZlGC8ACgkQf6wiK/H8DBIhHgCdFfHu
+2whpwaX1/YaS03SxvtT8hegAnRSdQ88UIcXYj2PkFOiuFPrFBvee
+=crZ+
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    7FE9900F412D622E
 uid    Wouter van Oortmerssen <[email protected]>
 
 sub    AE6B5325E74ED034
 -----BEGIN PGP PUBLIC KEY BLOCK-----
-Version: BCPG v1.77.00
+Version: BCPG v@RELEASE_NAME@
 
 mQENBFnyVlkBCACe8zGkIlDV0dUKmk9PWe2Hw8qM9DdPbtpUOpmUOidGY5svQDL3
 eqvHk85TbxqFEe3Qbjjt+R+iApFuXy5kmueXTvwCm7nAU+k/pZtPuzHyhDs3iFFH
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 2d28c739..3cbe2f3 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -121,6 +121,7 @@
          <trusted-key id="1F47744C9B6E14F2049C2857F1F111AF65925306" group="io.github.classgraph"/>
          <trusted-key id="1FA37FBE4453C1073E7EF61D6449005F96BC97A3" group="de.undercouch"/>
          <trusted-key id="20723A6399BC060154283B37CFAE163B64AC9189" group="org.jetbrains.skiko"/>
+         <trusted-key id="217BFC8CA98788CB33198A8E7FAC222BF1FC0C12" group="net.thauvin.erik.urlencoder"/>
          <trusted-key id="22B79F456B06F4E75B8B579DB57BD58EF6D0A713" group="com.google.protobuf"/>
          <trusted-key id="24D04176586361FDA94EE0315F7786DF73E61F56" group="com.google.devtools.ksp"/>
          <trusted-key id="26063B04869F7D235CCC057447586A1B75EF0DE5" group="com.squareup.wire"/>
@@ -310,6 +311,7 @@
          <trusted-key id="7FE5E98DF3A5C0DC34663AB7C1ADD37CA0069309" group="org.spdx" name="spdx-gradle-plugin"/>
          <trusted-key id="808D78B17A5A2D7C3668E31FBFFC9B54721244AD" group="org.apache.commons"/>
          <trusted-key id="80F6D6B0D90C6747753344CAB5A9E81B565E89E0" group="org.tomlj"/>
+         <trusted-key id="8247F88933C2D56F830AF73436AB7ACFFF2027B1" group="it.krzeminski"/>
          <trusted-key id="8254180BFC943B816E0B5E2E5E2F2B3D474EFE6B" group="it.unimi.dsi"/>
          <trusted-key id="82C9EC0E52C47A936A849E0113D979595E6D01E1" group="org.apache.maven.shared" name="maven-shared-utils"/>
          <trusted-key id="82F833963889D7ED06F1E4DC6525FD70CC303655" group="org.codehaus.mojo"/>
diff --git a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
index 9ba4c00..86da8db 100644
--- a/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
+++ b/graphics/graphics-core/src/androidTest/java/androidx/graphics/surface/SurfaceControlCompatTest.kt
@@ -77,9 +77,7 @@
     @Test
     fun testSurfaceControlCompatBuilder_parent() {
         val callbackLatch = CountDownLatch(1)
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
 
         try {
             scenario.onActivity {
@@ -97,7 +95,7 @@
 
                 it.addSurface(it.getSurfaceView(), callback)
             }
-            scenario.moveToState(Lifecycle.State.RESUMED)
+
             assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
         } catch (e: java.lang.IllegalArgumentException) {
             fail()
@@ -110,9 +108,7 @@
     @Test
     fun testSurfaceControlCompatBuilder_parentSurfaceControl() {
         val callbackLatch = CountDownLatch(1)
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
 
         try {
             scenario.onActivity {
@@ -136,7 +132,6 @@
 
                 it.addSurface(it.getSurfaceView(), callback)
             }
-            scenario.moveToState(Lifecycle.State.RESUMED)
             assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
         } catch (e: java.lang.IllegalArgumentException) {
             fail()
@@ -170,9 +165,7 @@
     fun testSurfaceTransactionOnCommitCallback() {
         val listener = TransactionOnCommitListener()
 
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
 
         try {
             scenario.onActivity {
@@ -180,7 +173,6 @@
                     .addTransactionCommittedListener(executor!!, listener)
                     .commit()
             }
-            scenario.moveToState(Lifecycle.State.RESUMED)
 
             listener.mLatch.await(3, TimeUnit.SECONDS)
             assertEquals(0, listener.mLatch.count)
@@ -197,9 +189,7 @@
         val listener = TransactionOnCommitListener()
         val listener2 = TransactionOnCommitListener()
 
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
 
         try {
             scenario.onActivity {
@@ -209,8 +199,6 @@
                     .commit()
             }
 
-            scenario.moveToState(Lifecycle.State.RESUMED)
-
             listener.mLatch.await(3, TimeUnit.SECONDS)
             listener2.mLatch.await(3, TimeUnit.SECONDS)
 
@@ -228,9 +216,7 @@
     @Test
     fun testSurfaceControlIsValid_valid() {
         val callbackLatch = CountDownLatch(1)
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
         try {
             scenario.onActivity {
                 val callback =
@@ -250,7 +236,6 @@
                 it.addSurface(it.mSurfaceView, callback)
             }
 
-            scenario.moveToState(Lifecycle.State.RESUMED)
             assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
         } catch (e: java.lang.IllegalArgumentException) {
             fail()
@@ -263,9 +248,7 @@
     @Test
     fun testSurfaceControlIsValid_validNotValid() {
         val callbackLatch = CountDownLatch(1)
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
         try {
             scenario.onActivity {
                 val callback =
@@ -287,8 +270,6 @@
 
                 it.addSurface(it.mSurfaceView, callback)
             }
-
-            scenario.moveToState(Lifecycle.State.RESUMED)
             assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
         } catch (e: java.lang.IllegalArgumentException) {
             fail()
@@ -301,9 +282,7 @@
     @Test
     fun testSurfaceControlIsValid_multipleReleases() {
         val callbackLatch = CountDownLatch(1)
-        val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
+        val scenario = ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
         try {
             scenario.onActivity {
                 val callback =
@@ -327,7 +306,6 @@
                 it.addSurface(it.mSurfaceView, callback)
             }
 
-            scenario.moveToState(Lifecycle.State.RESUMED)
             assertTrue(callbackLatch.await(3000, TimeUnit.MILLISECONDS))
         } catch (e: java.lang.IllegalArgumentException) {
             fail()
@@ -1220,41 +1198,37 @@
         var scCompat: SurfaceControlCompat? = null
         var surfaceView: SurfaceView? = null
         val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
-                .onActivity {
-                    it.setDestroyCallback { destroyLatch.countDown() }
-                    surfaceView = it.mSurfaceView
-                    val callback =
-                        object : SurfaceHolderCallback() {
-                            override fun surfaceCreated(sh: SurfaceHolder) {
-                                scCompat =
-                                    SurfaceControlCompat.Builder()
-                                        .setParent(it.getSurfaceView())
-                                        .setName("SurfaceControlCompatTest")
-                                        .build()
+            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+                it.setDestroyCallback { destroyLatch.countDown() }
+                surfaceView = it.mSurfaceView
+                val callback =
+                    object : SurfaceHolderCallback() {
+                        override fun surfaceCreated(sh: SurfaceHolder) {
+                            scCompat =
+                                SurfaceControlCompat.Builder()
+                                    .setParent(it.getSurfaceView())
+                                    .setName("SurfaceControlCompatTest")
+                                    .build()
 
-                                // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
-                                val buffer =
-                                    SurfaceControlUtils.getSolidBuffer(
-                                        SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
-                                        SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
-                                        Color.BLUE
-                                    )
+                            // Buffer colorspace is RGBA, so Color.BLUE will be visually Red
+                            val buffer =
+                                SurfaceControlUtils.getSolidBuffer(
+                                    SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+                                    SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+                                    Color.BLUE
+                                )
 
-                                SurfaceControlCompat.Transaction()
-                                    .addTransactionCommittedListener(executor!!, listener)
-                                    .setBuffer(scCompat!!, buffer)
-                                    .setVisibility(scCompat!!, true)
-                                    .setCrop(scCompat!!, Rect(20, 30, 90, 60))
-                                    .commit()
-                            }
+                            SurfaceControlCompat.Transaction()
+                                .addTransactionCommittedListener(executor!!, listener)
+                                .setBuffer(scCompat!!, buffer)
+                                .setVisibility(scCompat!!, true)
+                                .setCrop(scCompat!!, Rect(20, 30, 90, 60))
+                                .commit()
                         }
+                    }
 
-                    it.addSurface(it.mSurfaceView, callback)
-                }
-
-        scenario.moveToState(Lifecycle.State.RESUMED)
+                it.addSurface(it.mSurfaceView, callback)
+            }
 
         assertTrue(listener.mLatch.await(3000, TimeUnit.MILLISECONDS))
 
@@ -1568,49 +1542,43 @@
     @SdkSuppress(minSdkVersion = Build.VERSION_CODES.Q)
     @Test
     fun testClearFrameRate() {
-        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-            .moveToState(Lifecycle.State.CREATED)
-            .onActivity {
-                val callback =
-                    object : SurfaceHolderCallback() {
-                        override fun surfaceCreated(sh: SurfaceHolder) {
+        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+            val callback =
+                object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
 
-                            val surfaceControl =
-                                SurfaceControlCompat.Builder()
-                                    .setName("testSurfaceControl")
-                                    .setParent(it.mSurfaceView)
-                                    .build()
-                            SurfaceControlCompat.Transaction()
-                                .clearFrameRate(surfaceControl)
-                                .commit()
-                        }
+                        val surfaceControl =
+                            SurfaceControlCompat.Builder()
+                                .setName("testSurfaceControl")
+                                .setParent(it.mSurfaceView)
+                                .build()
+                        SurfaceControlCompat.Transaction().clearFrameRate(surfaceControl).commit()
                     }
+                }
 
-                it.addSurface(it.mSurfaceView, callback)
-            }
+            it.addSurface(it.mSurfaceView, callback)
+        }
     }
 
     private fun testFrameRate(frameRate: Float, compatibility: Int, strategy: Int) {
-        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-            .moveToState(Lifecycle.State.CREATED)
-            .onActivity {
-                val callback =
-                    object : SurfaceHolderCallback() {
-                        override fun surfaceCreated(sh: SurfaceHolder) {
+        ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+            val callback =
+                object : SurfaceHolderCallback() {
+                    override fun surfaceCreated(sh: SurfaceHolder) {
 
-                            val surfaceControl =
-                                SurfaceControlCompat.Builder()
-                                    .setName("testSurfaceControl")
-                                    .setParent(it.mSurfaceView)
-                                    .build()
-                            SurfaceControlCompat.Transaction()
-                                .setFrameRate(surfaceControl, frameRate, compatibility, strategy)
-                                .commit()
-                        }
+                        val surfaceControl =
+                            SurfaceControlCompat.Builder()
+                                .setName("testSurfaceControl")
+                                .setParent(it.mSurfaceView)
+                                .build()
+                        SurfaceControlCompat.Transaction()
+                            .setFrameRate(surfaceControl, frameRate, compatibility, strategy)
+                            .commit()
                     }
+                }
 
-                it.addSurface(it.mSurfaceView, callback)
-            }
+            it.addSurface(it.mSurfaceView, callback)
+        }
     }
 
     @SuppressLint("NewApi")
@@ -1653,86 +1621,84 @@
     fun testSetExtendedRangeBrightness() {
         val destroyLatch = CountDownLatch(1)
         val scenario =
-            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java)
-                .moveToState(Lifecycle.State.CREATED)
-                .onActivity {
-                    it.setDestroyCallback { destroyLatch.countDown() }
-                    val display = it.display
-                    assertNotNull(display)
-                    if (display!!.isHdrSdrRatioAvailable) {
-                        assertEquals(1.0f, display.hdrSdrRatio, .0001f)
-                    }
+            ActivityScenario.launch(SurfaceControlWrapperTestActivity::class.java).onActivity {
+                it.setDestroyCallback { destroyLatch.countDown() }
+                val display = it.display
+                assertNotNull(display)
+                if (display!!.isHdrSdrRatioAvailable) {
+                    assertEquals(1.0f, display.hdrSdrRatio, .0001f)
+                }
 
-                    it.window.attributes.screenBrightness = 0.01f
-                    val hdrReady = CountDownLatch(1)
-                    val listenerErrors = arrayOfNulls<Exception>(1)
-                    if (display.isHdrSdrRatioAvailable) {
-                        display.registerHdrSdrRatioChangedListener(
-                            executor!!,
-                            object : Consumer<Display?> {
-                                var mIsRegistered = true
+                it.window.attributes.screenBrightness = 0.01f
+                val hdrReady = CountDownLatch(1)
+                val listenerErrors = arrayOfNulls<Exception>(1)
+                if (display.isHdrSdrRatioAvailable) {
+                    display.registerHdrSdrRatioChangedListener(
+                        executor!!,
+                        object : Consumer<Display?> {
+                            var mIsRegistered = true
 
-                                override fun accept(updatedDisplay: Display?) {
-                                    try {
-                                        assertEquals(display.displayId, updatedDisplay!!.displayId)
-                                        assertTrue(mIsRegistered)
-                                        if (display.hdrSdrRatio > 2f) {
-                                            hdrReady.countDown()
-                                            display.unregisterHdrSdrRatioChangedListener(this)
-                                            mIsRegistered = false
-                                        }
-                                    } catch (e: Exception) {
-                                        synchronized(it) {
-                                            listenerErrors[0] = e
-                                            hdrReady.countDown()
-                                        }
+                            override fun accept(updatedDisplay: Display?) {
+                                try {
+                                    assertEquals(display.displayId, updatedDisplay!!.displayId)
+                                    assertTrue(mIsRegistered)
+                                    if (display.hdrSdrRatio > 2f) {
+                                        hdrReady.countDown()
+                                        display.unregisterHdrSdrRatioChangedListener(this)
+                                        mIsRegistered = false
+                                    }
+                                } catch (e: Exception) {
+                                    synchronized(it) {
+                                        listenerErrors[0] = e
+                                        hdrReady.countDown()
                                     }
                                 }
                             }
+                        }
+                    )
+                } else {
+                    assertThrows(IllegalStateException::class.java) {
+                        display.registerHdrSdrRatioChangedListener(
+                            executor!!,
+                            Consumer { _: Display? -> }
                         )
-                    } else {
-                        assertThrows(IllegalStateException::class.java) {
-                            display.registerHdrSdrRatioChangedListener(
-                                executor!!,
-                                Consumer { _: Display? -> }
-                            )
+                    }
+                }
+                val extendedDataspace =
+                    DataSpace.pack(
+                        DataSpace.STANDARD_BT709,
+                        DataSpace.TRANSFER_SRGB,
+                        DataSpace.RANGE_EXTENDED
+                    )
+                val buffer =
+                    getSolidBuffer(
+                        SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
+                        SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
+                        Color.RED
+                    )
+                val callback =
+                    object : SurfaceHolderCallback() {
+                        override fun surfaceCreated(sh: SurfaceHolder) {
+                            val scCompat =
+                                SurfaceControlCompat.Builder()
+                                    .setParent(it.getSurfaceView())
+                                    .setName("SurfaceControlCompatTest")
+                                    .build()
+
+                            SurfaceControlCompat.Transaction()
+                                .setBuffer(scCompat, buffer)
+                                .setDataSpace(scCompat, extendedDataspace)
+                                .setExtendedRangeBrightness(scCompat, 1.0f, 3.0f)
+                                .setVisibility(scCompat, true)
+                                .commit()
                         }
                     }
-                    val extendedDataspace =
-                        DataSpace.pack(
-                            DataSpace.STANDARD_BT709,
-                            DataSpace.TRANSFER_SRGB,
-                            DataSpace.RANGE_EXTENDED
-                        )
-                    val buffer =
-                        getSolidBuffer(
-                            SurfaceControlWrapperTestActivity.DEFAULT_WIDTH,
-                            SurfaceControlWrapperTestActivity.DEFAULT_HEIGHT,
-                            Color.RED
-                        )
-                    val callback =
-                        object : SurfaceHolderCallback() {
-                            override fun surfaceCreated(sh: SurfaceHolder) {
-                                val scCompat =
-                                    SurfaceControlCompat.Builder()
-                                        .setParent(it.getSurfaceView())
-                                        .setName("SurfaceControlCompatTest")
-                                        .build()
 
-                                SurfaceControlCompat.Transaction()
-                                    .setBuffer(scCompat, buffer)
-                                    .setDataSpace(scCompat, extendedDataspace)
-                                    .setExtendedRangeBrightness(scCompat, 1.0f, 3.0f)
-                                    .setVisibility(scCompat, true)
-                                    .commit()
-                            }
-                        }
-
-                    it.addSurface(it.mSurfaceView, callback)
-                }
+                it.addSurface(it.mSurfaceView, callback)
+            }
 
         try {
-            scenario.moveToState(Lifecycle.State.RESUMED).onActivity {
+            scenario.onActivity {
                 SurfaceControlUtils.validateOutput(it.window) { bitmap ->
                     val coord = intArrayOf(0, 0)
                     it.mSurfaceView.getLocationInWindow(coord)
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
index 598c67e..2110acd 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/lowlatency/CanvasFrontBufferedRenderer.kt
@@ -70,13 +70,15 @@
 @JvmOverloads
 constructor(
     surfaceView: SurfaceView,
-    private val callback: Callback<T>,
+    callback: Callback<T>,
     @HardwareBufferFormat val bufferFormat: Int = HardwareBuffer.RGBA_8888
 ) {
 
     /** Target SurfaceView for rendering */
     private var mSurfaceView: SurfaceView? = null
 
+    private var mCallback: Callback<T>? = null
+
     /**
      * Executor used to deliver callbacks for rendering as well as issuing surface control
      * transactions
@@ -185,6 +187,7 @@
 
     init {
         mSurfaceView = surfaceView
+        mCallback = callback
         surfaceView.holder.addCallback(mHolderCallback)
         with(surfaceView.holder) {
             if (surface != null && surface.isValid) {
@@ -253,7 +256,7 @@
                                     }
                                     canvas.drawColor(Color.BLACK, BlendMode.CLEAR)
                                 }
-                                callback.onDrawFrontBufferedLayer(canvas, width, height, param)
+                                mCallback?.onDrawFrontBufferedLayer(canvas, width, height, param)
                             }
 
                             @SuppressLint("WrongConstant")
@@ -293,7 +296,7 @@
                                             transformHint
                                         )
                                     }
-                                    callback.onFrontBufferedLayerRenderComplete(
+                                    mCallback?.onFrontBufferedLayerRenderComplete(
                                         frontBufferSurfaceControl,
                                         transaction
                                     )
@@ -460,7 +463,7 @@
             if (transform != BufferTransformHintResolver.UNKNOWN_TRANSFORM) {
                 transaction.setBufferTransform(parentSurfaceControl, transform)
             }
-            callback.onMultiBufferedLayerRenderComplete(
+            mCallback?.onMultiBufferedLayerRenderComplete(
                 frontBufferSurfaceControl,
                 parentSurfaceControl,
                 transaction
@@ -566,7 +569,7 @@
                     with(multiBufferedRenderer) {
                         mMultiBufferedRenderNode?.let { renderNode ->
                             val canvas = renderNode.beginRecording()
-                            callback.onDrawMultiBufferedLayer(canvas, width, height, params)
+                            mCallback?.onDrawMultiBufferedLayer(canvas, width, height, params)
                             renderNode.endRecording()
                         }
 
@@ -671,6 +674,7 @@
             mSurfaceView?.holder?.removeCallback(mHolderCallback)
             mSurfaceView = null
             releaseInternal(cancelPending) {
+                mCallback = null
                 onReleaseComplete?.invoke()
                 mHandlerThread.quit()
             }
diff --git a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
index b06c22c..182f845 100644
--- a/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
+++ b/graphics/graphics-core/src/main/java/androidx/graphics/surface/SurfaceControlCompat.kt
@@ -530,12 +530,13 @@
             @ChangeFrameRateStrategy changeFrameRateStrategy: Int
         ): Transaction {
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
-                mImpl.setFrameRate(
-                    surfaceControl.scImpl,
-                    frameRate,
-                    compatibility,
-                    changeFrameRateStrategy
-                )
+                val strategy =
+                    when (changeFrameRateStrategy) {
+                        CHANGE_FRAME_RATE_ALWAYS -> changeFrameRateStrategy
+                        CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS -> changeFrameRateStrategy
+                        else -> CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS
+                    }
+                mImpl.setFrameRate(surfaceControl.scImpl, frameRate, compatibility, strategy)
             }
             return this
         }
diff --git a/health/connect/connect-client/build.gradle b/health/connect/connect-client/build.gradle
index 9b1779f..b212af5 100644
--- a/health/connect/connect-client/build.gradle
+++ b/health/connect/connect-client/build.gradle
@@ -77,8 +77,7 @@
     }
     testOptions.unitTests.includeAndroidResources = true
     namespace "androidx.health.connect.client"
-    compileSdk = 34
-    compileSdkExtension = 10
+    compileSdk = 35
     // TODO(b/352609562): Typedef with `toLong()`
     experimentalProperties["android.lint.useK2Uast"] = false
 }
diff --git a/health/connect/connect-client/samples/build.gradle b/health/connect/connect-client/samples/build.gradle
index 08b7839..fb7bcef 100644
--- a/health/connect/connect-client/samples/build.gradle
+++ b/health/connect/connect-client/samples/build.gradle
@@ -51,4 +51,5 @@
     defaultConfig {
         minSdkVersion 26
     }
+    compileSdk = 35
 }
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
index 14f303c..ee854d9 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/HealthConnectClientUpsideDownImplTest.kt
@@ -95,7 +95,7 @@
                 context.packageName,
                 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
             )
-            .requestedPermissions
+            .requestedPermissions!!
             .filter { it.startsWith(PERMISSION_PREFIX) }
             .toTypedArray()
 
diff --git a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
index 05fe60c..653ea0c 100644
--- a/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
+++ b/health/connect/connect-client/src/androidTest/java/androidx/health/connect/client/impl/platform/aggregate/HealthConnectClientAggregationExtensionsTest.kt
@@ -78,7 +78,7 @@
                 context.packageName,
                 PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS.toLong())
             )
-            .requestedPermissions
+            .requestedPermissions!!
             .filter { it.startsWith(PERMISSION_PREFIX) }
             .toTypedArray()
 
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a03c963..e38be6e 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -312,7 +312,7 @@
         @Suppress("Deprecation")
         packageInfo.versionCode = versionCode
         packageInfo.applicationInfo = ApplicationInfo()
-        packageInfo.applicationInfo.enabled = enabled
+        packageInfo.applicationInfo!!.enabled = enabled
         val packageManager = context.packageManager
         Shadows.shadowOf(packageManager).installPackage(packageInfo)
     }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
index dea6c6b..47d6fb9 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/feature/HealthConnectApkImplTest.kt
@@ -121,7 +121,7 @@
         @Suppress("Deprecation")
         packageInfo.versionCode = versionCode
         packageInfo.applicationInfo = ApplicationInfo()
-        packageInfo.applicationInfo.enabled = true
+        packageInfo.applicationInfo!!.enabled = true
         val packageManager = context.packageManager
         Shadows.shadowOf(packageManager).installPackage(packageInfo)
     }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
index f1480f8..2bb9f0c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/HealthConnectClientImplTest.kt
@@ -841,7 +841,7 @@
         val packageInfo = PackageInfo()
         packageInfo.packageName = packageName
         packageInfo.applicationInfo = ApplicationInfo()
-        packageInfo.applicationInfo.enabled = enabled
+        packageInfo.applicationInfo!!.enabled = enabled
         val packageManager = context.packageManager
         Shadows.shadowOf(packageManager).installPackage(packageInfo)
     }
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
index 609c83a5..ff2ea5c 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClientTest.kt
@@ -186,7 +186,7 @@
         val packageInfo = PackageInfo()
         packageInfo.packageName = packageName
         packageInfo.applicationInfo = ApplicationInfo()
-        packageInfo.applicationInfo.enabled = enabled
+        packageInfo.applicationInfo!!.enabled = enabled
         val packageManager = context.packageManager
         shadowOf(packageManager).installPackage(packageInfo)
     }
diff --git a/health/connect/connect-testing/build.gradle b/health/connect/connect-testing/build.gradle
index 9439dd3..1c1c2b5 100644
--- a/health/connect/connect-testing/build.gradle
+++ b/health/connect/connect-testing/build.gradle
@@ -47,8 +47,7 @@
     }
     namespace "androidx.health.connect.testing"
     testOptions.unitTests.includeAndroidResources = true
-    compileSdk = 34
-    compileSdkExtension = 10
+    compileSdk = 35
 }
 
 androidx {
diff --git a/health/connect/connect-testing/samples/build.gradle b/health/connect/connect-testing/samples/build.gradle
index c93f7cc..a9cb0e5 100644
--- a/health/connect/connect-testing/samples/build.gradle
+++ b/health/connect/connect-testing/samples/build.gradle
@@ -51,6 +51,7 @@
     defaultConfig {
         minSdkVersion 26
     }
+    compileSdk = 35
 }
 
 tasks.withType(KotlinCompile).configureEach {
diff --git a/ink/ink-brush/api/current.txt b/ink/ink-brush/api/current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/current.txt
+++ b/ink/ink-brush/api/current.txt
@@ -57,6 +57,16 @@
   public static final class BrushFamily.Companion {
   }
 
+  public final class BrushUtil {
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
   }
 
diff --git a/ink/ink-brush/api/restricted_current.txt b/ink/ink-brush/api/restricted_current.txt
index 7582fcf..7599e2b 100644
--- a/ink/ink-brush/api/restricted_current.txt
+++ b/ink/ink-brush/api/restricted_current.txt
@@ -57,6 +57,16 @@
   public static final class BrushFamily.Companion {
   }
 
+  public final class BrushUtil {
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush copyWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color, optional androidx.ink.brush.BrushFamily family, optional float size, optional float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static android.graphics.Color createAndroidColor(androidx.ink.brush.Brush);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder createBuilderWithAndroidColor(android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.Brush.Companion, androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush createWithAndroidColor(androidx.ink.brush.BrushFamily family, android.graphics.Color color, float size, float epsilon);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder setAndroidColor(androidx.ink.brush.Brush.Builder, android.graphics.Color color);
+    method @CheckResult @RequiresApi(android.os.Build.VERSION_CODES.O) public static androidx.ink.brush.Brush.Builder toBuilderWithAndroidColor(androidx.ink.brush.Brush, android.graphics.Color color);
+  }
+
   @SuppressCompatibility @kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level.ERROR) @kotlin.annotation.MustBeDocumented @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention.BINARY) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget.CLASS, kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS, kotlin.annotation.AnnotationTarget.PROPERTY, kotlin.annotation.AnnotationTarget.FIELD, kotlin.annotation.AnnotationTarget.LOCAL_VARIABLE, kotlin.annotation.AnnotationTarget.VALUE_PARAMETER, kotlin.annotation.AnnotationTarget.CONSTRUCTOR, kotlin.annotation.AnnotationTarget.FUNCTION, kotlin.annotation.AnnotationTarget.PROPERTY_GETTER, kotlin.annotation.AnnotationTarget.PROPERTY_SETTER, kotlin.annotation.AnnotationTarget.TYPEALIAS}) public @interface ExperimentalInkCustomBrushApi {
   }
 
diff --git a/ink/ink-brush/build.gradle b/ink/ink-brush/build.gradle
index 2483290..4679548 100644
--- a/ink/ink-brush/build.gradle
+++ b/ink/ink-brush/build.gradle
@@ -100,5 +100,4 @@
     type = LibraryType.PUBLISHED_LIBRARY
     inceptionYear = "2024"
     description = "Define brushes for freehand input."
-    metalavaK2UastEnabled = false
 }
diff --git a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
index b23e689..1a277fe 100644
--- a/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
+++ b/ink/ink-brush/src/androidInstrumentedTest/kotlin/androidx/ink/brush/BrushExtensionsTest.kt
@@ -41,7 +41,7 @@
     private val testFamily = BrushFamily(uri = "/brush-family:pencil")
 
     @Test
-    fun brushGetAndroidColor_getsCorrectColor() {
+    fun brushCreateAndroidColor_getsCorrectColor() {
         val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
 
         // Note that expectedColor is not necessarily the same as testColor, because of precision
@@ -50,7 +50,7 @@
         // the
         // color internally as a ColorLong anyway).
         val expectedColor = AndroidColor.valueOf(testColorLong)
-        assertThat(brush.getAndroidColor()).isEqualTo(expectedColor)
+        assertThat(brush.createAndroidColor()).isEqualTo(expectedColor)
     }
 
     @Test
@@ -97,7 +97,7 @@
     }
 
     @Test
-    fun brushBuilderAndroidColor_setsColor() {
+    fun brushBuilderSetAndroidColor_setsColor() {
         val brush =
             Brush.builder()
                 .setFamily(testFamily)
@@ -110,7 +110,7 @@
     }
 
     @Test
-    fun brushBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun brushBuilderSetAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush =
             Brush.builder()
@@ -126,13 +126,13 @@
     }
 
     @Test
-    fun brushWithAndroidColor_createsBrushWithColor() {
+    fun brushCreateWithAndroidColor_createsBrushWithColor() {
         val brush = Brush.createWithAndroidColor(testFamily, testColor, 1f, 1f)
         assertThat(brush.colorLong).isEqualTo(testColorLong)
     }
 
     @Test
-    fun brushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
+    fun brushCreateWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush = Brush.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
 
@@ -142,21 +142,10 @@
     }
 
     @Test
-    fun brushUtilGetAndroidColor_getsCorrectColor() {
-        val brush = Brush.createWithColorLong(testFamily, testColorLong, 1f, 1f)
-
-        // Note that expectedColor is not necessarily the same as testColor, because of precision
-        // loss
-        // when converting from testColor to testColorLong.
-        val expectedColor = AndroidColor.valueOf(testColorLong)
-        assertThat(BrushUtil.getAndroidColor(brush)).isEqualTo(expectedColor)
-    }
-
-    @Test
-    fun brushUtilToBuilderWithAndroidColor_setsColor() {
+    fun brushToBuilderWithAndroidColor_setsColor() {
         val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
 
-        val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, testColor).build()
+        val newBrush = brush.toBuilderWithAndroidColor(testColor).build()
 
         assertThat(newBrush.colorLong).isEqualTo(testColorLong)
         assertThat(brush.family).isEqualTo(testFamily)
@@ -165,11 +154,11 @@
     }
 
     @Test
-    fun brushUtilToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun brushToBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val brush = Brush.createWithColorIntArgb(testFamily, 0x4499bb66, 2f, 0.2f)
 
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
-        val newBrush = BrushUtil.toBuilderWithAndroidColor(brush, unsupportedColor).build()
+        val newBrush = brush.toBuilderWithAndroidColor(unsupportedColor).build()
 
         // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
         val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
@@ -181,9 +170,9 @@
     }
 
     @Test
-    fun brushUtilMakeBuilderWithAndroidColor_setsColor() {
+    fun createBrushBuilderWithAndroidColor_setsColor() {
         val brush =
-            BrushUtil.createBuilderWithAndroidColor(testColor)
+            createBrushBuilderWithAndroidColor(testColor)
                 .setFamily(testFamily)
                 .setSize(2f)
                 .setEpsilon(0.2f)
@@ -196,10 +185,10 @@
     }
 
     @Test
-    fun brushUtilMakeBuilderAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
+    fun createBrushBuilderWithAndroidColor_withUnsupportedColorSpace_setsConvertedColor() {
         val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
         val brush =
-            BrushUtil.createBuilderWithAndroidColor(unsupportedColor)
+            createBrushBuilderWithAndroidColor(unsupportedColor)
                 .setFamily(testFamily)
                 .setSize(2f)
                 .setEpsilon(0.2f)
@@ -209,20 +198,4 @@
         val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
         assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
     }
-
-    @Test
-    fun brushUtilMakeBrushWithAndroidColor_createsBrushWithColor() {
-        val brush = BrushUtil.createWithAndroidColor(testFamily, testColor, 1f, 1f)
-        assertThat(brush.colorLong).isEqualTo(testColorLong)
-    }
-
-    @Test
-    fun brushUtilMakeBrushWithAndroidColor_withUnsupportedColorSpace_createsBrushWithConvertedColor() {
-        val unsupportedColor = AndroidColor.valueOf(0.6f, 0.7f, 0.4f, 0.3f, adobeRgb)
-        val brush = BrushUtil.createWithAndroidColor(testFamily, unsupportedColor, 1f, 1f)
-
-        // unsupportedColor gets converted to ColorLong (losing precision) and then to Display P3.
-        val expectedColor = AndroidColor.valueOf(unsupportedColor.pack()).convert(displayP3)
-        assertThat(brush.colorLong).isEqualTo(expectedColor.pack())
-    }
 }
diff --git a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
index a1a5bdc..ff89d66 100644
--- a/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
+++ b/ink/ink-brush/src/androidMain/kotlin/androidx/ink/brush/BrushExtensions.android.kt
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-@file:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
+@file:JvmName("BrushUtil")
 
 package androidx.ink.brush
 
@@ -22,17 +22,20 @@
 import android.os.Build
 import androidx.annotation.CheckResult
 import androidx.annotation.RequiresApi
-import androidx.annotation.RestrictTo
 
 /**
  * The brush color as an [android.graphics.Color] instance, which can express colors in several
  * different color spaces. sRGB and Display P3 are supported; a color in any other color space will
  * be converted to Display P3.
+ *
+ * Unless an instance of [android.graphics.Color] is actually needed, prefer to use
+ * [Brush.colorLong] to get the color without causing an allocation, especially in
+ * performance-sensitive code. [Brush.colorLong] is fully compatible with the [Long] representation
+ * of [android.graphics.Color].
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
-public fun Brush.getAndroidColor(): AndroidColor = BrushUtil.getAndroidColor(this)
+public fun Brush.createAndroidColor(): AndroidColor = AndroidColor.valueOf(colorLong)
 
 /**
  * Creates a copy of `this` [Brush] and allows named properties to be altered while keeping the rest
@@ -40,7 +43,6 @@
  * several different color spaces. sRGB and Display P3 are supported; a color in any other color
  * space will be converted to Display P3.
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.copyWithAndroidColor(
@@ -53,19 +55,53 @@
 /**
  * Set the color on a [Brush.Builder] as an [android.graphics.Color] instance. sRGB and Display P3
  * are supported; a color in any other color space will be converted to Display P3.
+ *
+ * Java callers should prefer [toBuilderWithAndroidColor] or [createBrushBuilderWithAndroidColor] as
+ * a more fluent API.
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.Builder.setAndroidColor(color: AndroidColor): Brush.Builder =
     setColorLong(color.pack())
 
 /**
+ * Returns a [Brush.Builder] with values set equivalent to the [Brush] and the color specified by an
+ * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
+ * Display P3 are supported; a color in any other color space will be converted to Display P3. Java
+ * developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
+ * [copyWithAndroidColor] method.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.toBuilder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun Brush.toBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+    toBuilder().setAndroidColor(color)
+
+/**
+ * Returns a new, blank [Brush.Builder] with the color specified by an [android.graphics.Color]
+ * instance, which can encode several different color spaces. sRGB and Display P3 are supported; a
+ * color in any other color space will be converted to Display P3.
+ *
+ * In Kotlin, calling this is equivalent to calling [Brush.builder] followed by
+ * [Brush.Builder.setAndroidColor]. For Java callers, this function allows more fluent call
+ * chaining.
+ */
+@JvmName("createBuilderWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
+    Brush.Builder().setAndroidColor(color)
+
+/**
  * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
  * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
  * color space will be converted to Display P3.
+ *
+ * Java callers should prefer `BrushUtil.createWithAndroidColor` ([createBrushWithAndroidColor]).
  */
-@JvmSynthetic
 @CheckResult
 @RequiresApi(Build.VERSION_CODES.O)
 public fun Brush.Companion.createWithAndroidColor(
@@ -73,57 +109,21 @@
     color: AndroidColor,
     size: Float,
     epsilon: Float,
-): Brush = BrushUtil.createWithAndroidColor(family, color, size, epsilon)
+): Brush = createWithColorLong(family, color.pack(), size, epsilon)
 
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-public object BrushUtil {
-
-    /**
-     * The brush color as an [android.graphics.Color] instance, which can express colors in several
-     * different color spaces. sRGB and Display P3 are supported; a color in any other color space
-     * will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun getAndroidColor(brush: Brush): AndroidColor = AndroidColor.valueOf(brush.colorLong)
-
-    /**
-     * Returns a [Brush.Builder] with values set equivalent to [brush] and the color specified by an
-     * [android.graphics.Color] instance, which can encode several different color spaces. sRGB and
-     * Display P3 are supported; a color in any other color space will be converted to Display P3.
-     * Java developers, use the returned builder to build a copy of a Brush. Kotlin developers, see
-     * [copyWithAndroidColor] method.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun toBuilderWithAndroidColor(brush: Brush, color: AndroidColor): Brush.Builder =
-        brush.toBuilder().setAndroidColor(color)
-
-    /**
-     * Returns a new [Brush.Builder] with the color specified by an [android.graphics.Color]
-     * instance, which can encode several different color spaces. sRGB and Display P3 are supported;
-     * a color in any other color space will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun createBuilderWithAndroidColor(color: AndroidColor): Brush.Builder =
-        Brush.Builder().setAndroidColor(color)
-
-    /**
-     * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which
-     * can encode several different color spaces. sRGB and Display P3 are supported; a color in any
-     * other color space will be converted to Display P3.
-     */
-    @JvmStatic
-    @CheckResult
-    @RequiresApi(Build.VERSION_CODES.O)
-    public fun createWithAndroidColor(
-        family: BrushFamily,
-        color: AndroidColor,
-        size: Float,
-        epsilon: Float,
-    ): Brush = Brush.createWithColorLong(family, color.pack(), size, epsilon)
-}
+/**
+ * Returns a new [Brush] with the color specified by an [android.graphics.Color] instance, which can
+ * encode several different color spaces. sRGB and Display P3 are supported; a color in any other
+ * color space will be converted to Display P3.
+ *
+ * Kotlin callers should prefer [Brush.Companion.createWithAndroidColor].
+ */
+@JvmName("createWithAndroidColor")
+@CheckResult
+@RequiresApi(Build.VERSION_CODES.O)
+public fun createBrushWithAndroidColor(
+    family: BrushFamily,
+    color: AndroidColor,
+    size: Float,
+    epsilon: Float,
+): Brush = Brush.createWithAndroidColor(family, color, size, epsilon)
diff --git a/ink/ink-geometry/api/current.txt b/ink/ink-geometry/api/current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/current.txt
+++ b/ink/ink-geometry/api/current.txt
@@ -65,6 +65,7 @@
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+    method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
   public final class Intersection {
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+    method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
   public static final class Parallelogram.Companion {
   }
 
+  public final class PartitionedMesh {
+    method public androidx.ink.geometry.Box? computeBoundingBox();
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method protected void finalize();
+    method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+    method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+    method @IntRange(from=0L) public int getRenderGroupCount();
+    method public void initializeSpatialIndex();
+    method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+    field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+  }
+
+  public static final class PartitionedMesh.Companion {
+  }
+
   public abstract class Segment {
     method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
     method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/api/restricted_current.txt b/ink/ink-geometry/api/restricted_current.txt
index 7d352f46..8901f35 100644
--- a/ink/ink-geometry/api/restricted_current.txt
+++ b/ink/ink-geometry/api/restricted_current.txt
@@ -65,6 +65,7 @@
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Box? box);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.BoxAccumulator? other);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Parallelogram parallelogram);
+    method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.PartitionedMesh mesh);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Segment segment);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Triangle triangle);
     method public androidx.ink.geometry.BoxAccumulator add(androidx.ink.geometry.Vec point);
@@ -172,26 +173,37 @@
   public final class Intersection {
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Box other);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToBox);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Box, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Parallelogram other);
+    method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToParallelogram);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Parallelogram, androidx.ink.geometry.Vec point);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Box box, androidx.ink.geometry.AffineTransform meshToBox);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Parallelogram parallelogram, androidx.ink.geometry.AffineTransform meshToParallelogram);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.PartitionedMesh other, androidx.ink.geometry.AffineTransform thisToCommonTransForm, androidx.ink.geometry.AffineTransform otherToCommonTransform);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Segment segment, androidx.ink.geometry.AffineTransform meshToSegment);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Triangle triangle, androidx.ink.geometry.AffineTransform meshToTriangle);
+    method public static boolean intersects(androidx.ink.geometry.PartitionedMesh, androidx.ink.geometry.Vec point, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToSegment);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Segment other);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Segment, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToTriangle);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Triangle other);
     method public static boolean intersects(androidx.ink.geometry.Triangle, androidx.ink.geometry.Vec point);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Box box);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Parallelogram parallelogram);
+    method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.PartitionedMesh mesh, androidx.ink.geometry.AffineTransform meshToPoint);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Segment segment);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Triangle triangle);
     method public static boolean intersects(androidx.ink.geometry.Vec, androidx.ink.geometry.Vec other);
@@ -313,6 +325,36 @@
   public static final class Parallelogram.Companion {
   }
 
+  public final class PartitionedMesh {
+    method public androidx.ink.geometry.Box? computeBoundingBox();
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Box box, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Parallelogram parallelogram, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.PartitionedMesh other, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle);
+    method @FloatRange(from=0.0, to=1.0) public float computeCoverage(androidx.ink.geometry.Triangle triangle, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Box box, float coverageThreshold, optional androidx.ink.geometry.AffineTransform boxToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Parallelogram parallelogram, float coverageThreshold, optional androidx.ink.geometry.AffineTransform parallelogramToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.PartitionedMesh other, float coverageThreshold, optional androidx.ink.geometry.AffineTransform otherShapeToThis);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold);
+    method public boolean computeCoverageIsGreaterThan(androidx.ink.geometry.Triangle triangle, float coverageThreshold, optional androidx.ink.geometry.AffineTransform triangleToThis);
+    method protected void finalize();
+    method @IntRange(from=0L) public int getOutlineCount(@IntRange(from=0L) int groupIndex);
+    method @IntRange(from=0L) public int getOutlineVertexCount(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex);
+    method @IntRange(from=0L) public int getRenderGroupCount();
+    method public void initializeSpatialIndex();
+    method public androidx.ink.geometry.MutableVec populateOutlinePosition(@IntRange(from=0L) int groupIndex, @IntRange(from=0L) int outlineIndex, @IntRange(from=0L) int outlineVertexIndex, androidx.ink.geometry.MutableVec outPosition);
+    field public static final androidx.ink.geometry.PartitionedMesh.Companion Companion;
+  }
+
+  public static final class PartitionedMesh.Companion {
+  }
+
   public abstract class Segment {
     method public final androidx.ink.geometry.ImmutableBox computeBoundingBox();
     method public final androidx.ink.geometry.MutableBox computeBoundingBox(androidx.ink.geometry.MutableBox outBox);
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
index 79501d8..dde4d23 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/BoxAccumulator.kt
@@ -227,8 +227,7 @@
      *
      * @return `this`
      */
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
-    public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.bounds)
+    public fun add(mesh: PartitionedMesh): BoxAccumulator = this.add(mesh.computeBoundingBox())
 
     /**
      * Compares this [BoxAccumulator] with [other], and returns true if either: Both this and
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
index 1ee275d..fcdf2f5 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/Intersection.kt
@@ -16,7 +16,6 @@
 
 package androidx.ink.geometry
 
-import androidx.annotation.RestrictTo
 import androidx.ink.nativeloader.NativeLoader
 
 /**
@@ -116,7 +115,6 @@
      * intersection of the point in [mesh]’s object coordinates.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Vec.intersects(mesh: PartitionedMesh, meshToPoint: AffineTransform): Boolean {
         return nativeMeshVecIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -218,7 +216,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Segment.intersects(mesh: PartitionedMesh, meshToSegment: AffineTransform): Boolean {
         return nativeMeshSegmentIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -311,7 +308,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Triangle.intersects(
         mesh: PartitionedMesh,
         meshToTriangle: AffineTransform
@@ -382,7 +378,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Box.intersects(mesh: PartitionedMesh, meshToBox: AffineTransform): Boolean {
         return nativeMeshBoxIntersects(
             nativeMeshAddress = mesh.getNativeAddress(),
@@ -436,7 +431,6 @@
      * checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun Parallelogram.intersects(
         mesh: PartitionedMesh,
         meshToParallelogram: AffineTransform,
@@ -467,7 +461,6 @@
      * coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         other: PartitionedMesh,
         thisToCommonTransForm: AffineTransform,
@@ -566,7 +559,6 @@
      * intersection of the point in [mesh]’s object coordinates.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(point: Vec, meshToPoint: AffineTransform): Boolean =
         point.intersects(this, meshToPoint)
 
@@ -578,7 +570,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         segment: Segment,
         meshToSegment: AffineTransform
@@ -592,7 +583,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         triangle: Triangle,
         meshToTriangle: AffineTransform,
@@ -606,7 +596,6 @@
      * coordinate space to the coordinate space that the intersection should be checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(box: Box, meshToBox: AffineTransform): Boolean =
         box.intersects(this, meshToBox)
 
@@ -619,7 +608,6 @@
      * checked in.
      */
     @JvmStatic
-    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
     public fun PartitionedMesh.intersects(
         parallelogram: Parallelogram,
         meshToParallelogram: AffineTransform,
diff --git a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
index d279937..6f186f6 100644
--- a/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
+++ b/ink/ink-geometry/src/jvmAndroidMain/kotlin/androidx/ink/geometry/PartitionedMesh.kt
@@ -25,23 +25,22 @@
 import androidx.ink.nativeloader.NativeLoader
 
 /**
- * An immutable† complex shape expressed as a set of triangles. This is used to represent the shape
- * of a stroke or other complex objects see [MeshCreation]. The mesh may be divided into multiple
- * partitions, which enables certain brush effects (e.g. "multi-coat"), and allows ink to create
- * strokes requiring greater than 216 triangles (which must be rendered in multiple passes).
+ * An immutable** complex shape expressed as a set of triangles. This is used to represent the shape
+ * of a stroke or other complex objects. The mesh may be divided into multiple partitions, which
+ * enables certain brush effects (e.g. "multi-coat"), and allows strokes to be created using greater
+ * than 2^16 triangles (which must be rendered in multiple passes).
  *
- * A PartitionedMesh may optionally have one or more "outlines", which are polylines that traverse
+ * A [PartitionedMesh] may optionally have one or more "outlines", which are polylines that traverse
  * some or all of the vertices in the mesh; these are used for path-based rendering of strokes. This
  * supports disjoint meshes such as dashed lines.
  *
- * PartitionedMesh provides fast intersection and coverage testing by use of an internal spatial
+ * [PartitionedMesh] provides fast intersection and coverage testing by use of an internal spatial
  * index.
  *
- * † PartitionedMesh is technically not immutable, as the spatial index is lazily instantiated;
+ * ** [PartitionedMesh] is technically not immutable, as the spatial index is lazily instantiated;
  * however, from the perspective of a caller, its properties do not change over the course of its
  * lifetime. The entire object is thread-safe.
  */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // PublicApiNotReadyForJetpackReview
 @Suppress("NotCloseable") // Finalize is only used to free the native peer.
 public class PartitionedMesh
 /** Only for use within the ink library. Constructs a [PartitionedMesh] from native pointer. */
@@ -73,42 +72,53 @@
     @VisibleForTesting internal constructor() : this(ModeledShapeNative.alloc())
 
     /**
-     * The number of render groups in this mesh. Each outline in the [PartitionedMesh] belongs to
-     * exactly one render group, which are numbered in z-order: the group with index zero should be
-     * rendered on bottom; the group with the highest index should be rendered on top.
+     * Returns the number of render groups in this mesh. Each outline in the [PartitionedMesh]
+     * belongs to exactly one render group, which are numbered in z-order: the group with index zero
+     * should be rendered on bottom; the group with the highest index should be rendered on top.
      */
     @IntRange(from = 0)
-    public val renderGroupCount: Int =
+    public fun getRenderGroupCount(): Int =
         ModeledShapeNative.getRenderGroupCount(nativeAddress).also { check(it >= 0) }
 
     /** The [Mesh] objects that make up this shape. */
     private val meshesByGroup: List<List<Mesh>> = buildList {
-        for (groupIndex in 0 until renderGroupCount) {
+        for (groupIndex in 0 until getRenderGroupCount()) {
             val nativeAddressesOfMeshes =
                 ModeledShapeNative.getNativeAddressesOfMeshes(nativeAddress, groupIndex)
             add(nativeAddressesOfMeshes.map(::Mesh))
         }
     }
 
+    private var _bounds: Box? = null
+
     /**
-     * The minimum bounding box of the [PartitionedMesh]. This will be null if the [PartitionedMesh]
-     * is empty.
+     * Returns the minimum bounding box of the [PartitionedMesh]. This will be null if the
+     * [PartitionedMesh] is empty.
      */
-    public val bounds: Box? = run {
+    public fun computeBoundingBox(): Box? {
+        // If we've already computed the bounding box, re-use it -- it won't change over the
+        // lifetime of
+        // this object.
+        if (_bounds != null) return _bounds
+
+        // If we have no meshes, then the bounding box is null.
+        if (meshesByGroup.isEmpty()) return null
+
         val envelope = BoxAccumulator()
         for (meshes in meshesByGroup) {
             for (mesh in meshes) {
                 envelope.add(mesh.bounds)
             }
         }
-        envelope.box
+        _bounds = envelope.box
+        return envelope.box
     }
 
     /** Returns the [MeshFormat] used for each [Mesh] in the specified render group. */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
     public fun renderGroupFormat(@IntRange(from = 0) groupIndex: Int): MeshFormat {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return MeshFormat(ModeledShapeNative.getRenderGroupFormat(nativeAddress, groupIndex))
     }
@@ -119,51 +129,51 @@
      */
     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) // NonPublicApi
     public fun renderGroupMeshes(@IntRange(from = 0) groupIndex: Int): List<Mesh> {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return meshesByGroup[groupIndex]
     }
 
-    /** The number of outlines that comprise this shape. */
+    /** Returns the number of outlines that comprise this shape. */
     @IntRange(from = 0)
-    public fun outlineCount(@IntRange(from = 0) groupIndex: Int): Int {
-        require(groupIndex >= 0 && groupIndex < renderGroupCount) {
-            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${renderGroupCount}"
+    public fun getOutlineCount(@IntRange(from = 0) groupIndex: Int): Int {
+        require(groupIndex >= 0 && groupIndex < getRenderGroupCount()) {
+            "groupIndex=$groupIndex must be between 0 and renderGroupCount=${getRenderGroupCount()}"
         }
         return ModeledShapeNative.getOutlineCount(nativeAddress, groupIndex).also { check(it >= 0) }
     }
 
     /**
-     * The number of vertices that are in the outline at index [outlineIndex], and within the render
-     * group at [groupIndex].
+     * Returns the number of vertices that are in the outline at index [outlineIndex], and within
+     * the render group at [groupIndex].
      */
     @IntRange(from = 0)
-    public fun outlineVertexCount(
+    public fun getOutlineVertexCount(
         @IntRange(from = 0) groupIndex: Int,
         @IntRange(from = 0) outlineIndex: Int,
     ): Int {
-        require(outlineIndex >= 0 && outlineIndex < outlineCount(groupIndex)) {
-            "outlineIndex=$outlineIndex must be between 0 and outlineCount=${outlineCount(groupIndex)}"
+        require(outlineIndex >= 0 && outlineIndex < getOutlineCount(groupIndex)) {
+            "outlineIndex=$outlineIndex must be between 0 and outlineCount=${getOutlineCount(groupIndex)}"
         }
         return ModeledShapeNative.getOutlineVertexCount(nativeAddress, groupIndex, outlineIndex)
             .also { check(it >= 0) }
     }
 
     /**
-     * Retrieve the outline vertex position from the outline at index [outlineIndex] (which can be
-     * up to, but not including, [outlineCount]), and the vertex from within that outline at index
-     * [outlineVertexIndex] (which can be up to, but not including, the result of calling
-     * [outlineVertexCount] with [outlineIndex]). The resulting x/y position of that outline vertex
-     * will be put into [outPosition], which can be pre-allocated and reused to avoid allocations.
+     * Populates [outPosition] with the position of the outline vertex at [outlineVertexIndex] in
+     * the outline at [outlineIndex] in the render group at [groupIndex], and returns [outPosition].
+     * [groupIndex] must be less than [getRenderGroupCount], [outlineIndex] must be less
+     * [getOutlineVertexCount] for [groupIndex], and [outlineVertexIndex] must be less than
+     * [getOutlineVertexCount] for [groupIndex] and [outlineIndex].
      */
     public fun populateOutlinePosition(
         @IntRange(from = 0) groupIndex: Int,
         @IntRange(from = 0) outlineIndex: Int,
         @IntRange(from = 0) outlineVertexIndex: Int,
         outPosition: MutableVec,
-    ) {
-        val outlineVertexCount = outlineVertexCount(groupIndex, outlineIndex)
+    ): MutableVec {
+        val outlineVertexCount = getOutlineVertexCount(groupIndex, outlineIndex)
         require(outlineVertexIndex >= 0 && outlineVertexIndex < outlineVertexCount) {
             "outlineVertexIndex=$outlineVertexIndex must be between 0 and " +
                 "outlineVertexCount($outlineVertexIndex)=$outlineVertexCount"
@@ -178,6 +188,7 @@
         val (meshIndex, meshVertexIndex) = scratchIntArray
         val mesh = meshesByGroup[groupIndex][meshIndex]
         mesh.fillPosition(meshVertexIndex, outPosition)
+        return outPosition
     }
 
     /**
@@ -187,18 +198,18 @@
      * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         triangle: Triangle,
         triangleToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -225,17 +236,20 @@
      * [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space. Triangles in the
      * [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that loops back over
      * itself) are counted individually. Note that, if any triangles have negative area (due to
-     * winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute value of their
-     * area will be used instead.
+     * winding, see [Triangle.computeSignedArea]), the absolute value of their area will be used
+     * instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
-     * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+     * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(box: Box, boxToThis: AffineTransform = AffineTransform.IDENTITY): Float =
+    public fun computeCoverage(
+        box: Box,
+        boxToThis: AffineTransform = AffineTransform.IDENTITY
+    ): Float =
         ModeledShapeNative.modeledShapeBoxCoverage(
             nativeAddress = nativeAddress,
             boxXMin = box.xMin,
@@ -257,18 +271,18 @@
      * of all triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [parallelogramToThis] contains the transform that maps from
      * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
-     * defaults to the [IDENTITY].
+     * defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         parallelogram: Parallelogram,
         parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -295,18 +309,18 @@
      * triangles in the [PartitionedMesh], all in the [PartitionedMesh]'s coordinate space.
      * Triangles in the [PartitionedMesh] that overlap each other (e.g. in the case of a stroke that
      * loops back over itself) are counted individually. Note that, if any triangles have negative
-     * area (due to winding, see [com.google.inputmethod.ink.Triangle.signedArea]), the absolute
-     * value of their area will be used instead.t
+     * area (due to winding, see [Triangle.computeSignedArea]), the absolute value of their area
+     * will be used instead.
      *
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
     @FloatRange(from = 0.0, to = 1.0)
-    public fun coverage(
+    public fun computeCoverage(
         other: PartitionedMesh,
         otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
     ): Float =
@@ -327,7 +341,7 @@
      *
      * This is equivalent to:
      * ```
-     * this.coverage(triangle, triangleToThis) > coverageThreshold
+     * computeCoverage(triangle, triangleToThis) > coverageThreshold
      * ```
      *
      * but may be faster.
@@ -335,11 +349,11 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [triangleToThis] contains the transform that maps from [triangle]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         triangle: Triangle,
         coverageThreshold: Float,
         triangleToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -375,10 +389,10 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [boxToThis] contains the transform that maps from [box]'s coordinate space
-     * to this [PartitionedMesh]'s coordinate space, which defaults to the [IDENTITY].
+     * to this [PartitionedMesh]'s coordinate space, which defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         box: Box,
         coverageThreshold: Float,
         boxToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -413,10 +427,10 @@
      *
      * Optional argument [parallelogramToThis] contains the transform that maps from
      * [parallelogram]'s coordinate space to this [PartitionedMesh]'s coordinate space, which
-     * defaults to the [IDENTITY].
+     * defaults to [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         parallelogram: Parallelogram,
         coverageThreshold: Float,
         parallelogramToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -452,11 +466,11 @@
      * On an empty [PartitionedMesh], this will always return 0.
      *
      * Optional argument [otherShapeToThis] contains the transform that maps from [other]'s
-     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to the
-     * [IDENTITY].
+     * coordinate space to this [PartitionedMesh]'s coordinate space, which defaults to
+     * [AffineTransform.IDENTITY].
      */
     @JvmOverloads
-    public fun coverageIsGreaterThan(
+    public fun computeCoverageIsGreaterThan(
         other: PartitionedMesh,
         coverageThreshold: Float,
         otherShapeToThis: AffineTransform = AffineTransform.IDENTITY,
@@ -488,7 +502,7 @@
 
     override fun toString(): String {
         val address = java.lang.Long.toHexString(nativeAddress)
-        return "PartitionedMesh(bounds=$bounds, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
+        return "PartitionedMesh(bounds=${computeBoundingBox()}, meshesByGroup=$meshesByGroup, nativeAddress=$address)"
     }
 
     protected fun finalize() {
diff --git a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
index 10bf22b..50106fe 100644
--- a/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
+++ b/ink/ink-geometry/src/jvmAndroidTest/kotlin/androidx/ink/geometry/PartitionedMeshTest.kt
@@ -31,35 +31,35 @@
 class PartitionedMeshTest {
 
     @Test
-    fun bounds_shouldBeEmpty() {
+    fun computeBoundingBox_shouldBeEmpty() {
         val partitionedMesh = PartitionedMesh()
 
-        assertThat(partitionedMesh.bounds).isNull()
+        assertThat(partitionedMesh.computeBoundingBox()).isNull()
     }
 
     @Test
-    fun renderGroupCount_whenEmptyShape_shouldBeZero() {
+    fun getRenderGroupCount_whenEmptyShape_shouldBeZero() {
         val partitionedMesh = PartitionedMesh()
 
-        assertThat(partitionedMesh.renderGroupCount).isEqualTo(0)
+        assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(0)
     }
 
     @Test
-    fun outlineCount_whenEmptyShape_shouldThrow() {
+    fun getOutlineCount_whenEmptyShape_shouldThrow() {
         val partitionedMesh = PartitionedMesh()
 
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(-1) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineCount(1) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(-1) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineCount(1) }
     }
 
     @Test
-    fun outlineVertexCount_whenEmptyShape_shouldThrow() {
+    fun getOutlineVertexCount_whenEmptyShape_shouldThrow() {
         val partitionedMesh = PartitionedMesh()
 
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(-1, 0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(0, 0) }
-        assertFailsWith<IllegalArgumentException> { partitionedMesh.outlineVertexCount(1, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(-1, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(0, 0) }
+        assertFailsWith<IllegalArgumentException> { partitionedMesh.getOutlineVertexCount(1, 0) }
     }
 
     @Test
@@ -92,14 +92,14 @@
     fun populateOutlinePosition_withStrokeShape_shouldBeWithinBounds() {
         val shape = buildTestStrokeShape()
 
-        assertThat(shape.renderGroupCount).isEqualTo(1)
-        assertThat(shape.outlineCount(0)).isEqualTo(1)
-        assertThat(shape.outlineVertexCount(0, 0)).isGreaterThan(2)
+        assertThat(shape.getRenderGroupCount()).isEqualTo(1)
+        assertThat(shape.getOutlineCount(0)).isEqualTo(1)
+        assertThat(shape.getOutlineVertexCount(0, 0)).isGreaterThan(2)
 
-        val bounds = assertNotNull(shape.bounds)
+        val bounds = assertNotNull(shape.computeBoundingBox())
 
         val p = MutableVec()
-        for (outlineVertexIndex in 0 until shape.outlineVertexCount(0, 0)) {
+        for (outlineVertexIndex in 0 until shape.getOutlineVertexCount(0, 0)) {
             shape.populateOutlinePosition(groupIndex = 0, outlineIndex = 0, outlineVertexIndex, p)
             assertThat(p.x).isAtLeast(bounds.xMin)
             assertThat(p.y).isAtLeast(bounds.yMin)
@@ -124,7 +124,7 @@
     @Test
     fun meshFormat_forTestShape_isEquivalentToMeshFormatOfFirstMesh() {
         val partitionedMesh = buildTestStrokeShape()
-        assertThat(partitionedMesh.renderGroupCount).isEqualTo(1)
+        assertThat(partitionedMesh.getRenderGroupCount()).isEqualTo(1)
         val shapeFormat = partitionedMesh.renderGroupFormat(0)
         val meshes = partitionedMesh.renderGroupMeshes(0)
         assertThat(meshes).isNotEmpty()
@@ -152,9 +152,9 @@
                 p2 = ImmutableVec(100f, 700f),
             )
 
-        assertThat(partitionedMesh.coverage(intersectingTriangle)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalTriangle)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingTriangle)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalTriangle)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalTriangle, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -169,9 +169,9 @@
         val externalBox =
             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
 
-        assertThat(partitionedMesh.coverage(intersectingBox)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalBox)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingBox)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalBox)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalBox, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -196,9 +196,10 @@
                 shearFactor = 2f,
             )
 
-        assertThat(partitionedMesh.coverage(intersectingParallelogram)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalParallelogram)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalParallelogram, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingParallelogram)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalParallelogram)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalParallelogram, SCALE_TRANSFORM))
+            .isEqualTo(0f)
     }
 
     /**
@@ -221,9 +222,9 @@
                 )
                 .shape
 
-        assertThat(partitionedMesh.coverage(intersectingShape)).isGreaterThan(0f)
-        assertThat(partitionedMesh.coverage(externalShape)).isEqualTo(0f)
-        assertThat(partitionedMesh.coverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(intersectingShape)).isGreaterThan(0f)
+        assertThat(partitionedMesh.computeCoverage(externalShape)).isEqualTo(0f)
+        assertThat(partitionedMesh.computeCoverage(externalShape, SCALE_TRANSFORM)).isEqualTo(0f)
     }
 
     /**
@@ -246,9 +247,11 @@
                 p2 = ImmutableVec(100f, 700f),
             )
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingTriangle, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f)).isFalse()
+        assertThat(
+                partitionedMesh.computeCoverageIsGreaterThan(externalTriangle, 0f, SCALE_TRANSFORM)
+            )
             .isFalse()
     }
 
@@ -270,9 +273,9 @@
         val externalBox =
             ImmutableBox.fromTwoPoints(ImmutableVec(100f, 200f), ImmutableVec(300f, 400f))
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingBox, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingBox, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalBox, 0f, SCALE_TRANSFORM))
             .isFalse()
     }
 
@@ -298,10 +301,16 @@
                 shearFactor = 2f,
             )
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingParallelogram, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingParallelogram, 0f))
+            .isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalParallelogram, 0f))
+            .isFalse()
         assertThat(
-                partitionedMesh.coverageIsGreaterThan(externalParallelogram, 0f, SCALE_TRANSFORM)
+                partitionedMesh.computeCoverageIsGreaterThan(
+                    externalParallelogram,
+                    0f,
+                    SCALE_TRANSFORM
+                )
             )
             .isFalse()
     }
@@ -333,9 +342,9 @@
                 )
                 .shape
 
-        assertThat(partitionedMesh.coverageIsGreaterThan(intersectingShape, 0f)).isTrue()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f)).isFalse()
-        assertThat(partitionedMesh.coverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(intersectingShape, 0f)).isTrue()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f)).isFalse()
+        assertThat(partitionedMesh.computeCoverageIsGreaterThan(externalShape, 0f, SCALE_TRANSFORM))
             .isFalse()
     }
 
@@ -360,7 +369,7 @@
             )
         assertThat(partitionedMesh.isSpatialIndexInitialized()).isFalse()
 
-        assertThat(partitionedMesh.coverage(triangle)).isNotNaN()
+        assertThat(partitionedMesh.computeCoverage(triangle)).isNotNaN()
 
         assertThat(partitionedMesh.isSpatialIndexInitialized()).isTrue()
     }
diff --git a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
index 050e442..5b262d7 100644
--- a/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
+++ b/ink/ink-strokes/src/jvmAndroidMain/kotlin/androidx/ink/strokes/Stroke.kt
@@ -125,8 +125,8 @@
      */
     internal constructor(nativeAddress: Long, brush: Brush) {
         val shape = PartitionedMesh(StrokeJni.allocShallowCopyOfShape(nativeAddress))
-        require(shape.renderGroupCount == brush.family.coats.size) {
-            "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+        require(shape.getRenderGroupCount() == brush.family.coats.size) {
+            "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
         }
         this.nativeAddress = nativeAddress
         this.brush = brush
@@ -142,8 +142,8 @@
      * [PartitionedMesh] is being stored in addition to the [Brush] and [StrokeInputBatch].
      */
     public constructor(brush: Brush, inputs: StrokeInputBatch, shape: PartitionedMesh) {
-        require(shape.renderGroupCount == brush.family.coats.size) {
-            "The shape must have one render group per brush coat, but found ${shape.renderGroupCount} render groups in shape and ${brush.family.coats.size} brush coats in brush."
+        require(shape.getRenderGroupCount() == brush.family.coats.size) {
+            "The shape must have one render group per brush coat, but found ${shape.getRenderGroupCount()} render groups in shape and ${brush.family.coats.size} brush coats in brush."
         }
         this.brush = brush
         this.shape = shape
diff --git a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
index c730120..2155aff 100644
--- a/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
+++ b/ink/ink-strokes/src/jvmAndroidTest/kotlin/androidx/ink/strokes/StrokeTest.kt
@@ -68,7 +68,7 @@
         // Create a [ModeledShape] with render group.
         val inputs = makeTestInputs()
         val shape = Stroke(buildTestBrush(), inputs).shape
-        assertThat(shape.renderGroupCount).isEqualTo(1)
+        assertThat(shape.getRenderGroupCount()).isEqualTo(1)
 
         // Create a brush with two brush coats.
         val coat = BrushCoat(BrushTip(), BrushPaint())
diff --git a/inspection/inspection-gradle-plugin/lint-baseline.xml b/inspection/inspection-gradle-plugin/lint-baseline.xml
index 603e7bd..ef0ffef 100644
--- a/inspection/inspection-gradle-plugin/lint-baseline.xml
+++ b/inspection/inspection-gradle-plugin/lint-baseline.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.6.0-beta01" type="baseline" client="gradle" dependencies="false" name="AGP (8.6.0-beta01)" variant="all" version="8.6.0-beta01">
+<issues format="6" by="lint 8.7.0-alpha02" type="baseline" client="gradle" dependencies="false" name="AGP (8.7.0-alpha02)" variant="all" version="8.7.0-alpha02">
 
     <issue
         id="EagerGradleConfiguration"
@@ -38,6 +38,24 @@
     </issue>
 
     <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method findProject"
+        errorLine1="    val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+        errorLine2="                                                      ~~~~~~~~~~~">
+        <location
+            file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+    </issue>
+
+    <issue
+        id="GradleProjectIsolation"
+        message="Avoid using method getRootProject"
+        errorLine1="    val inspectorProject = libraryProject.rootProject.findProject(inspectorProjectPath)"
+        errorLine2="                                          ~~~~~~~~~~~">
+        <location
+            file="src/main/kotlin/androidx/inspection/gradle/InspectionPlugin.kt"/>
+    </issue>
+
+    <issue
         id="WithPluginClasspathUsage"
         message="Avoid usage of GradleRunner#withPluginClasspath, which is broken. Instead use something like https://github.com/autonomousapps/dependency-analysis-gradle-plugin/tree/main/testkit#gradle-testkit-support-plugin"
         errorLine1="            GradleRunner.create().withProjectDir(projectSetup.rootDir).withPluginClasspath()"
diff --git a/libraryversions.toml b/libraryversions.toml
index 3980454..75a598d 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -1,6 +1,6 @@
 [versions]
 ACTIVITY = "1.10.0-alpha02"
-ANNOTATION = "1.9.0-alpha02"
+ANNOTATION = "1.9.0-alpha03"
 ANNOTATION_EXPERIMENTAL = "1.5.0-alpha01"
 APPCOMPAT = "1.8.0-alpha01"
 APPSEARCH = "1.1.0-alpha05"
@@ -89,9 +89,9 @@
 LEANBACK_TAB = "1.1.0-beta01"
 LEGACY = "1.1.0-alpha01"
 LIBYUV = "0.1.0-dev01"
-LIFECYCLE = "2.9.0-alpha01"
+LIFECYCLE = "2.9.0-alpha02"
 LIFECYCLE_EXTENSIONS = "2.2.0"
-LINT = "1.0.0-alpha01"
+LINT = "1.0.0-alpha02"
 LOADER = "1.2.0-alpha01"
 MEDIA = "1.7.0-rc01"
 MEDIAROUTER = "1.8.0-alpha01"
diff --git a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
index e8680ae..3f129b8 100644
--- a/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
+++ b/lint/lint-gradle/src/main/java/androidx/lint/gradle/DiscouragedGradleMethodDetector.kt
@@ -140,8 +140,11 @@
                     Replacement(NAMED_DOMAIN_OBJECT_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
                 "findByName" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
                 "findByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
+                "findProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
                 "findProperty" to
                     Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+                "hasProperty" to
+                    Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
                 "property" to
                     Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
                 "iterator" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
@@ -149,8 +152,10 @@
                 "getAt" to Replacement(TASK_COLLECTION, "named", EAGER_CONFIGURATION_ISSUE),
                 "getByPath" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
                 "getByName" to Replacement(TASK_CONTAINER, "named", EAGER_CONFIGURATION_ISSUE),
+                "getParent" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
                 "getProperties" to
                     Replacement(PROJECT, "providers.gradleProperty", PROJECT_ISOLATION_ISSUE),
+                "getRootProject" to Replacement(PROJECT, null, PROJECT_ISOLATION_ISSUE),
                 "matching" to Replacement(TASK_COLLECTION, null, EAGER_CONFIGURATION_ISSUE),
                 "replace" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
                 "remove" to Replacement(TASK_CONTAINER, null, EAGER_CONFIGURATION_ISSUE),
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index bb6ad19..cabfeb1 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -590,5 +590,8 @@
     method public static inline <reified T> T toRoute(androidx.lifecycle.SavedStateHandle, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<? extends java.lang.Object?>> typeMap);
   }
 
+  public interface SupportingPane {
+  }
+
 }
 
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index bb6ad19..cabfeb1 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -590,5 +590,8 @@
     method public static inline <reified T> T toRoute(androidx.lifecycle.SavedStateHandle, optional java.util.Map<kotlin.reflect.KType,androidx.navigation.NavType<? extends java.lang.Object?>> typeMap);
   }
 
+  public interface SupportingPane {
+  }
+
 }
 
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.kt b/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.kt
new file mode 100644
index 0000000..a6fd4ff
--- /dev/null
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/SupportingPane.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 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.navigation
+
+/**
+ * A marker interface for [NavDestination] subclasses that sit alongside the view of other
+ * destinations.
+ *
+ * Supporting pane destinations have the same lifecycle as the other visible destinations (e.g., a
+ * non-SupportingPane destination will continue to be resumed when a supporting pane is added to the
+ * back stack).
+ *
+ * [androidx.navigation.NavController.OnDestinationChangedListener] instances can also customize
+ * their behavior based on whether the destination is a SupportingPane.
+ */
+public interface SupportingPane
diff --git a/navigation/navigation-compose/samples/build.gradle b/navigation/navigation-compose/samples/build.gradle
index b672a68..eb7498d 100644
--- a/navigation/navigation-compose/samples/build.gradle
+++ b/navigation/navigation-compose/samples/build.gradle
@@ -57,10 +57,6 @@
     kotlinTarget = KotlinTarget.KOTLIN_2_0
 }
 
-tasks.withType(KotlinCompile).configureEach {
-    kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
-}
-
 android {
     compileSdk 35
     namespace "androidx.navigation.compose.samples"
diff --git a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
index 838929a6..bfb1c33 100644
--- a/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
+++ b/navigation/navigation-compose/samples/src/main/java/androidx/navigation/compose/samples/SharedElementSample.kt
@@ -103,65 +103,77 @@
     SharedTransitionLayout {
         val selectFirst = mutableStateOf(true)
         NavHost(navController, startDestination = RedBox) {
-            composable<RedBox> { RedBox(this, selectFirst) { navController.navigate(BlueBox) } }
-            composable<BlueBox> { BlueBox(this, selectFirst) { navController.popBackStack() } }
+            composable<RedBox> {
+                RedBox(this@SharedTransitionLayout, this, selectFirst) {
+                    navController.navigate(BlueBox)
+                }
+            }
+            composable<BlueBox> {
+                BlueBox(this@SharedTransitionLayout, this, selectFirst) {
+                    navController.popBackStack()
+                }
+            }
         }
     }
 }
 
-context(SharedTransitionScope)
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun RedBox(
+    sharedScope: SharedTransitionScope,
     scope: AnimatedContentScope,
     selectFirst: MutableState<Boolean>,
     onNavigate: () -> Unit
 ) {
-    Box(
-        Modifier.sharedBounds(
-                rememberSharedContentState("name"),
-                scope,
-                renderInOverlayDuringTransition = selectFirst.value
-            )
-            .clickable(
-                onClick = {
-                    selectFirst.value = !selectFirst.value
-                    onNavigate()
-                }
-            )
-            .background(Color.Red)
-            .size(100.dp)
-    ) {
-        Text("start", color = Color.White)
+    with(sharedScope) {
+        Box(
+            Modifier.sharedBounds(
+                    rememberSharedContentState("name"),
+                    scope,
+                    renderInOverlayDuringTransition = selectFirst.value
+                )
+                .clickable(
+                    onClick = {
+                        selectFirst.value = !selectFirst.value
+                        onNavigate()
+                    }
+                )
+                .background(Color.Red)
+                .size(100.dp)
+        ) {
+            Text("start", color = Color.White)
+        }
     }
 }
 
-context(SharedTransitionScope)
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun BlueBox(
+    sharedScope: SharedTransitionScope,
     scope: AnimatedContentScope,
     selectFirst: MutableState<Boolean>,
     onPopBack: () -> Unit
 ) {
-    Box(
-        Modifier.offset(180.dp, 180.dp)
-            .sharedBounds(
-                rememberSharedContentState("name"),
-                scope,
-                renderInOverlayDuringTransition = !selectFirst.value
-            )
-            .clickable(
-                onClick = {
-                    selectFirst.value = !selectFirst.value
-                    onPopBack()
-                }
-            )
-            .alpha(0.5f)
-            .background(Color.Blue)
-            .size(180.dp)
-    ) {
-        Text("finish", color = Color.White)
+    with(sharedScope) {
+        Box(
+            Modifier.offset(180.dp, 180.dp)
+                .sharedBounds(
+                    rememberSharedContentState("name"),
+                    scope,
+                    renderInOverlayDuringTransition = !selectFirst.value
+                )
+                .clickable(
+                    onClick = {
+                        selectFirst.value = !selectFirst.value
+                        onPopBack()
+                    }
+                )
+                .alpha(0.5f)
+                .background(Color.Blue)
+                .size(180.dp)
+        ) {
+            Text("finish", color = Color.White)
+        }
     }
 }
 
diff --git a/navigation/navigation-runtime/lint-baseline.xml b/navigation/navigation-runtime/lint-baseline.xml
index e8b9764..287e223 100644
--- a/navigation/navigation-runtime/lint-baseline.xml
+++ b/navigation/navigation-runtime/lint-baseline.xml
@@ -40,6 +40,15 @@
     <issue
         id="NewApi"
         message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
+        errorLine1="                nextResumed.removeFirstKt()"
+        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+        <location
+            file="src/main/java/androidx/navigation/NavController.kt"/>
+    </issue>
+
+    <issue
+        id="NewApi"
+        message="This Kotlin extension function will be hidden by `java.util.SequencedCollection` starting in API 35"
         errorLine1="                val started = nextStarted.removeFirstKt()"
         errorLine2="                              ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
index b393c43..71559e7 100644
--- a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/NavBackStackEntryLifecycleTest.kt
@@ -23,7 +23,11 @@
 import androidx.lifecycle.testing.TestLifecycleOwner
 import androidx.navigation.test.FloatingTestNavigator
 import androidx.navigation.test.R
+import androidx.navigation.test.SupportingFloatingTestNavigator
+import androidx.navigation.test.SupportingTestNavigator
 import androidx.navigation.test.dialog
+import androidx.navigation.test.supportingDialog
+import androidx.navigation.test.supportingPane
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ApplicationProvider
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -164,6 +168,79 @@
     }
 
     /**
+     * Test that navigating from a sibling to a SupportingPane sibling leaves the previous
+     * destination resumed.
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleWithSupportingPane() {
+        val navController = createNavController()
+        val navGraph =
+            navController.navigatorProvider.navigation(
+                route = "graph",
+                startDestination = "start"
+            ) {
+                test("start")
+                test("second")
+                supportingPane("supportingPane")
+            }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry("graph")
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val startBackStackEntry = navController.getBackStackEntry("start")
+        assertWithMessage("The start destination should be resumed")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("second")
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should be created when not visible")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        val secondBackStackEntry = navController.getBackStackEntry("second")
+        assertWithMessage("The second destination should be resumed")
+            .that(secondBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("supportingPane")
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should still be in created")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertWithMessage("The second destination should be resumed when a SupportingPane is open")
+            .that(secondBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val supportingPaneBackStackEntry = navController.getBackStackEntry("supportingPane")
+        assertWithMessage("The supporting pane destination should be resumed")
+            .that(supportingPaneBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.popBackStack()
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should still be in created")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.CREATED)
+        assertWithMessage("The second destination should be resumed after pop")
+            .that(secondBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The popped destination should be destroyed")
+            .that(supportingPaneBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+    }
+
+    /**
      * Test that navigating from a sibling to a FloatingWindow sibling leaves the previous
      * destination started.
      */
@@ -224,6 +301,78 @@
             .isEqualTo(Lifecycle.State.DESTROYED)
     }
 
+    /**
+     * Test that navigating from a sibling + SupportingPane sibling to a dialog leaves both started.
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleWithSupportingPaneAndDialog() {
+        val navController = createNavController()
+        val navGraph =
+            navController.navigatorProvider.navigation(
+                route = "graph",
+                startDestination = "start"
+            ) {
+                test("start")
+                supportingPane("supportingPane")
+                dialog("dialog")
+            }
+        navController.graph = navGraph
+
+        val graphBackStackEntry = navController.getBackStackEntry("graph")
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val startBackStackEntry = navController.getBackStackEntry("start")
+        assertWithMessage("The start destination should be resumed")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("supportingPane")
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should be resumed when a SupportingPane is open")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        val supportingPaneBackStackEntry = navController.getBackStackEntry("supportingPane")
+        assertWithMessage("The supporting pane destination should be resumed")
+            .that(supportingPaneBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("dialog")
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should be started under a dialog")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.STARTED)
+        assertWithMessage("The supporting pane destination should be started under a dialog")
+            .that(supportingPaneBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.STARTED)
+        val dialogBackStackEntry = navController.getBackStackEntry("dialog")
+        assertWithMessage("The dialog destination should be resumed")
+            .that(dialogBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.popBackStack()
+
+        assertWithMessage("The parent graph should be resumed when its child is resumed")
+            .that(graphBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The start destination should still be resumed after pop")
+            .that(startBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The supporting pane destination should be resumed after pop")
+            .that(supportingPaneBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertWithMessage("The popped destination should be destroyed")
+            .that(dialogBackStackEntry.lifecycle.currentState)
+            .isEqualTo(Lifecycle.State.DESTROYED)
+    }
+
     /** Test that all visible floating windows underneath the top one are marked started. */
     @UiThreadTest
     @Test
@@ -268,6 +417,53 @@
         assertThat(topDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
     }
 
+    /**
+     * Test that all visible floating windows underneath the top one are marked started unless a
+     * SupportingPane+FloatingWindow destination is above a FloatingWindow.
+     */
+    @UiThreadTest
+    @Test
+    fun testLifecycleWithSupportingDialogs() {
+        val navController = createNavController()
+        val navGraph =
+            navController.navigatorProvider.navigation(
+                route = "graph",
+                startDestination = "start"
+            ) {
+                test("start")
+                supportingDialog("bottomDialog")
+                dialog("midDialog")
+                supportingDialog("topDialog")
+            }
+        navController.graph = navGraph
+
+        val graphEntry = navController.getBackStackEntry("graph")
+        assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        val startEntry = navController.getBackStackEntry("start")
+        assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("bottomDialog")
+        assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        val bottomDialogEntry = navController.getBackStackEntry("bottomDialog")
+        assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("midDialog")
+        assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        val midDialogEntry = navController.getBackStackEntry("midDialog")
+        assertThat(midDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        navController.navigate("topDialog")
+        assertThat(graphEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(startEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(bottomDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(midDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        val topDialogEntry = navController.getBackStackEntry("topDialog")
+        assertThat(topDialogEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
     @UiThreadTest
     @Test
     fun testLifecycleWithDialogsAndGraphs() {
@@ -1386,7 +1582,9 @@
     ): NavController {
         val navController = NavHostController(ApplicationProvider.getApplicationContext())
         navController.navigatorProvider.addNavigator(TestNavigator())
+        navController.navigatorProvider.addNavigator(SupportingTestNavigator())
         navController.navigatorProvider.addNavigator(FloatingTestNavigator())
+        navController.navigatorProvider.addNavigator(SupportingFloatingTestNavigator())
         navController.setLifecycleOwner(lifecycleOwner)
         return navController
     }
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt
new file mode 100644
index 0000000..9a3c82b
--- /dev/null
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingFloatingTestNavigator.kt
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:Suppress("NOTHING_TO_INLINE")
+
+package androidx.navigation.test
+
+import androidx.navigation.FloatingWindow
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
+import androidx.navigation.get
+import androidx.testutils.TestNavigator
+
[email protected]("supporting_dialog")
+class SupportingFloatingTestNavigator : TestNavigator() {
+    override fun createDestination(): Destination {
+        return SupportingFloatingDestination(this)
+    }
+
+    class SupportingFloatingDestination(navigator: TestNavigator) :
+        Destination(navigator), FloatingWindow, SupportingPane
+}
+
+/** Construct a new [TestNavigator.Destination] from a [SupportingFloatingTestNavigator]. */
+inline fun NavGraphBuilder.supportingDialog(
+    route: String,
+    builder: SupportingFloatingTestNavigatorDestinationBuilder.() -> Unit = {}
+) =
+    destination(
+        SupportingFloatingTestNavigatorDestinationBuilder(
+                provider[SupportingFloatingTestNavigator::class],
+                route
+            )
+            .apply(builder)
+    )
+
+/**
+ * DSL for constructing a new [TestNavigator.Destination] from a [SupportingFloatingTestNavigator].
+ */
+@NavDestinationDsl
+class SupportingFloatingTestNavigatorDestinationBuilder(
+    navigator: SupportingFloatingTestNavigator,
+    route: String
+) : NavDestinationBuilder<TestNavigator.Destination>(navigator, route)
diff --git a/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt
new file mode 100644
index 0000000..fac28ea
--- /dev/null
+++ b/navigation/navigation-runtime/src/androidTest/java/androidx/navigation/test/SupportingTestNavigator.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.navigation.test
+
+import androidx.navigation.NavDestinationBuilder
+import androidx.navigation.NavDestinationDsl
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
+import androidx.navigation.get
+import androidx.testutils.TestNavigator
+
[email protected]("supporting_pane")
+class SupportingTestNavigator : TestNavigator() {
+    override fun createDestination(): Destination {
+        return SupportingDestination(this)
+    }
+
+    class SupportingDestination(navigator: TestNavigator) : Destination(navigator), SupportingPane
+}
+
+/** Construct a new [TestNavigator.Destination] from a [SupportingTestNavigator]. */
+inline fun NavGraphBuilder.supportingPane(
+    route: String,
+    builder: SupportingTestNavigatorDestinationBuilder.() -> Unit = {}
+) =
+    destination(
+        SupportingTestNavigatorDestinationBuilder(provider[SupportingTestNavigator::class], route)
+            .apply(builder)
+    )
+
+/** DSL for constructing a new [TestNavigator.Destination] from a [SupportingTestNavigator]. */
+@NavDestinationDsl
+class SupportingTestNavigatorDestinationBuilder(navigator: SupportingTestNavigator, route: String) :
+    NavDestinationBuilder<TestNavigator.Destination>(navigator, route)
diff --git a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
index 9e36bf8..0075458 100644
--- a/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
+++ b/navigation/navigation-runtime/src/main/java/androidx/navigation/NavController.kt
@@ -1105,12 +1105,58 @@
             // Nothing to update
             return
         }
-        // First determine what the current resumed destination is and, if and only if
-        // the current resumed destination is a FloatingWindow, what destinations are
-        // underneath it that must remain started.
-        var nextResumed: NavDestination? = backStack.last().destination
+        // Lifecycle can be split into three layers:
+        // 1. Resumed - these are the topmost destination(s) that the user can interact with
+        // 2. Started - these destinations are visible, but are underneath resumed destinations
+        // 3. Created - these destinations are not visible or on the process of being animated out
+
+        // So first, we need to determine which destinations should be resumed and started
+        // This is done by looking at the two special interfaces we have:
+        // - FloatingWindow indicates a destination that is above all other destinations, leaving
+        //   destinations below it visible, but not interactable. These are always only on the
+        //   top of the back stack
+        // - SupportingPane indicates a destination that sits alongside the previous destination
+        //   and shares the same lifecycle (e.g., both will be resumed, started, or created)
+
+        // This means no matter what, the topmost destination should be able to be resumed,
+        // then we add in all of the destinations that also need to be resumed (if the
+        // topmost screen is a SupportingPane)
+        val topmostDestination = backStack.last().destination
+        val nextResumed: MutableList<NavDestination> = mutableListOf(topmostDestination)
+        if (topmostDestination is SupportingPane) {
+            // A special note for destinations that are marked as both a FloatingWindow and a
+            // SupportingPane: a supporting floating window destination can only support other
+            // floating windows - if a supporting floating window destination is above
+            // a regular destination, the regular destination will *not* be resumed, but instead
+            // follow the normal rules between floating windows and regular destinations and only
+            // be started.
+            val onlyAllowFloatingWindows = topmostDestination is FloatingWindow
+            val iterator = backStack.reversed().drop(1).iterator()
+            while (iterator.hasNext()) {
+                val destination = iterator.next().destination
+                if (
+                    onlyAllowFloatingWindows &&
+                        destination !is FloatingWindow &&
+                        destination !is NavGraph
+                ) {
+                    break
+                }
+                // Add all visible destinations (e.g., SupportingDestination destinations, their
+                // NavGraphs, and the screen directly below all SupportingDestination destinations)
+                // to nextResumed
+                nextResumed.add(destination)
+                // break if we find first visible screen
+                if (destination !is SupportingPane && destination !is NavGraph) {
+                    break
+                }
+            }
+        }
+
+        // Now that we've marked all of the resumed destinations, we continue to iterate
+        // through the back stack to find any destinations that should be started - ones that are
+        // below FloatingWindow destinations
         val nextStarted: MutableList<NavDestination> = mutableListOf()
-        if (nextResumed is FloatingWindow) {
+        if (nextResumed.last() is FloatingWindow) {
             // Find all visible destinations in the back stack as they
             // should still be STARTED when the FloatingWindow destination is above it.
             val iterator = backStack.reversed().iterator()
@@ -1121,12 +1167,17 @@
                 // to nextStarted
                 nextStarted.add(destination)
                 // break if we find first visible screen
-                if (destination !is FloatingWindow && destination !is NavGraph) {
+                if (
+                    destination !is FloatingWindow &&
+                        destination !is SupportingPane &&
+                        destination !is NavGraph
+                ) {
                     break
                 }
             }
         }
-        // First iterate downward through the stack, applying downward Lifecycle
+
+        // Now iterate downward through the stack, applying downward Lifecycle
         // transitions and capturing any upward Lifecycle transitions to apply afterwards.
         // This ensures proper nesting where parent navigation graphs are started before
         // their children and stopped only after their children are stopped.
@@ -1136,7 +1187,7 @@
             val entry = iterator.next()
             val currentMaxLifecycle = entry.maxLifecycle
             val destination = entry.destination
-            if (nextResumed != null && destination.id == nextResumed.id) {
+            if (nextResumed.firstOrNull()?.id == destination.id) {
                 // Upward Lifecycle transitions need to be done afterwards so that
                 // the parent navigation graph is resumed before their children
                 if (currentMaxLifecycle != Lifecycle.State.RESUMED) {
@@ -1153,7 +1204,8 @@
                     }
                 }
                 if (nextStarted.firstOrNull()?.id == destination.id) nextStarted.removeFirstKt()
-                nextResumed = nextResumed.parent
+                nextResumed.removeFirstKt()
+                destination.parent?.let { nextResumed.add(it) }
             } else if (nextStarted.isNotEmpty() && destination.id == nextStarted.first().id) {
                 val started = nextStarted.removeFirstKt()
                 if (currentMaxLifecycle == Lifecycle.State.RESUMED) {
diff --git a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
index 82e6e99..4d38bfa 100644
--- a/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
+++ b/navigation/navigation-testing/src/androidTest/java/androidx/navigation/testing/TestNavigatorStateTest.kt
@@ -27,6 +27,7 @@
 import androidx.navigation.NavDestination
 import androidx.navigation.NavOptions
 import androidx.navigation.Navigator
+import androidx.navigation.SupportingPane
 import androidx.navigation.navOptions
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -86,6 +87,26 @@
     }
 
     @Test
+    fun testSupportingPaneLifecycle() {
+        val navigator = SupportingPaneTestNavigator()
+        navigator.onAttach(state)
+        val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        navigator.navigate(listOf(secondEntry), null, null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        navigator.popBackStack(secondEntry, false)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+    }
+
+    @Test
     fun testWithTransitionLifecycle() {
         val navigator = TestTransitionNavigator()
         navigator.onAttach(state)
@@ -129,6 +150,58 @@
     }
 
     @Test
+    fun testWithSupportingPaneTransitionLifecycle() {
+        val navigator = SupportingPaneTestTransitionNavigator()
+        navigator.onAttach(state)
+        val firstEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.INITIALIZED)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        state.markTransitionComplete(firstEntry)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        val secondEntry = state.createBackStackEntry(navigator.createDestination(), null)
+        navigator.navigate(listOf(secondEntry), null, null)
+        // Both are started because they are SupportingPane destinations
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+        state.markTransitionComplete(secondEntry)
+        // Even though the secondEntry has completed its transition, the firstEntry
+        // hasn't completed its transition, so it shouldn't be resumed yet
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        state.markTransitionComplete(firstEntry)
+        // Both are resumed because they are SupportingPane destinations that have finished
+        // their transitions
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+
+        navigator.popBackStack(secondEntry, true)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.CREATED)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+
+        state.markTransitionComplete(firstEntry)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+        state.markTransitionComplete(secondEntry)
+        assertThat(secondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+
+        val restoredSecondEntry = state.restoreBackStackEntry(secondEntry)
+        navigator.navigate(listOf(restoredSecondEntry), null, null)
+        assertThat(firstEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.STARTED)
+        assertThat(state.transitionsInProgress.value.contains(firstEntry)).isTrue()
+
+        state.markTransitionComplete(firstEntry)
+        state.markTransitionComplete(restoredSecondEntry)
+        assertThat(restoredSecondEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @Test
     fun testSameEntry() {
         val navigator = TestTransitionNavigator()
         navigator.onAttach(state)
@@ -362,6 +435,35 @@
     internal class FloatingTestDestination(navigator: Navigator<out NavDestination>) :
         NavDestination(navigator), FloatingWindow
 
+    @Navigator.Name("test")
+    internal class SupportingPaneTestNavigator : Navigator<SupportingPaneTestDestination>() {
+        override fun createDestination(): SupportingPaneTestDestination =
+            SupportingPaneTestDestination(this)
+    }
+
+    @Navigator.Name("test")
+    internal class SupportingPaneTestTransitionNavigator :
+        Navigator<SupportingPaneTestDestination>() {
+
+        override fun createDestination(): SupportingPaneTestDestination =
+            SupportingPaneTestDestination(this)
+
+        override fun navigate(
+            entries: List<NavBackStackEntry>,
+            navOptions: NavOptions?,
+            navigatorExtras: Extras?
+        ) {
+            entries.forEach { entry -> state.pushWithTransition(entry) }
+        }
+
+        override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+            state.popWithTransition(popUpTo, savedState)
+        }
+    }
+
+    internal class SupportingPaneTestDestination(navigator: Navigator<out NavDestination>) :
+        NavDestination(navigator), SupportingPane
+
     class TestViewModel : ViewModel() {
         var wasCleared = false
 
diff --git a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
index beb1d0a..f26e922 100644
--- a/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
+++ b/navigation/navigation-testing/src/main/java/androidx/navigation/testing/TestNavigatorState.kt
@@ -25,6 +25,7 @@
 import androidx.navigation.NavDestination
 import androidx.navigation.NavViewModelStoreProvider
 import androidx.navigation.NavigatorState
+import androidx.navigation.SupportingPane
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.runBlocking
@@ -174,6 +175,17 @@
                                 } else {
                                     Lifecycle.State.STARTED
                                 }
+                            previousEntry.destination is SupportingPane -> {
+                                // Match the previous entry's destination, making sure
+                                // a transitioning destination does not go to resumed
+                                previousEntry.maxLifecycle.coerceAtMost(
+                                    if (!transitioning) {
+                                        Lifecycle.State.RESUMED
+                                    } else {
+                                        Lifecycle.State.STARTED
+                                    }
+                                )
+                            }
                             previousEntry.destination is FloatingWindow -> Lifecycle.State.STARTED
                             else -> Lifecycle.State.CREATED
                         }
diff --git a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
index 1e48878..db1406d 100644
--- a/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
+++ b/pdf/pdf-viewer-fragment/src/main/java/androidx/pdf/viewer/fragment/PdfViewerFragment.kt
@@ -66,6 +66,7 @@
 import androidx.pdf.widget.ZoomView
 import androidx.pdf.widget.ZoomView.ZoomScroll
 import com.google.android.material.floatingactionbutton.FloatingActionButton
+import java.io.IOException
 import kotlinx.coroutines.launch
 
 /**
@@ -298,7 +299,7 @@
                         }
                     }
                 },
-                onDocumentLoadFailure = { thrown -> onLoadDocumentError(thrown) }
+                onDocumentLoadFailure = { thrown -> showLoadingErrorView(thrown) }
             )
 
         setUpEditFab()
@@ -494,11 +495,8 @@
         savedState?.let { state ->
             if (isFileRestoring) {
                 state.containsKey(KEY_LAYOUT_REACH).let {
-                    val layoutReach = state.getInt(KEY_LAYOUT_REACH, -1)
-                    if (layoutReach != -1) {
-                        layoutHandler?.pageLayoutReach = layoutReach
-                        layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
-                    }
+                    val layoutReach = state.getInt(KEY_LAYOUT_REACH)
+                    layoutHandler?.setInitialPageLayoutReachWithMax(layoutReach)
                 }
 
                 // Restore page selection from saved state if it exists
@@ -584,7 +582,6 @@
         pdfLoaderCallbacks?.pdfLoader = pdfLoader
 
         layoutHandler = LayoutHandler(pdfLoader)
-        paginatedView?.model?.size?.let { layoutHandler!!.pageLayoutReach = it }
 
         val updatedSelectionModel = PdfSelectionModel(pdfLoader)
         updateSelectionModel(updatedSelectionModel)
@@ -634,7 +631,7 @@
                 // app that owns it has been killed by the system. We will still recover,
                 // but log this.
                 viewState.set(ViewState.ERROR)
-                onLoadDocumentError(e)
+                showLoadingErrorView(e)
             }
         }
     }
@@ -658,7 +655,9 @@
     }
 
     private fun destroyContentModel() {
+
         pdfLoader?.cancelAll()
+
         paginationModel = null
 
         selectionHandles?.destroy()
@@ -739,6 +738,13 @@
         )
     }
 
+    private fun showLoadingErrorView(error: Throwable) {
+        context?.resources?.getString(R.string.error_cannot_open_pdf)?.let {
+            loadingView?.showErrorView(it)
+        }
+        onLoadDocumentError(error)
+    }
+
     private fun loadFile(fileUri: Uri) {
         Preconditions.checkNotNull(fileUri)
         Preconditions.checkArgument(
@@ -761,8 +767,13 @@
         try {
             validateFileUri(fileUri)
             fetchFile(fileUri)
-        } catch (e: SecurityException) {
-            onLoadDocumentError(e)
+        } catch (error: Exception) {
+            when (error) {
+                is IOException,
+                is SecurityException,
+                is NullPointerException -> showLoadingErrorView(error)
+                else -> throw error
+            }
         }
         if (localUri != null && localUri != fileUri) {
             annotationButton?.hide()
@@ -789,7 +800,7 @@
                 }
 
                 override fun failed(thrown: Throwable) {
-                    onLoadDocumentError(thrown)
+                    showLoadingErrorView(thrown)
                 }
 
                 override fun progress(progress: Float) {}
diff --git a/pdf/pdf-viewer/build.gradle b/pdf/pdf-viewer/build.gradle
index 871b0a6..78bb274 100644
--- a/pdf/pdf-viewer/build.gradle
+++ b/pdf/pdf-viewer/build.gradle
@@ -21,7 +21,6 @@
     id("com.android.library")
     id("androidx.stableaidl")
     id("kotlin-android")
-    id ("kotlin-parcelize")
 }
 
 dependencies {
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
index ea847f0..3cdd35d 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginatedView.java
@@ -18,8 +18,6 @@
 
 import android.content.Context;
 import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.util.AttributeSet;
 import android.util.SparseArray;
 import android.view.View;
@@ -29,7 +27,6 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
-import androidx.core.os.ParcelCompat;
 import androidx.pdf.ViewState;
 import androidx.pdf.data.Range;
 import androidx.pdf.util.PaginationUtils;
@@ -171,22 +168,6 @@
         }
     }
 
-    @Nullable
-    @Override
-    protected Parcelable onSaveInstanceState() {
-        Parcelable superState = super.onSaveInstanceState();
-        return new SavedState(superState, mModel);
-    }
-
-    @Override
-    protected void onRestoreInstanceState(Parcelable state) {
-        SavedState savedState = (SavedState) state;
-        super.onRestoreInstanceState(((SavedState) state).getSuperState());
-        mModel = savedState.mModel;
-        mPageRangeHandler = new PageRangeHandler(mModel);
-        requestLayout();
-    }
-
     /**
      * Returns the current viewport in content coordinates
      */
@@ -541,24 +522,4 @@
     public boolean isConfigurationChanged() {
         return mIsConfigurationChanged;
     }
-
-    static class SavedState extends View.BaseSavedState {
-        final PaginationModel mModel;
-
-        SavedState(Parcelable superState, PaginationModel model) {
-            super(superState);
-            mModel = model;
-        }
-
-        SavedState(Parcel source, ClassLoader loader) {
-            super(source);
-            mModel = ParcelCompat.readParcelable(source, loader, PaginationModel.class);
-        }
-
-        @Override
-        public void writeToParcel(Parcel out, int flags) {
-            super.writeToParcel(out, flags);
-            out.writeParcelable(mModel, flags);
-        }
-    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
index 6536157..8407a743 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModel.java
@@ -16,15 +16,11 @@
 
 package androidx.pdf.viewer;
 
-import android.annotation.SuppressLint;
 import android.content.Context;
 import android.graphics.Rect;
-import android.os.Parcel;
-import android.os.Parcelable;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RestrictTo;
-import androidx.core.os.ParcelCompat;
 import androidx.pdf.data.Range;
 import androidx.pdf.models.Dimensions;
 import androidx.pdf.util.PaginationUtils;
@@ -61,8 +57,7 @@
  * pages are added
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
-@SuppressLint("BanParcelableUsage")
-public class PaginationModel implements Parcelable {
+public class PaginationModel {
     /**
      * The spacing added before and after each page (the actual space between 2 consecutive pages is
      * twice this distance), in pixels.
@@ -92,30 +87,6 @@
         mPageSpacingPx = PaginationUtils.getPageSpacingInPixels(context);
     }
 
-    protected PaginationModel(@NonNull Parcel in) {
-        PaginationModelData data = ParcelCompat.readParcelable(in, getClass().getClassLoader(),
-                PaginationModelData.class);
-        mPageSpacingPx = data.getPageSpacingPx();
-        mMaxPages = data.getMaxPages();
-        mPages = data.getPages();
-        mPageStops = data.getPageStops();
-        mSize = data.getSize();
-        mEstimatedPageHeight = data.getEstimatedPageHeight();
-        mAccumulatedPageSize = data.getAccumulatedPageSize();
-    }
-
-    public static final Creator<PaginationModel> CREATOR = new Creator<PaginationModel>() {
-        @Override
-        public PaginationModel createFromParcel(Parcel in) {
-            return new PaginationModel(in);
-        }
-
-        @Override
-        public PaginationModel[] newArray(int size) {
-            return new PaginationModel[size];
-        }
-    };
-
     /**
      * Initializes the model.
      *
@@ -293,6 +264,7 @@
     }
 
 
+
     /**
      * Returns the location of the page in the model.
      *
@@ -305,7 +277,7 @@
      *       maximizes the portion of that view that is visible on the screen
      * </ul>
      *
-     * @param pageNum  - index of requested page
+     * @param pageNum - index of requested page
      * @param viewArea - the current viewport in content coordinates
      * @return - coordinates of the page within this model
      */
@@ -419,16 +391,4 @@
         mObservers.clear();
         super.finalize();
     }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(@NonNull Parcel dest, int flags) {
-        PaginationModelData data = new PaginationModelData(mPageSpacingPx, mMaxPages, mPages,
-                mPageStops, mSize, mEstimatedPageHeight, mAccumulatedPageSize);
-        dest.writeParcelable(data, flags);
-    }
 }
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt
deleted file mode 100644
index cfff185..0000000
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/PaginationModelData.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2024 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.pdf.viewer
-
-import android.annotation.SuppressLint
-import android.os.Parcelable
-import androidx.pdf.models.Dimensions
-import kotlinx.parcelize.Parcelize
-
-/** Parcelable representation of the data stored by [PaginationModel] */
-@Parcelize
-@SuppressLint("BanParcelableUsage")
-internal data class PaginationModelData(
-    /** The space between pages in pixels */
-    val pageSpacingPx: Int,
-    /** The maximum number of pages in the model, i.e. the number of pages in the PDF */
-    val maxPages: Int,
-    /** The dimensions of all pages the model has received dimensions for, in content coordinates */
-    val pages: Array<Dimensions>,
-    /**
-     * The bottom position of each page, in content coordinates. Derived from [pages] and
-     * [pageSpacingPx], but stored separately to avoid re-computation.
-     */
-    val pageStops: IntArray,
-    /** The number of pages for which dimensions are known */
-    val size: Int,
-    /**
-     * The estimated height of the next PDF page, based on known dimensions. Derived from [pages]
-     * and [size], but stored separately to avoid re-computation.
-     */
-    val estimatedPageHeight: Float,
-    /**
-     * The cumulative height of all pages for which dimensions are known. Derived from [pages], but
-     * stored separately to avoid re-computation.
-     */
-    val accumulatedPageSize: Int,
-) : Parcelable {
-    override fun equals(other: Any?): Boolean {
-        if (this === other) return true
-        if (javaClass != other?.javaClass) return false
-
-        other as PaginationModelData
-
-        if (pageSpacingPx != other.pageSpacingPx) return false
-        if (maxPages != other.maxPages) return false
-        if (!pages.contentEquals(other.pages)) return false
-        if (!pageStops.contentEquals(other.pageStops)) return false
-        if (size != other.size) return false
-        if (estimatedPageHeight != other.estimatedPageHeight) return false
-        if (accumulatedPageSize != other.accumulatedPageSize) return false
-
-        return true
-    }
-
-    override fun hashCode(): Int {
-        var result = pageSpacingPx
-        result = 31 * result + maxPages
-        result = 31 * result + pages.contentHashCode()
-        result = 31 * result + pageStops.contentHashCode()
-        result = 31 * result + size
-        result = 31 * result + estimatedPageHeight.hashCode()
-        result = 31 * result + accumulatedPageSize
-        return result
-    }
-}
diff --git a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
index 30ebfcb..6599830 100644
--- a/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
+++ b/pdf/pdf-viewer/src/main/java/androidx/pdf/viewer/loader/PdfLoaderCallbacksImpl.kt
@@ -228,9 +228,6 @@
                         "Document not loaded but status " + status.number
                     )
                 PdfStatus.PDF_ERROR -> {
-                    loadingView.showErrorView(
-                        context.resources.getString(R.string.error_cannot_open_pdf)
-                    )
                     handleError(status)
                 }
                 PdfStatus.FILE_ERROR,
@@ -259,7 +256,9 @@
 
     override fun setPageDimensions(pageNum: Int, dimensions: Dimensions) {
         if (viewState.get() != ViewState.NO_VIEW) {
+
             paginatedView.model.addPage(pageNum, dimensions)
+
             layoutHandler!!.pageLayoutReach = paginatedView.model.size
 
             if (
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
index 86fed41..79fdec1 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/BaseFragment.kt
@@ -17,8 +17,11 @@
 package androidx.privacysandbox.ui.integration.testapp
 
 import android.app.Activity
+import android.graphics.Color
+import android.graphics.Typeface
 import android.os.Bundle
 import android.util.Log
+import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
 import androidx.fragment.app.Fragment
@@ -59,6 +62,10 @@
         }
     }
 
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        getSandboxedSdkViews().forEach { it.addStateChangedListener() }
+    }
+
     /** Returns a handle to the already loaded SDK. */
     fun getSdkApi(): ISdkApi {
         return sdkApi
@@ -119,6 +126,8 @@
                 val parent = view.parent as ViewGroup
                 val index = parent.indexOfChild(view)
                 val textView = TextView(requireActivity())
+                textView.setTypeface(null, Typeface.BOLD_ITALIC)
+                textView.setTextColor(Color.RED)
                 textView.text = state.throwable.message
 
                 requireActivity().runOnUiThread {
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
index 4a9ccf7..d3eba3b 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/PoolingContainerFragment.kt
@@ -104,6 +104,7 @@
                 } catch (e: Exception) {
                     Log.w(TAG, "Ad not loaded $e")
                 }
+                childSandboxedSdkView.addStateChangedListener()
                 sandboxedSdkViewSet.add(childSandboxedSdkView)
             }
         }
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
index 3a3f32b..166d109 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ResizeFragment.kt
@@ -67,7 +67,6 @@
     }
 
     private fun loadResizableBannerAd() {
-        resizableBannerView.addStateChangedListener()
         loadBannerAd(
             currentAdType,
             currentMediationOption,
diff --git a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
index e49be51..2d38fe7 100644
--- a/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
+++ b/privacysandbox/ui/integration-tests/testapp/src/main/java/androidx/privacysandbox/ui/integration/testapp/ScrollFragment.kt
@@ -61,7 +61,6 @@
     }
 
     private fun loadBottomBannerAd() {
-        bottomBannerView.addStateChangedListener()
         bottomBannerView.layoutParams =
             inflatedView.findViewById<LinearLayout>(R.id.bottom_banner_container).layoutParams
         requireActivity().runOnUiThread {
diff --git a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
index 5cb971c..1a1f6d9 100644
--- a/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
+++ b/room/room-compiler-processing/src/main/java/androidx/room/compiler/codegen/java/JavaFunSpec.kt
@@ -53,16 +53,19 @@
             name: String,
             annotations: List<XAnnotationSpec>
         ) = apply {
+            val paramSpec = ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
             actual.addParameter(
-                ParameterSpec.builder(typeName.java, name, Modifier.FINAL)
-                    .apply {
-                        if (typeName.nullability == XNullability.NULLABLE) {
-                            addAnnotation(NULLABLE_ANNOTATION)
-                        } else if (typeName.nullability == XNullability.NONNULL) {
-                            addAnnotation(NONNULL_ANNOTATION)
-                        }
-                    }
-                    .build()
+                // Adding nullability annotation to primitive parameters is redundant as
+                // primitives can never be null.
+                if (typeName.isPrimitive) {
+                    paramSpec.build()
+                } else {
+                    when (typeName.nullability) {
+                        XNullability.NULLABLE -> paramSpec.addAnnotation(NULLABLE_ANNOTATION)
+                        XNullability.NONNULL -> paramSpec.addAnnotation(NONNULL_ANNOTATION)
+                        else -> paramSpec
+                    }.build()
+                }
             )
             // TODO(b/247247439): Add other annotations
         }
diff --git a/room/room-compiler/build.gradle b/room/room-compiler/build.gradle
index 060f27d..ff92f34 100644
--- a/room/room-compiler/build.gradle
+++ b/room/room-compiler/build.gradle
@@ -90,6 +90,7 @@
     testImplementation(libs.antlr4)
     testImplementation(SdkHelperKt.getSdkDependency(project))
     testImplementationAarAsJar(project(":room:room-runtime"))
+    testImplementationAarAsJar(project(":room:room-paging"))
     testImplementationAarAsJar(project(":sqlite:sqlite"))
     testImplementation(project(":internal-testutils-common"))
 }
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 29cbd79..d34aa80 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -82,6 +82,7 @@
 import org.hamcrest.CoreMatchers.notNullValue
 import org.hamcrest.CoreMatchers.nullValue
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Ignore
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -607,6 +608,7 @@
     }
 
     @Test
+    @Ignore("Temporarily disabling to unblock b/362512509")
     fun testMissingRoomPaging() {
         runProcessorTest { invocation ->
             val pagingSourceElement =
diff --git a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
index ef3d537..6502b66 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/input/ComplexDao.java
@@ -16,6 +16,7 @@
 
 package foo.bar;
 import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
 import androidx.room.*;
 import androidx.sqlite.db.SupportSQLiteQuery;
 import com.google.common.util.concurrent.ListenableFuture;
@@ -85,4 +86,7 @@
 
     @RawQuery(observedEntities = User.class)
     abstract public User getUserViaRawQuery(SupportSQLiteQuery rawQuery);
+
+    @Query("SELECT * FROM Child1 ORDER BY id ASC")
+    abstract public PagingSource<Integer, Child1> loadItems();
 }
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
index 0fa55d4..6821f7a 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withLambda/ComplexDao.java
@@ -3,8 +3,11 @@
 import android.database.Cursor;
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
 import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
 import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
 import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
 import androidx.room.util.SQLiteStatementUtil;
@@ -627,6 +630,51 @@
   }
 
   @Override
+  public PagingSource<Integer, Child1> loadItems() {
+    final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+    final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+    return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+      @Override
+      @NonNull
+      protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+              final int itemCount) {
+        _rawQuery.getBindingFunction().invoke(statement);
+        final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+        final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+        final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+        final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+        final List<Child1> _result = new ArrayList<Child1>();
+        while (statement.step()) {
+          final Child1 _item;
+          final int _tmpId;
+          _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+          final String _tmpName;
+          if (statement.isNull(_cursorIndexOfName)) {
+            _tmpName = null;
+          } else {
+            _tmpName = statement.getText(_cursorIndexOfName);
+          }
+          final Info _tmpInfo;
+          if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+            _tmpInfo = new Info();
+            _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+            if (statement.isNull(_cursorIndexOfCode)) {
+              _tmpInfo.code = null;
+            } else {
+              _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+            }
+          } else {
+            _tmpInfo = null;
+          }
+          _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+          _result.add(_item);
+        }
+        return _result;
+      }
+    };
+  }
+
+  @Override
   public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
     __db.assertNotSuspendingTransaction();
     final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
index 8074e12..b4dceb9 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/javac/withoutLambda/ComplexDao.java
@@ -4,8 +4,11 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
 import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
 import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
 import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
 import androidx.room.util.SQLiteStatementUtil;
@@ -690,6 +693,51 @@
   }
 
   @Override
+  public PagingSource<Integer, Child1> loadItems() {
+    final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+    final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+    return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+      @Override
+      @NonNull
+      protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+              final int itemCount) {
+        _rawQuery.getBindingFunction().invoke(statement);
+        final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+        final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+        final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+        final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+        final List<Child1> _result = new ArrayList<Child1>();
+        while (statement.step()) {
+          final Child1 _item;
+          final int _tmpId;
+          _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+          final String _tmpName;
+          if (statement.isNull(_cursorIndexOfName)) {
+            _tmpName = null;
+          } else {
+            _tmpName = statement.getText(_cursorIndexOfName);
+          }
+          final Info _tmpInfo;
+          if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+            _tmpInfo = new Info();
+            _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+            if (statement.isNull(_cursorIndexOfCode)) {
+              _tmpInfo.code = null;
+            } else {
+              _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+            }
+          } else {
+            _tmpInfo = null;
+          }
+          _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+          _result.add(_item);
+        }
+        return _result;
+      }
+    };
+  }
+
+  @Override
   public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
     __db.assertNotSuspendingTransaction();
     final Cursor _cursor = DBUtil.query(__db, rawQuery, false, null);
diff --git a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
index 54daec0..9b4ca9b 100644
--- a/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
+++ b/room/room-compiler/src/test/test-data/daoWriter/output/ksp/ComplexDao.java
@@ -3,8 +3,11 @@
 import android.database.Cursor;
 import androidx.annotation.NonNull;
 import androidx.lifecycle.LiveData;
+import androidx.paging.PagingSource;
 import androidx.room.RoomDatabase;
+import androidx.room.RoomRawQuery;
 import androidx.room.guava.GuavaRoom;
+import androidx.room.paging.LimitOffsetPagingSource;
 import androidx.room.util.CursorUtil;
 import androidx.room.util.DBUtil;
 import androidx.room.util.SQLiteStatementUtil;
@@ -622,6 +625,51 @@
     });
   }
 
+    @Override
+    public PagingSource<Integer, Child1> loadItems() {
+        final String _sql = "SELECT * FROM Child1 ORDER BY id ASC";
+        final RoomRawQuery _rawQuery = new RoomRawQuery(_sql);
+        return new LimitOffsetPagingSource<Child1>(_rawQuery, __db, "Child1") {
+            @Override
+            @NonNull
+            protected List<Child1> convertRows(@NonNull final SQLiteStatement statement,
+                    final int itemCount) {
+                _rawQuery.getBindingFunction().invoke(statement);
+                final int _cursorIndexOfId = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "id");
+                final int _cursorIndexOfName = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "name");
+                final int _cursorIndexOfSerial = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "serial");
+                final int _cursorIndexOfCode = SQLiteStatementUtil.getColumnIndexOrThrow(statement, "code");
+                final List<Child1> _result = new ArrayList<Child1>();
+                while (statement.step()) {
+                    final Child1 _item;
+                    final int _tmpId;
+                    _tmpId = (int) (statement.getLong(_cursorIndexOfId));
+                    final String _tmpName;
+                    if (statement.isNull(_cursorIndexOfName)) {
+                        _tmpName = null;
+                    } else {
+                        _tmpName = statement.getText(_cursorIndexOfName);
+                    }
+                    final Info _tmpInfo;
+                    if (!(statement.isNull(_cursorIndexOfSerial) && statement.isNull(_cursorIndexOfCode))) {
+                        _tmpInfo = new Info();
+                        _tmpInfo.serial = (int) (statement.getLong(_cursorIndexOfSerial));
+                        if (statement.isNull(_cursorIndexOfCode)) {
+                            _tmpInfo.code = null;
+                        } else {
+                            _tmpInfo.code = statement.getText(_cursorIndexOfCode);
+                        }
+                    } else {
+                        _tmpInfo = null;
+                    }
+                    _item = new Child1(_tmpId,_tmpName,_tmpInfo);
+                    _result.add(_item);
+                }
+                return _result;
+            }
+        };
+    }
+
   @Override
   public User getUserViaRawQuery(final SupportSQLiteQuery rawQuery) {
     __db.assertNotSuspendingTransaction();
diff --git a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
index 5115d6f..0deb550 100644
--- a/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
+++ b/room/room-runtime/src/androidMain/kotlin/androidx/room/RoomTrackingLiveData.android.kt
@@ -22,8 +22,8 @@
 import androidx.sqlite.SQLiteConnection
 import java.util.concurrent.Callable
 import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.coroutines.EmptyCoroutineContext
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 
 /**
  * A LiveData implementation that closely works with [InvalidationTracker] to implement database
@@ -53,6 +53,17 @@
     private val computing = AtomicBoolean(false)
     private val registeredObserver = AtomicBoolean(false)
 
+    private val launchContext =
+        if (database.inCompatibilityMode()) {
+            if (inTransaction) {
+                database.getTransactionContext()
+            } else {
+                database.getQueryContext()
+            }
+        } else {
+            EmptyCoroutineContext
+        }
+
     private suspend fun refresh() {
         if (registeredObserver.compareAndSet(false, true)) {
             database.invalidationTracker.subscribe(
@@ -105,7 +116,7 @@
         val isActive = hasActiveObservers()
         if (invalid.compareAndSet(false, true)) {
             if (isActive) {
-                database.getCoroutineScope().launch { refresh() }
+                database.getCoroutineScope().launch(launchContext) { refresh() }
             }
         }
     }
@@ -115,7 +126,7 @@
     override fun onActive() {
         super.onActive()
         container.onActive(this)
-        database.getCoroutineScope().launch { refresh() }
+        database.getCoroutineScope().launch(launchContext) { refresh() }
     }
 
     override fun onInactive() {
@@ -132,13 +143,7 @@
     private val callableFunction: Callable<T?>
 ) : RoomTrackingLiveData<T>(database, container, inTransaction, tableNames) {
     override suspend fun compute(): T? {
-        val queryContext =
-            if (inTransaction) {
-                database.getTransactionContext()
-            } else {
-                database.getQueryContext()
-            }
-        return withContext(queryContext) { callableFunction.call() }
+        return callableFunction.call()
     }
 }
 
diff --git a/settings.gradle b/settings.gradle
index 988cca5..ca5d8f6 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -29,7 +29,12 @@
         classpath("com.gradle:develocity-gradle-plugin:3.18")
         classpath("com.gradle:common-custom-user-data-gradle-plugin:2.0.1")
         classpath("androidx.build.gradle.gcpbuildcache:gcpbuildcache:1.0.0-beta10")
-        classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+        def agpOverride = System.getenv("GRADLE_PLUGIN_VERSION")
+        if (agpOverride != null) {
+            classpath("com.android.settings:com.android.settings.gradle.plugin:$agpOverride")
+        } else {
+            classpath("com.android.settings:com.android.settings.gradle.plugin:8.7.0-alpha02")
+        }
     }
 }
 
diff --git a/wear/compose/compose-foundation/api/current.txt b/wear/compose/compose-foundation/api/current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/current.txt
+++ b/wear/compose/compose-foundation/api/current.txt
@@ -388,8 +388,10 @@
 
   public sealed interface LazyColumnLayoutInfo {
     method public int getTotalItemsCount();
+    method public long getViewportSize();
     method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
     property public abstract int totalItemsCount;
+    property public abstract long viewportSize;
     property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
   }
 
diff --git a/wear/compose/compose-foundation/api/restricted_current.txt b/wear/compose/compose-foundation/api/restricted_current.txt
index ea38e1a..596925c 100644
--- a/wear/compose/compose-foundation/api/restricted_current.txt
+++ b/wear/compose/compose-foundation/api/restricted_current.txt
@@ -388,8 +388,10 @@
 
   public sealed interface LazyColumnLayoutInfo {
     method public int getTotalItemsCount();
+    method public long getViewportSize();
     method public java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> getVisibleItems();
     property public abstract int totalItemsCount;
+    property public abstract long viewportSize;
     property public abstract java.util.List<androidx.wear.compose.foundation.lazy.LazyColumnVisibleItemInfo> visibleItems;
   }
 
diff --git a/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
new file mode 100644
index 0000000..f07a6b7
--- /dev/null
+++ b/wear/compose/compose-foundation/src/androidTest/kotlin/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfoTest.kt
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.foundation.lazy
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class LazyColumnLayoutInfoTest {
+    @get:Rule val rule = createComposeRule()
+
+    private var itemSizePx: Int = 50
+    private var itemSizeDp: Dp = Dp.Infinity
+
+    @Before
+    fun before() {
+        with(rule.density) { itemSizeDp = itemSizePx.toDp() }
+    }
+
+    @Test
+    fun visibleItemsAreCorrect() {
+        lateinit var state: LazyColumnState
+
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                // Viewport take 4 items, item 0 is exactly above the center and there is space for
+                // two more items below the center line.
+                modifier = Modifier.requiredSize(itemSizeDp * 4f),
+                verticalArrangement = Arrangement.spacedBy(0.dp)
+            ) {
+                items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+            // Start offset compensates for the layout where the first item is exactly above the
+            // center line.
+            state.layoutInfo.assertVisibleItems(count = 3, startOffset = itemSizePx)
+        }
+    }
+
+    @Test
+    fun visibleItemsAreCorrectWithSpacing() {
+        lateinit var state: LazyColumnState
+
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                // Viewport take 4 items, item 0 is exactly above the center and there is space for
+                // two more items below the center line.
+                modifier = Modifier.requiredSize(itemSizeDp * 4f),
+                verticalArrangement = Arrangement.spacedBy(itemSizeDp),
+            ) {
+                items((0..5).toList()) { Box(Modifier.requiredSize(itemSizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize.height).isEqualTo(itemSizePx * 4)
+            // Start offset compensates for the layout where the first item is exactly above the
+            // center line.
+            state.layoutInfo.assertVisibleItems(
+                count = 2,
+                spacing = itemSizePx,
+                startOffset = itemSizePx
+            )
+        }
+    }
+
+    @Test
+    fun visibleItemsAreObservableWhenResize() {
+        lateinit var state: LazyColumnState
+        var size by mutableStateOf(itemSizeDp * 2)
+        var currentInfo: LazyColumnLayoutInfo? = null
+        @Composable
+        fun observingFun() {
+            currentInfo = state.layoutInfo
+        }
+        rule.setContent {
+            LazyColumn(
+                state = rememberLazyColumnState().also { state = it },
+                modifier = Modifier.requiredSize(itemSizeDp * 4f)
+            ) {
+                item { Box(Modifier.requiredSize(size)) }
+            }
+            observingFun()
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx * 2)
+            currentInfo = null
+            size = itemSizeDp
+        }
+
+        rule.runOnIdle {
+            assertThat(currentInfo).isNotNull()
+            currentInfo!!.assertVisibleItems(count = 1, expectedSize = itemSizePx)
+        }
+    }
+
+    @Test
+    fun totalCountIsCorrect() {
+        var count by mutableStateOf(10)
+        lateinit var state: LazyColumnState
+        rule.setContent {
+            LazyColumn(state = rememberLazyColumnState().also { state = it }) {
+                items((0 until count).toList()) { Box(Modifier.requiredSize(10.dp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.totalItemsCount).isEqualTo(10)
+            count = 20
+        }
+
+        rule.runOnIdle { assertThat(state.layoutInfo.totalItemsCount).isEqualTo(20) }
+    }
+
+    @Test
+    fun viewportOffsetsAndSizeAreCorrect() {
+        val sizePx = 45
+        val sizeDp = with(rule.density) { sizePx.toDp() }
+        lateinit var state: LazyColumnState
+        rule.setContent {
+            LazyColumn(
+                Modifier.height(sizeDp).width(sizeDp * 2),
+                state = rememberLazyColumnState().also { state = it }
+            ) {
+                items((0..3).toList()) { Box(Modifier.requiredSize(sizeDp)) }
+            }
+        }
+
+        rule.runOnIdle {
+            assertThat(state.layoutInfo.viewportSize).isEqualTo(IntSize(sizePx * 2, sizePx))
+        }
+    }
+
+    private fun LazyColumnLayoutInfo.assertVisibleItems(
+        count: Int,
+        startIndex: Int = 0,
+        startOffset: Int = 0,
+        expectedSize: Int = itemSizePx,
+        spacing: Int = 0
+    ) {
+        assertThat(visibleItems.size).isEqualTo(count)
+        var currentIndex = startIndex
+        var currentOffset = startOffset
+        visibleItems.forEach {
+            assertThat(it.index).isEqualTo(currentIndex)
+            assertWithMessage("Offset of item $currentIndex")
+                .that(it.offset)
+                .isEqualTo(currentOffset)
+            assertThat(it.height).isEqualTo(expectedSize)
+            currentIndex++
+            currentOffset += it.height + spacing
+        }
+    }
+}
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
index c5002a0..14fc4c4 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnLayoutInfo.kt
@@ -16,6 +16,8 @@
 
 package androidx.wear.compose.foundation.lazy
 
+import androidx.compose.ui.unit.IntSize
+
 /**
  * Scroll progress of an item in a [LazyColumn] before any modifications to the item's height are
  * applied (using [LazyColumnItemScope.transformedHeight] modifier).
@@ -44,21 +46,26 @@
 sealed interface LazyColumnVisibleItemInfo {
     /** The index of the item in the underlying data source. */
     val index: Int
+
     /** The offset of the item from the start of the visible area. */
     val offset: Int
+
     /** The height of the item after applying any height changes. */
     val height: Int
+
     /** The scroll progress of the item, indicating its position within the visible area. */
     val scrollProgress: LazyColumnItemScrollProgress
 }
 
 /** Holds the layout information for a [LazyColumn]. */
 sealed interface LazyColumnLayoutInfo {
+
     /** A list of [LazyColumnVisibleItemInfo] objects representing the visible items in the list. */
     val visibleItems: List<LazyColumnVisibleItemInfo>
 
     /** The total count of items passed to [LazyColumn]. */
     val totalItemsCount: Int
 
-    // TODO: b/352686661 - Expose more properties related to layout.
+    /** The size of the viewport in pixels. */
+    val viewportSize: IntSize
 }
diff --git a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
index 3f316f2..1bd6f3e 100644
--- a/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
+++ b/wear/compose/compose-foundation/src/main/java/androidx/wear/compose/foundation/lazy/LazyColumnMeasureResult.kt
@@ -17,6 +17,7 @@
 package androidx.wear.compose.foundation.lazy
 
 import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.unit.IntSize
 
 /** The result of the measure pass of the [LazyColumn]. */
 internal class LazyColumnMeasureResult(
@@ -32,4 +33,8 @@
     override val visibleItems: List<LazyColumnVisibleItemInfo>,
     /** see [LazyColumnLayoutInfo.totalItemsCount] */
     override val totalItemsCount: Int,
-) : LazyColumnLayoutInfo, MeasureResult by measureResult
+) : LazyColumnLayoutInfo, MeasureResult by measureResult {
+    /** see [LazyColumnLayoutInfo.viewportSize] */
+    override val viewportSize: IntSize
+        get() = IntSize(width = width, height = height)
+}
diff --git a/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
new file mode 100644
index 0000000..e9e624a
--- /dev/null
+++ b/wear/compose/compose-material3/benchmark/src/androidTest/java/androidx/wear/compose/material3/benchmark/ProgressIndicatorBenchmark.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.wear.compose.material3.benchmark
+
+import androidx.compose.runtime.Composable
+import androidx.compose.testutils.LayeredComposeTestCase
+import androidx.compose.testutils.benchmark.ComposeBenchmarkRule
+import androidx.compose.testutils.benchmark.benchmarkToFirstPixel
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.MediumTest
+import androidx.wear.compose.material3.CircularProgressIndicator
+import androidx.wear.compose.material3.MaterialTheme
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@MediumTest
+@RunWith(AndroidJUnit4::class)
+class ProgressIndicatorBenchmark {
+    @get:Rule val benchmarkRule = ComposeBenchmarkRule()
+
+    private val testCaseFactory = { ProgressIndicatorTestCase() }
+
+    @Test
+    fun first_pixel() {
+        benchmarkRule.benchmarkToFirstPixel(testCaseFactory)
+    }
+}
+
+internal class ProgressIndicatorTestCase : LayeredComposeTestCase() {
+    @Composable
+    override fun MeasuredContent() {
+        CircularProgressIndicator(progress = { 0.5f })
+    }
+
+    @Composable
+    override fun ContentWrappers(content: @Composable () -> Unit) {
+        MaterialTheme { content() }
+    }
+}
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
index e049442..7074d90 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogScreenshotTest.kt
@@ -64,12 +64,12 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = false,
-            showText = false,
             showContent = false,
             showTwoButtons = false,
             scrollToBottom = false,
             screenSize = screenSize,
-            titleText = "Network error"
+            titleText = "Network error",
+            messageText = null
         )
 
     @Test
@@ -78,12 +78,12 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = false,
-            showText = false,
             showContent = false,
             showTwoButtons = true,
             scrollToBottom = false,
             screenSize = screenSize,
-            titleText = "Network error"
+            titleText = "Network error",
+            messageText = null
         )
 
     @Test
@@ -92,11 +92,11 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = false,
-            showText = false,
             showContent = false,
             showTwoButtons = false,
             scrollToBottom = false,
             screenSize = screenSize,
+            messageText = null,
         )
 
     @Test
@@ -105,11 +105,11 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = false,
-            showText = false,
             showContent = false,
             showTwoButtons = true,
             scrollToBottom = false,
-            screenSize = screenSize
+            screenSize = screenSize,
+            messageText = null
         )
 
     @Test
@@ -118,11 +118,11 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = false,
             showContent = false,
             showTwoButtons = false,
             scrollToBottom = false,
-            screenSize = screenSize
+            screenSize = screenSize,
+            messageText = null
         )
     }
 
@@ -132,11 +132,11 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = false,
             showContent = false,
             showTwoButtons = true,
             scrollToBottom = false,
-            screenSize = screenSize
+            screenSize = screenSize,
+            messageText = null
         )
     }
 
@@ -146,7 +146,6 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = true,
             showContent = false,
             showTwoButtons = false,
             scrollToBottom = false,
@@ -162,7 +161,6 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = true,
             showContent = true,
             showTwoButtons = false,
             scrollToBottom = false,
@@ -178,7 +176,6 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = true,
             showContent = true,
             showTwoButtons = false,
             scrollToBottom = true,
@@ -194,7 +191,6 @@
             testName = testName,
             screenshotRule = screenshotRule,
             showIcon = true,
-            showText = true,
             showContent = true,
             showTwoButtons = true,
             scrollToBottom = true,
@@ -202,15 +198,43 @@
         )
     }
 
+    @Test
+    fun alert_title_longMessageText_bottomButton(@TestParameter screenSize: ScreenSize) {
+        rule.verifyAlertDialogScreenshot(
+            testName = testName,
+            screenshotRule = screenshotRule,
+            showIcon = false,
+            showContent = false,
+            showTwoButtons = false,
+            scrollToBottom = false,
+            screenSize = screenSize,
+            messageText = longMessageText
+        )
+    }
+
+    @Test
+    fun alert_title_longMessageText_confirmDismissButtons(@TestParameter screenSize: ScreenSize) {
+        rule.verifyAlertDialogScreenshot(
+            testName = testName,
+            screenshotRule = screenshotRule,
+            showIcon = false,
+            showContent = false,
+            showTwoButtons = true,
+            scrollToBottom = false,
+            screenSize = screenSize,
+            messageText = longMessageText
+        )
+    }
+
     private fun ComposeContentTestRule.verifyAlertDialogScreenshot(
         testName: TestName,
         screenshotRule: AndroidXScreenshotTestRule,
         showIcon: Boolean,
-        showText: Boolean,
         showContent: Boolean,
         showTwoButtons: Boolean,
         scrollToBottom: Boolean,
         screenSize: ScreenSize,
+        messageText: String? = "Your battery is low. Turn on battery saver.",
         titleText: String = "Mobile network is not currently available"
     ) {
         setContentWithTheme() {
@@ -239,8 +263,8 @@
                         } else null,
                     showTwoButtons = showTwoButtons,
                     text =
-                        if (showText) {
-                            { Text("Your battery is low. Turn on battery saver.") }
+                        if (messageText != null) {
+                            { Text(messageText) }
                         } else null,
                     content =
                         if (showContent) {
@@ -308,3 +332,6 @@
         )
     }
 }
+
+internal const val longMessageText =
+    "Allow Map to access your location even when you're not using the app? Your location is used to automatically map places to activities."
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
index 6e59369..f5f3893 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/AlertDialogTest.kt
@@ -259,6 +259,48 @@
     }
 
     @Test
+    fun calls_onDismissRequest_when_dialogBottomButton_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            AlertDialog(
+                modifier = Modifier.testTag(TEST_TAG),
+                title = {},
+                bottomButton = {},
+                onDismissRequest = { dismissed = true },
+                show = show.value
+            )
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
+    fun calls_onDismissRequest_when_dialogConfirmDismissButtons_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            AlertDialog(
+                modifier = Modifier.testTag(TEST_TAG),
+                title = {},
+                confirmButton = {},
+                onDismissRequest = { dismissed = true },
+                show = show.value
+            )
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
     fun applies_correct_titleProperties() {
         var expectedContentColor: Color = Color.Unspecified
         var expectedTextStyle: TextStyle = TextStyle.Default
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
index 31206a1..31069ee 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/ConfirmationTest.kt
@@ -212,6 +212,86 @@
     }
 
     @Test
+    fun calls_onDismissRequest_when_confirmationLinearText_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            Confirmation(
+                modifier = Modifier.testTag(TEST_TAG),
+                onDismissRequest = { dismissed = true },
+                text = {},
+                show = show.value
+            ) {}
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
+    fun calls_onDismissRequest_when_confirmationCurvedText_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            Confirmation(
+                modifier = Modifier.testTag(TEST_TAG),
+                onDismissRequest = { dismissed = true },
+                curvedText = {},
+                show = show.value
+            ) {}
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
+    fun calls_onDismissRequest_when_successConfirmation_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            SuccessConfirmation(
+                modifier = Modifier.testTag(TEST_TAG),
+                onDismissRequest = { dismissed = true },
+                curvedText = {},
+                show = show.value
+            )
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
+    fun calls_onDismissRequest_when_failureConfirmation_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            FailureConfirmation(
+                modifier = Modifier.testTag(TEST_TAG),
+                onDismissRequest = { dismissed = true },
+                curvedText = {},
+                show = show.value
+            )
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
     fun confirmation_displays_icon_with_linearText() {
         rule.setContentWithTheme {
             Confirmation(
diff --git a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
index c5de9c1..e8b933a 100644
--- a/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
+++ b/wear/compose/compose-material3/src/androidTest/kotlin/androidx/wear/compose/material3/OpenOnPhoneDialogTest.kt
@@ -68,6 +68,25 @@
     }
 
     @Test
+    fun calls_onDismissRequest_when_openOnPhone_becomes_hidden() {
+        val show = mutableStateOf(true)
+        var dismissed = false
+
+        rule.setContentWithTheme {
+            OpenOnPhoneDialog(
+                modifier = Modifier.testTag(TEST_TAG),
+                onDismissRequest = { dismissed = true },
+                show = show.value
+            )
+        }
+        rule.waitForIdle()
+        show.value = false
+
+        rule.waitForIdle()
+        assert(dismissed)
+    }
+
+    @Test
     fun hides_openOnPhone_when_show_false() {
         rule.setContentWithTheme {
             OpenOnPhoneDialog(
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
index f0cc000..a20ffa0 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/AlertDialog.kt
@@ -67,9 +67,11 @@
  * Example of an [AlertDialog] with an icon, title and two buttons to confirm and dismiss:
  *
  * @sample androidx.wear.compose.material3.samples.AlertDialogWithConfirmAndDismissSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ *   'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ *   'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping
- *   right (typically also called by the [dismissButton]).
+ *   right (typically also called by the [dismissButton]) or by other dismiss action.
  * @param confirmButton A slot for a [Button] indicating positive sentiment. Clicking the button
  *   must remove the dialog from the composition hierarchy. It's recommended to use
  *   [AlertDialogDefaults.ConfirmButton] in this slot with onClick callback.
@@ -138,7 +140,9 @@
  * Example of an [AlertDialog] with content groups and a bottom [EdgeButton]:
  *
  * @sample androidx.wear.compose.material3.samples.AlertDialogWithContentGroupsSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ *   'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ *   'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed by swiping to
  *   the right or by other dismiss action.
  * @param bottomButton A slot for a [EdgeButton] indicating positive sentiment. Clicking the button
@@ -338,7 +342,7 @@
     alertButtonsParams: AlertButtonsParams,
     content: (ScalingLazyListScope.() -> Unit)?
 ) {
-    val state = rememberScalingLazyListState()
+    val state = rememberScalingLazyListState(initialCenterItemIndex = 0)
 
     Dialog(
         showDialog = show,
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
index 0a94d14..2328f65 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Confirmation.kt
@@ -70,9 +70,12 @@
  * Example of a [Confirmation] with an icon and a curved text content:
  *
  * @sample androidx.wear.compose.material3.samples.ConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ *   an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ *   false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ *   becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- *   swiping right or when the [durationMillis] has passed.
+ *   swiping right, when the [durationMillis] has passed or by other dismiss action.
  * @param curvedText A slot for displaying curved text content which will be shown along the bottom
  *   edge of the dialog.
  * @param modifier Modifier to be applied to the confirmation content.
@@ -120,9 +123,12 @@
  * Example of a [Confirmation] with an icon and a text which fits into 3 lines:
  *
  * @sample androidx.wear.compose.material3.samples.LongTextConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ *   an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ *   false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ *   becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- *   swiping right or when the [durationMillis] has passed.
+ *   swiping right, when the [durationMillis] has passed or by other dismiss action.
  * @param text A slot for displaying text below the icon. It should not exceed 3 lines.
  * @param modifier Modifier to be applied to the confirmation content.
  * @param colors A [ConfirmationColors] object for customizing the colors used in this
@@ -210,9 +216,12 @@
  * Example of a [SuccessConfirmation] usage:
  *
  * @sample androidx.wear.compose.material3.samples.SuccessConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ *   an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ *   false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ *   becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- *   swiping right or when the [durationMillis] has passed.
+ *   swiping right, when the [durationMillis] has passed or by other dismiss action.
  * @param modifier Modifier to be applied to the confirmation content.
  * @param curvedText A slot for displaying curved text content which will be shown along the bottom
  *   edge of the dialog. Defaults to a localized success message.
@@ -259,9 +268,12 @@
  * Example of a [FailureConfirmation] usage:
  *
  * @sample androidx.wear.compose.material3.samples.FailureConfirmationSample
- * @param show A boolean indicating whether the confirmation should be displayed.
+ * @param show A boolean indicating whether the confirmation should be displayed. When set to true
+ *   an 'intro' animation is triggered and the confirmation is displayed. Subsequently, when set to
+ *   false an 'outro' animation is triggered, then [onDismissRequest] is called and confirmation
+ *   becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- *   swiping right or when the [durationMillis] has passed.
+ *   swiping right, when the [durationMillis] has passed or by other dismiss action.
  * @param modifier Modifier to be applied to the confirmation content.
  * @param curvedText A slot for displaying curved text content which will be shown along the bottom
  *   edge of the dialog. Defaults to a localized failure message.
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
index f5c6dc6..c119f66 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/Dialog.kt
@@ -103,18 +103,17 @@
                     transitionState.targetState = DialogVisibility.Hide
                 }
             }
-
-            LaunchedEffect(transitionState.currentState) {
-                if (
-                    pendingOnDismissCall &&
-                        transitionState.currentState == DialogVisibility.Hide &&
-                        transitionState.isIdle
-                ) {
-                    // After the outro animation, leave the dialog & reset alpha/scale transitions.
-                    onDismissRequest()
-                    pendingOnDismissCall = false
-                }
-            }
+        }
+    }
+    LaunchedEffect(transitionState.currentState) {
+        if (
+            pendingOnDismissCall &&
+                transitionState.currentState == DialogVisibility.Hide &&
+                transitionState.isIdle
+        ) {
+            // After the outro animation, leave the dialog & reset alpha/scale transitions.
+            onDismissRequest()
+            pendingOnDismissCall = false
         }
     }
 }
diff --git a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
index fa819c7..bea3905 100644
--- a/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
+++ b/wear/compose/compose-material3/src/main/java/androidx/wear/compose/material3/OpenOnPhoneDialog.kt
@@ -72,9 +72,11 @@
  * Example of an [OpenOnPhoneDialog] usage:
  *
  * @sample androidx.wear.compose.material3.samples.OpenOnPhoneDialogSample
- * @param show A boolean indicating whether the dialog should be displayed.
+ * @param show A boolean indicating whether the dialog should be displayed. When set to true an
+ *   'intro' animation is triggered and the dialog is displayed. Subsequently, when set to false an
+ *   'outro' animation is triggered, then [onDismissRequest] is called and dialog becomes hidden.
  * @param onDismissRequest A lambda function to be called when the dialog is dismissed - either by
- *   swiping right or when the [durationMillis] has passed.
+ *   swiping right, when the [durationMillis] has passed or by other dismiss action.
  * @param modifier Modifier to be applied to the dialog content.
  * @param curvedText A slot for displaying curved text content which will be shown along the bottom
  *   edge of the dialog. Defaults to a localized open on phone message.
diff --git a/window/window-core/api/current.txt b/window/window-core/api/current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/current.txt
+++ b/window/window-core/api/current.txt
@@ -32,9 +32,9 @@
     method public int getMinWidthDp();
     method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
     method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
-    method public boolean isAtLeast(int widthDp, int heightDp);
-    method public boolean isHeightAtLeast(int heightDp);
-    method public boolean isWidthAtLeast(int widthDp);
+    method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+    method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+    method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
     property public final int minHeightDp;
     property public final int minWidthDp;
     property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/api/restricted_current.txt b/window/window-core/api/restricted_current.txt
index 2c8a4d2..8b80f00 100644
--- a/window/window-core/api/restricted_current.txt
+++ b/window/window-core/api/restricted_current.txt
@@ -32,9 +32,9 @@
     method public int getMinWidthDp();
     method @Deprecated public androidx.window.core.layout.WindowHeightSizeClass getWindowHeightSizeClass();
     method @Deprecated public androidx.window.core.layout.WindowWidthSizeClass getWindowWidthSizeClass();
-    method public boolean isAtLeast(int widthDp, int heightDp);
-    method public boolean isHeightAtLeast(int heightDp);
-    method public boolean isWidthAtLeast(int widthDp);
+    method public boolean isAtLeastBreakpoint(int widthBreakpointDp, int heightBreakpointDp);
+    method public boolean isHeightAtLeastBreakpoint(int heightBreakpointDp);
+    method public boolean isWidthAtLeastBreakpoint(int widthBreakpointDp);
     property public final int minHeightDp;
     property public final int minWidthDp;
     property @Deprecated public final androidx.window.core.layout.WindowHeightSizeClass windowHeightSizeClass;
diff --git a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
index 43bef20..847ea73 100644
--- a/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
+++ b/window/window-core/src/commonMain/kotlin/androidx/window/core/layout/WindowSizeClass.kt
@@ -80,25 +80,28 @@
         get() = WindowHeightSizeClass.compute(minHeightDp.toFloat())
 
     /**
-     * Returns `true` when [widthDp] is greater than or equal to [minWidthDp], `false` otherwise.
+     * Returns `true` when [minWidthDp] is greater than or equal to [widthBreakpointDp], `false`
+     * otherwise.
      */
-    fun isWidthAtLeast(widthDp: Int): Boolean {
-        return widthDp >= minWidthDp
+    fun isWidthAtLeastBreakpoint(widthBreakpointDp: Int): Boolean {
+        return minWidthDp >= widthBreakpointDp
     }
 
     /**
-     * Returns `true` when [heightDp] is greater than or equal to [minHeightDp], `false` otherwise.
+     * Returns `true` when [minHeightDp] is greater than or equal to [heightBreakpointDp], `false`
+     * otherwise.
      */
-    fun isHeightAtLeast(heightDp: Int): Boolean {
-        return heightDp >= minHeightDp
+    fun isHeightAtLeastBreakpoint(heightBreakpointDp: Int): Boolean {
+        return minHeightDp >= heightBreakpointDp
     }
 
     /**
-     * Returns `true` when [widthDp] is greater than or equal to [minWidthDp] and [heightDp] is
-     * greater than or equal to [minHeightDp], `false` otherwise.
+     * Returns `true` when [widthBreakpointDp] is greater than or equal to [minWidthDp] and
+     * [heightBreakpointDp] is greater than or equal to [minHeightDp], `false` otherwise.
      */
-    fun isAtLeast(widthDp: Int, heightDp: Int): Boolean {
-        return isWidthAtLeast(widthDp) && isHeightAtLeast(heightDp)
+    fun isAtLeastBreakpoint(widthBreakpointDp: Int, heightBreakpointDp: Int): Boolean {
+        return isWidthAtLeastBreakpoint(widthBreakpointDp) &&
+            isHeightAtLeastBreakpoint(heightBreakpointDp)
     }
 
     override fun equals(other: Any?): Boolean {
diff --git a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
index 816e40c..e9a0aba 100644
--- a/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
+++ b/window/window-core/src/commonTest/kotlin/androidx/window/core/layout/WindowSizeClassTest.kt
@@ -16,6 +16,10 @@
 
 package androidx.window.core.layout
 
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.HEIGHT_DP_MEDIUM_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_EXPANDED_LOWER_BOUND
+import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND
 import kotlin.test.Test
 import kotlin.test.assertEquals
 import kotlin.test.assertFailsWith
@@ -115,85 +119,252 @@
     }
 
     @Test
-    fun is_width_at_least_returns_true_when_input_is_greater() {
+    fun is_width_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isWidthAtLeast(width + 1))
+        assertFalse(sizeClass.isWidthAtLeastBreakpoint(width + 1))
     }
 
     @Test
-    fun is_width_at_least_returns_true_when_input_is_equal() {
+    fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isWidthAtLeast(width))
+        assertTrue(sizeClass.isWidthAtLeastBreakpoint(width))
     }
 
     @Test
-    fun is_width_at_least_returns_false_when_input_is_smaller() {
+    fun is_width_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertFalse(sizeClass.isWidthAtLeast(width - 1))
+        assertTrue(sizeClass.isWidthAtLeastBreakpoint(width - 1))
+    }
+
+    /**
+     * Tests that the width breakpoint logic works as expected. The following sample shows what the
+     * dev use site should be
+     *
+     * WIDTH_DP_MEDIUM_LOWER_BOUND = 600 WIDTH_DP_EXPANDED_LOWER_BOUND = 840
+     *
+     * fun process(sizeClass: WindowSizeClass) { when {
+     * sizeClass.isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+     * sizeClass.isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+     *
+     * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 300, minHeightDp = 0) val
+     * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 0) val
+     * expandedBreakpoint = WindowSizeClass(minWidthDp = 840, minHeightDp = 0)
+     *
+     * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+     * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+     *
+     * So the following must be true
+     *
+     * expandedBreakpoint WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+     * true WindowSizeClass(840, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * equalMediumBreakpoint WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+     * == false WindowSizeClass(600, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+     * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+     */
+    @Test
+    fun is_width_at_least_bounds_checks() {
+        // expandedBreakpoint
+        assertTrue(
+            WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+                .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+        )
+        assertTrue(
+            WindowSizeClass(WIDTH_DP_EXPANDED_LOWER_BOUND, 0)
+                .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+        )
+
+        // equalMediumBreakpoint
+        assertFalse(
+            WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+                .isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND)
+        )
+        assertTrue(
+            WindowSizeClass(WIDTH_DP_MEDIUM_LOWER_BOUND, 0)
+                .isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND)
+        )
+
+        // belowBreakpoint
+        assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_EXPANDED_LOWER_BOUND))
+        assertFalse(WindowSizeClass(0, 0).isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND))
+    }
+
+    /**
+     * Tests that the width breakpoint logic works as expected. The following sample shows what the
+     * dev use site should be
+     *
+     * HEIGHT_DP_MEDIUM_LOWER_BOUND = 480 HEIGHT_DP_EXPANDED_LOWER_BOUND = 900
+     *
+     * fun process(sizeClass: WindowSizeClass) { when {
+     * sizeClass.isHeightAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) -> doExpanded()
+     * sizeClass.isHeightAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) -> doMedium() else -> doCompact() } }
+     *
+     * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+     * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 480) val
+     * expandedBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 900)
+     *
+     * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+     * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+     *
+     * So the following must be true
+     *
+     * expandedBreakpoint WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) ==
+     * true WindowSizeClass(0, 900).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * equalMediumBreakpoint WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+     * == false WindowSizeClass(0, 480).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_EXPANDED_LOWER_BOUND) == false
+     * WindowSizeClass(0, 0).isWidthAtLeast(HEIGHT_DP_MEDIUM_LOWER_BOUND) == false
+     */
+    @Test
+    fun is_height_at_least_bounds_checks() {
+        // expandedBreakpoint
+        assertTrue(
+            WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+                .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+        )
+        assertTrue(
+            WindowSizeClass(0, HEIGHT_DP_EXPANDED_LOWER_BOUND)
+                .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+        )
+
+        // equalMediumBreakpoint
+        assertFalse(
+            WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+                .isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND)
+        )
+        assertTrue(
+            WindowSizeClass(0, HEIGHT_DP_MEDIUM_LOWER_BOUND)
+                .isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND)
+        )
+
+        // belowBreakpoint
+        assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_EXPANDED_LOWER_BOUND))
+        assertFalse(WindowSizeClass(0, 0).isHeightAtLeastBreakpoint(HEIGHT_DP_MEDIUM_LOWER_BOUND))
+    }
+
+    /**
+     * Tests that the width breakpoint logic works as expected. The following sample shows what the
+     * dev use site should be
+     *
+     * DIAGONAL_BOUND_MEDIUM = 600, 600 DIAGONAL_BOUND_EXPANDED = 900, 900
+     *
+     * fun process(sizeClass: WindowSizeClass) { when { sizeClass.isAtLeast(DIAGONAL_BOUND_EXPANDED,
+     * DIAGONAL_BOUND_EXPANDED) -> doExpanded() sizeClass.isAtLeast(DIAGONAL_BOUND_MEDIUM,
+     * DIAGONAL_BOUND_MEDIUM) -> doMedium() else -> doCompact() } }
+     *
+     * val belowMediumBreakpoint = WindowSizeClass(minWidthDp = 0, minHeightDp = 0) val
+     * equalMediumBreakpoint = WindowSizeClass(minWidthDp = 600, minHeightDp = 600) val
+     * expandedBreakpoint = WindowSizeClass(minWidthDp = 900, minHeightDp = 900)
+     *
+     * process(belowBreakpoint) -> doSomethingCompact() process(equalMediumBreakpoint) ->
+     * doSomethingMedium() process(expandedBreakpoint) -> doSomethingExpanded()
+     *
+     * So the following must be true
+     *
+     * expandedBreakpoint WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) ==
+     * true WindowSizeClass(900, 900).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * equalMediumBreakpoint WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND)
+     * == false WindowSizeClass(600, 600).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == true
+     *
+     * belowBreakpoint WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_EXPANDED_LOWER_BOUND) == false
+     * WindowSizeClass(0, 0).isWidthAtLeast(WIDTH_DP_MEDIUM_LOWER_BOUND) == false
+     */
+    @Test
+    fun is_area_at_least_bounds_checks() {
+        val diagonalMedium = 600
+        val diagonalExpanded = 900
+        // expandedBreakpoint
+        assertTrue(
+            WindowSizeClass(diagonalExpanded, diagonalExpanded)
+                .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+        )
+        assertTrue(
+            WindowSizeClass(diagonalExpanded, diagonalExpanded)
+                .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+        )
+
+        // equalMediumBreakpoint
+        assertFalse(
+            WindowSizeClass(diagonalMedium, diagonalMedium)
+                .isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded)
+        )
+        assertTrue(
+            WindowSizeClass(diagonalMedium, diagonalMedium)
+                .isAtLeastBreakpoint(diagonalMedium, diagonalMedium)
+        )
+
+        // belowBreakpoint
+        assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalExpanded, diagonalExpanded))
+        assertFalse(WindowSizeClass(0, 0).isAtLeastBreakpoint(diagonalMedium, diagonalMedium))
     }
 
     @Test
-    fun is_height_at_least_returns_true_when_input_is_greater() {
+    fun is_height_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isHeightAtLeast(height + 1))
+        assertFalse(sizeClass.isHeightAtLeastBreakpoint(height + 1))
     }
 
     @Test
-    fun is_height_at_least_returns_true_when_input_is_equal() {
+    fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isHeightAtLeast(height))
+        assertTrue(sizeClass.isHeightAtLeastBreakpoint(height))
     }
 
     @Test
-    fun is_height_at_least_returns_false_when_input_is_smaller() {
+    fun is_height_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertFalse(sizeClass.isHeightAtLeast(height - 1))
+        assertTrue(sizeClass.isHeightAtLeastBreakpoint(height - 1))
     }
 
     @Test
-    fun is_at_least_returns_true_when_input_is_greater() {
+    fun is_at_least_breakpoint_returns_false_when_breakpoint_is_greater() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isAtLeast(width, height + 1))
-        assertTrue(sizeClass.isAtLeast(width + 1, height))
+        assertFalse(sizeClass.isAtLeastBreakpoint(width, height + 1))
+        assertFalse(sizeClass.isAtLeastBreakpoint(width + 1, height))
     }
 
     @Test
-    fun is_at_least_returns_true_when_input_is_equal() {
+    fun is_at_least_breakpoint_returns_true_when_breakpoint_is_equal() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertTrue(sizeClass.isAtLeast(width, height))
+        assertTrue(sizeClass.isAtLeastBreakpoint(width, height))
     }
 
     @Test
-    fun is_at_least_returns_false_when_input_is_smaller() {
+    fun is_at_least_breakpoint_returns_true_when_breakpoint_is_smaller() {
         val width = 200
         val height = 100
         val sizeClass = WindowSizeClass(width, height)
 
-        assertFalse(sizeClass.isAtLeast(width, height - 1))
-        assertFalse(sizeClass.isAtLeast(width - 1, height))
+        assertTrue(sizeClass.isAtLeastBreakpoint(width, height - 1))
+        assertTrue(sizeClass.isAtLeastBreakpoint(width - 1, height))
     }
 }