DO NOT MERGE: Merge remote-tracking branch 'aosp/androidx-platform-dev-temp' into merge_platform_dev

Fixes: 236262138
Change-Id: Ia5b278cc2d6989019e120e8ba5de10b35845c3ab
diff --git a/activity/activity-compose/build.gradle b/activity/activity-compose/build.gradle
index 43b6a5a..d51c200 100644
--- a/activity/activity-compose/build.gradle
+++ b/activity/activity-compose/build.gradle
@@ -36,7 +36,7 @@
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation("androidx.lifecycle:lifecycle-common-java8:2.5.0")
+    implementation("androidx.lifecycle:lifecycle-common-java8:2.5.1")
 
     androidTestImplementation projectOrArtifact(":compose:ui:ui-test-junit4")
     androidTestImplementation projectOrArtifact(":compose:material:material")
diff --git a/activity/activity-ktx/build.gradle b/activity/activity-ktx/build.gradle
index 71da6fa..b28eac7 100644
--- a/activity/activity-ktx/build.gradle
+++ b/activity/activity-ktx/build.gradle
@@ -33,10 +33,10 @@
     api("androidx.core:core-ktx:1.1.0") {
         because "Mirror activity dependency graph for -ktx artifacts"
     }
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0") {
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.savedstate:savedstate-ktx:1.2.0") {
         because 'Mirror activity dependency graph for -ktx artifacts'
     }
diff --git a/activity/activity/build.gradle b/activity/activity/build.gradle
index b2b7705..d086cea 100644
--- a/activity/activity/build.gradle
+++ b/activity/activity/build.gradle
@@ -23,10 +23,10 @@
     api("androidx.annotation:annotation:1.1.0")
     implementation("androidx.collection:collection:1.0.0")
     api("androidx.core:core:1.8.0")
-    api("androidx.lifecycle:lifecycle-runtime:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
+    api("androidx.lifecycle:lifecycle-runtime:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
     api("androidx.savedstate:savedstate:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     implementation("androidx.tracing:tracing:1.0.0")
     api(libs.kotlinStdlib)
 
diff --git a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
index 5719451..069c76c 100644
--- a/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
+++ b/appcompat/appcompat/src/androidTest/java/androidx/appcompat/app/LocalesActivityA.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,3 +20,4 @@
  * An activity for locales with a unique class name.
  */
 public class LocalesActivityA extends LocalesUpdateActivity {}
+
diff --git a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
index 9dc5995..c3e83b6 100644
--- a/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
+++ b/benchmark/benchmark-macro/src/androidTest/java/androidx/benchmark/macro/perfetto/PerfettoSdkHandshakeTest.kt
@@ -33,9 +33,12 @@
 import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED
 import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING
 import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_SUCCESS
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_CANCELLED
+import androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes.RESULT_CODE_ERROR_OTHER
 import com.google.common.truth.Truth.assertThat
 import java.io.File
 import java.io.StringReader
+import java.util.regex.Pattern
 import org.junit.After
 import org.junit.Assume.assumeTrue
 import org.junit.Before
@@ -251,6 +254,70 @@
         }
     }
 
+    @Test
+    fun test_handshake_package_does_not_exist() {
+        assumeTrue(isAbiSupported())
+        assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+        val response = perfettoCapture.enableAndroidxTracingPerfetto(
+            "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+            shouldProvideBinaries(testConfig.sdkDelivery)
+        )
+
+        assertThat(response).ignoringCase()
+            .contains("The broadcast to enable tracing was not received")
+    }
+
+    /**
+     * Unlike [test_handshake_package_does_not_exist], which uses [PerfettoCapture], this test
+     * uses a lower-level component [PerfettoHandshake].
+     */
+    @Test
+    fun test_handshake_framework_package_does_not_exist() {
+        assumeTrue(isAbiSupported())
+        assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+        val handshake = PerfettoHandshake(
+            "package.does.not.exist.89e51176_bc28_41f1_ac73_ca717454b517",
+            parseJsonMap = { emptyMap() },
+            Shell::executeCommand
+        )
+
+        // try
+        handshake.enableTracing(null).also { response ->
+            assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+            assertThat(response.requiredVersion).isNull()
+        }
+
+        // try again
+        handshake.enableTracing(null).also { response ->
+            assertThat(response.exitCode).isEqualTo(RESULT_CODE_CANCELLED)
+            assertThat(response.requiredVersion).isNull()
+        }
+    }
+
+    @Test
+    fun test_handshake_framework_parsing_error() {
+        assumeTrue(isAbiSupported())
+        assumeTrue(Build.VERSION.SDK_INT >= minSupportedSdk)
+
+        val parsingException = "I don't know how to JSON"
+        val handshake = PerfettoHandshake(
+            targetPackage,
+            parseJsonMap = { throw IllegalArgumentException(parsingException) },
+            Shell::executeCommand
+        )
+
+        handshake.enableTracing(null).also { response ->
+            assertThat(response.exitCode).isEqualTo(RESULT_CODE_ERROR_OTHER)
+            assertThat(response.requiredVersion).isNull()
+            assertThat(response.message).containsMatch(
+                "Exception occurred while trying to parse a response.*Error.*$parsingException"
+                    .toPattern(Pattern.CASE_INSENSITIVE)
+            )
+        }
+    }
+
     private fun enablePackage() {
         scope.pressHome()
         scope.startActivityAndWait()
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
index 8a43bf6..bfc7189 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ErrorProneConfiguration.kt
@@ -39,7 +39,7 @@
 const val ERROR_PRONE_TASK = "runErrorProne"
 
 private const val ERROR_PRONE_CONFIGURATION = "errorprone"
-private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.4.0"
+private const val ERROR_PRONE_VERSION = "com.google.errorprone:error_prone_core:2.14.0"
 private val log = Logging.getLogger("ErrorProneConfiguration")
 
 fun Project.configureErrorProneForJava() {
@@ -128,6 +128,32 @@
 
             "-XepExcludedPaths:.*/(build/generated|build/errorProne|external)/.*",
 
+            // Consider re-enabling the following checks. Disabled as part of
+            // error-prone upgrade
+            "-Xep:InlineMeSuggester:OFF",
+            "-Xep:UnusedVariable:OFF",
+            "-Xep:UnusedMethod:OFF",
+            "-Xep:NarrowCalculation:OFF",
+            "-Xep:LongDoubleConversion:OFF",
+            "-Xep:UnicodeEscape:OFF",
+            "-Xep:JavaUtilDate:OFF",
+            "-Xep:UnrecognisedJavadocTag:OFF",
+            "-Xep:ObjectEqualsForPrimitives:OFF",
+            "-Xep:UnnecessaryParentheses:OFF",
+            "-Xep:DoNotCallSuggester:OFF",
+            "-Xep:EqualsNull:OFF",
+            "-Xep:MalformedInlineTag:OFF",
+            "-Xep:MissingSuperCall:OFF",
+            "-Xep:ToStringReturnsNull:OFF",
+            "-Xep:ReturnValueIgnored:OFF",
+            "-Xep:MissingImplementsComparable:OFF",
+            "-Xep:EmptyTopLevelDeclaration:OFF",
+            "-Xep:InvalidThrowsLink:OFF",
+            "-Xep:StaticAssignmentOfThrowable:OFF",
+            "-Xep:DoNotClaimAnnotations:OFF",
+            "-Xep:AlreadyChecked:OFF",
+            "-Xep:StringSplitter:OFF",
+
             // We allow inter library RestrictTo usage.
             "-Xep:RestrictTo:OFF",
 
@@ -173,13 +199,13 @@
             "-Xep:MissingFail:ERROR",
             "-Xep:JavaLangClash:ERROR",
             "-Xep:TypeParameterUnusedInFormals:ERROR",
-            "-Xep:StringSplitter:ERROR",
+            // "-Xep:StringSplitter:ERROR", // disabled with upgrade to 2.14.0
             "-Xep:ReferenceEquality:ERROR",
             "-Xep:AssertionFailureIgnored:ERROR",
-            "-Xep:UnnecessaryParentheses:ERROR",
+            // "-Xep:UnnecessaryParentheses:ERROR", // disabled with upgrade to 2.14.0
             "-Xep:EqualsGetClass:ERROR",
-            "-Xep:UnusedVariable:ERROR",
-            "-Xep:UnusedMethod:ERROR",
+            // "-Xep:UnusedVariable:ERROR", // disabled with upgrade to 2.14.0
+            // "-Xep:UnusedMethod:ERROR", // disabled with upgrade to 2.14.0
             "-Xep:UndefinedEquals:ERROR",
             "-Xep:ThreadLocalUsage:ERROR",
             "-Xep:FutureReturnValueIgnored:ERROR",
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
index ae4ae00..44e2abd 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/impl/TagBundle.java
@@ -16,6 +16,7 @@
 
 package androidx.camera.core.impl;
 
+import android.hardware.camera2.CaptureRequest;
 import android.util.ArrayMap;
 import android.util.Pair;
 
@@ -40,6 +41,9 @@
 
     private static final TagBundle EMPTY_TAGBUNDLE = new TagBundle(new ArrayMap<>());
 
+    private static final String USER_TAG_PREFIX = "android.hardware.camera2.CaptureRequest.setTag.";
+
+    private static final String CAMERAX_USER_TAG_PREFIX = USER_TAG_PREFIX + "CX";
     /**
      * Creates an empty TagBundle.
      *
@@ -101,4 +105,24 @@
     public Set<String> listKeys() {
         return mTagMap.keySet();
     }
+
+    /**
+     * Produces a string that can be used to identify CameraX usage in a Camera2
+     * {@link CaptureRequest}.
+     *
+     * <p>In Android 13 or later, Camera2 will log the string representation of any
+     * tag set on {@link CaptureRequest.Builder#setTag(Object)}. Since
+     * tag bundles are always set internally by CameraX as the tag in a capture
+     * request, the constant string value returned here can be used to identify
+     * usage of CameraX versus application usage of Camera2.
+     *
+     * <p>Note: Doesn't return an actual string representation of the tag bundle.
+     *
+     * @return Returns a constant string value used to identify usage of CameraX.
+     */
+    @NonNull
+    @Override
+    public final String toString() {
+        return CAMERAX_USER_TAG_PREFIX;
+    }
 }
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
index e204317..3edf244 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/processing/ShaderProvider.java
@@ -18,9 +18,15 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.annotation.RestrictTo;
 
-/** A provider that supplies OpenGL shader code. */
-interface ShaderProvider {
+/**
+ * A provider that supplies OpenGL shader code.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public interface ShaderProvider {
 
     /**
      * Creates the fragment shader code with the given variable names.
@@ -34,14 +40,15 @@
      *         varying vec2 {$fragCoordsVarName};
      *         void main() {
      *           vec4 sampleColor = texture2D({$samplerVarName}, {$fragCoordsVarName});
-     *           gl_FragColor = vec4(sampleColor.r * 0.493 + sampleColor. g * 0.769 +
-     *              sampleColor.b * 0.289, sampleColor.r * 0.449 + sampleColor.g * 0.686 +
-     *              sampleColor.b * 0.268, sampleColor.r * 0.272 + sampleColor.g * 0.534 +
-     *              sampleColor.b * 0.131, 1.0);
+     *           gl_FragColor = vec4(
+     *               sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+     *               sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+     *               sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+     *               1.0);
      *         }
      * }</pre>
      *
-     * @param samplerVarName the variable name of the samplerExternalOES.
+     * @param samplerVarName    the variable name of the samplerExternalOES.
      * @param fragCoordsVarName the variable name of the fragment coordinates.
      * @return the shader code. Return null to use the default shader.
      */
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
index 616fb90..85572f1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/EffectBundleTest.kt
@@ -19,7 +19,6 @@
 import android.os.Build
 import androidx.camera.core.SurfaceEffect.PREVIEW
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
-import androidx.camera.testing.fakes.FakeSurfaceEffect
 import com.google.common.truth.Truth.assertThat
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -42,7 +41,11 @@
 
     @Test(expected = IllegalArgumentException::class)
     fun addMoreThanOnePreviewEffect_throwsException() {
-        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+        val surfaceEffect = object : SurfaceEffect {
+            override fun onInputSurface(request: SurfaceRequest) {}
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+        }
         EffectBundle.Builder(mainThreadExecutor())
             .addEffect(PREVIEW, surfaceEffect)
             .addEffect(PREVIEW, surfaceEffect)
@@ -51,7 +54,11 @@
     @Test
     fun addPreviewEffect_hasPreviewEffect() {
         // Arrange.
-        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+        val surfaceEffect = object : SurfaceEffect {
+            override fun onInputSurface(request: SurfaceRequest) {}
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+        }
         // Act.
         val effectBundle = EffectBundle.Builder(mainThreadExecutor())
             .addEffect(PREVIEW, surfaceEffect)
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
index 0cae6de..1be7cf1 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/PreviewTest.kt
@@ -42,7 +42,6 @@
 import androidx.camera.testing.fakes.FakeCamera
 import androidx.camera.testing.fakes.FakeCameraDeviceSurfaceManager
 import androidx.camera.testing.fakes.FakeCameraFactory
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import androidx.camera.testing.fakes.FakeUseCase
 import androidx.test.core.app.ApplicationProvider
 import com.google.common.truth.Truth.assertThat
@@ -72,6 +71,8 @@
 
     private lateinit var appSurface: Surface
     private lateinit var appSurfaceTexture: SurfaceTexture
+    private lateinit var effectSurface: Surface
+    private lateinit var effectSurfaceTexture: SurfaceTexture
     private lateinit var camera: FakeCamera
     private lateinit var cameraXConfig: CameraXConfig
     private lateinit var context: Context
@@ -81,6 +82,8 @@
     fun setUp() {
         appSurfaceTexture = SurfaceTexture(0)
         appSurface = Surface(appSurfaceTexture)
+        effectSurfaceTexture = SurfaceTexture(0)
+        effectSurface = Surface(effectSurfaceTexture)
         camera = FakeCamera()
 
         val cameraFactoryProvider =
@@ -103,6 +106,8 @@
     fun tearDown() {
         appSurfaceTexture.release()
         appSurface.release()
+        effectSurfaceTexture.release()
+        effectSurface.release()
         with(cameraUseCaseAdapter) {
             this?.removeUseCases(useCases)
         }
@@ -217,10 +222,27 @@
     @Test
     fun bindAndUnbindPreview_surfacesPropagated() {
         // Arrange.
-        val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
+        var surfaceOutputReceived: SurfaceOutput? = null
+        var effectSurfaceReadyToRelease = false
+        var isEffectReleased = false
+        val surfaceEffect = object : SurfaceEffectInternal {
+            override fun onInputSurface(request: SurfaceRequest) {
+                request.provideSurface(effectSurface, mainThreadExecutor()) {
+                    effectSurfaceReadyToRelease = true
+                }
+            }
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+                surfaceOutputReceived = surfaceOutput
+            }
+
+            override fun release() {
+                isEffectReleased = true
+            }
+        }
 
         // Act: create pipeline in Preview and provide Surface.
-        val preview = createPreviewPipelineAndAttachEffect(effect)
+        val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
         val surfaceRequest = preview.mCurrentSurfaceRequest!!
         var appSurfaceReadyToRelease = false
         surfaceRequest.provideSurface(appSurface, mainThreadExecutor()) {
@@ -229,26 +251,30 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: surfaceOutput received.
-        assertThat(effect.surfaceOutput).isNotNull()
-        assertThat(effect.isReleased).isFalse()
-        assertThat(effect.isOutputSurfaceRequestedToClose).isFalse()
-        assertThat(effect.isInputSurfaceReleased).isFalse()
+        assertThat(surfaceOutputReceived).isNotNull()
+        var requestedToReleaseOutputSurface = false
+        surfaceOutputReceived!!.getSurface(mainThreadExecutor()) {
+            requestedToReleaseOutputSurface = true
+        }
+        assertThat(isEffectReleased).isFalse()
+        assertThat(requestedToReleaseOutputSurface).isFalse()
+        assertThat(effectSurfaceReadyToRelease).isFalse()
         assertThat(appSurfaceReadyToRelease).isFalse()
         // effect surface is provided to camera.
-        assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effect.inputSurface)
+        assertThat(preview.sessionConfig.surfaces[0].surface.get()).isEqualTo(effectSurface)
 
         // Act: unbind Preview.
         preview.onDetached()
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect and effect surface is released.
-        assertThat(effect.isReleased).isTrue()
-        assertThat(effect.isOutputSurfaceRequestedToClose).isTrue()
-        assertThat(effect.isInputSurfaceReleased).isTrue()
+        assertThat(isEffectReleased).isTrue()
+        assertThat(requestedToReleaseOutputSurface).isTrue()
+        assertThat(effectSurfaceReadyToRelease).isTrue()
         assertThat(appSurfaceReadyToRelease).isFalse()
 
         // Act: close SurfaceOutput
-        effect.surfaceOutput!!.close()
+        surfaceOutputReceived!!.close()
         shadowOf(getMainLooper()).idle()
         assertThat(appSurfaceReadyToRelease).isTrue()
     }
@@ -256,8 +282,18 @@
     @Test
     fun invokedErrorListener_recreatePipeline() {
         // Arrange: create pipeline and get a reference of the SessionConfig.
-        val effect = FakeSurfaceEffectInternal(mainThreadExecutor())
-        val preview = createPreviewPipelineAndAttachEffect(effect)
+        val surfaceEffect = object : SurfaceEffectInternal {
+            override fun onInputSurface(request: SurfaceRequest) {}
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+                surfaceOutput.getSurface(mainThreadExecutor()) {
+                    surfaceOutput.close()
+                }
+            }
+
+            override fun release() {}
+        }
+        val preview = createPreviewPipelineAndAttachEffect(surfaceEffect)
         val originalSessionConfig = preview.sessionConfig
 
         // Act: invoke the error listener.
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
index 37ea1bf..1896e6c 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
+++ b/camera/camera-core/src/test/java/androidx/camera/core/impl/TagBundleTest.java
@@ -26,8 +26,6 @@
 import org.robolectric.RobolectricTestRunner;
 import org.robolectric.annotation.internal.DoNotInstrument;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Set;
 
 @RunWith(RobolectricTestRunner.class)
@@ -44,13 +42,6 @@
     private static final Integer TAG_VALUE_2 = 2;
 
     TagBundle mTagBundle;
-    private static final List<String> KEY_LIST = new ArrayList<>();
-
-    static {
-        KEY_LIST.add(TAG_0);
-        KEY_LIST.add(TAG_1);
-        KEY_LIST.add(TAG_2);
-    }
 
     @Before
     public void setUp() {
@@ -84,4 +75,10 @@
 
         assertThat(keyList).containsExactly(TAG_0, TAG_1, TAG_2);
     }
+
+    @Test
+    public void verifyTagBundleToString() {
+        assertThat(mTagBundle.toString()).startsWith("android.hardware.camera2.CaptureRequest"
+                + ".setTag.CX");
+    }
 }
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
index 951f021..42f71acb 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/internal/CameraUseCaseAdapterTest.kt
@@ -19,10 +19,11 @@
 import android.os.Build
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
-import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.processing.SurfaceEffectWithExecutor
-import androidx.camera.testing.fakes.FakeSurfaceEffect
 import com.google.common.truth.Truth.assertThat
 import java.util.concurrent.ExecutorService
 import java.util.concurrent.Executors
@@ -42,20 +43,22 @@
 @Config(minSdk = Build.VERSION_CODES.LOLLIPOP)
 class CameraUseCaseAdapterTest {
 
-    private lateinit var surfaceEffect: FakeSurfaceEffect
+    private lateinit var surfaceEffect: SurfaceEffect
     private lateinit var mEffectBundle: EffectBundle
     private lateinit var executor: ExecutorService
 
     @Before
     fun setUp() {
-        surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+        surfaceEffect = object : SurfaceEffect {
+            override fun onInputSurface(request: SurfaceRequest) {}
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+        }
         executor = Executors.newSingleThreadExecutor()
         mEffectBundle = EffectBundle.Builder(executor).addEffect(PREVIEW, surfaceEffect).build()
     }
 
     @After
     fun tearDown() {
-        surfaceEffect.cleanUp()
         executor.shutdown()
     }
 
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
index 7df85dd..ca79bdea 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectNodeTest.kt
@@ -24,10 +24,11 @@
 import android.util.Size
 import android.view.Surface
 import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.core.impl.utils.futures.Futures
 import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import com.google.common.truth.Truth.assertThat
 import org.junit.After
 import org.junit.Before
@@ -54,9 +55,15 @@
         private val CROP_RECT = Rect(0, 0, 600, 400)
     }
 
-    private lateinit var surfaceEffectInternal: FakeSurfaceEffectInternal
+    private lateinit var surfaceEffect: SurfaceEffectInternal
+    private var isReleased = false
+    private var surfaceOutputCloseRequested = false
+    private var surfaceOutputReceived: SurfaceOutput? = null
+    private var surfaceReceivedByEffect: Surface? = null
     private lateinit var appSurface: Surface
     private lateinit var appSurfaceTexture: SurfaceTexture
+    private lateinit var effectSurface: Surface
+    private lateinit var effectSurfaceTexture: SurfaceTexture
     private lateinit var node: SurfaceEffectNode
     private lateinit var inputEdge: SurfaceEdge
 
@@ -64,8 +71,30 @@
     fun setup() {
         appSurfaceTexture = SurfaceTexture(0)
         appSurface = Surface(appSurfaceTexture)
-        surfaceEffectInternal = FakeSurfaceEffectInternal(mainThreadExecutor())
-        node = SurfaceEffectNode(FakeCamera(), surfaceEffectInternal)
+        effectSurfaceTexture = SurfaceTexture(0)
+        effectSurface = Surface(effectSurfaceTexture)
+
+        surfaceEffect = object : SurfaceEffectInternal {
+            override fun onInputSurface(request: SurfaceRequest) {
+                request.provideSurface(effectSurface, mainThreadExecutor()) {
+                    effectSurfaceTexture.release()
+                    effectSurface.release()
+                }
+            }
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+                surfaceOutputReceived = surfaceOutput
+                surfaceReceivedByEffect = surfaceOutput.getSurface(mainThreadExecutor()) {
+                    surfaceOutput.close()
+                    surfaceOutputCloseRequested = true
+                }
+            }
+
+            override fun release() {
+                isReleased = true
+            }
+        }
+        node = SurfaceEffectNode(FakeCamera(), surfaceEffect)
         inputEdge = createInputEdge()
     }
 
@@ -73,7 +102,8 @@
     fun tearDown() {
         appSurfaceTexture.release()
         appSurface.release()
-        surfaceEffectInternal.release()
+        effectSurfaceTexture.release()
+        effectSurface.release()
         node.release()
         inputEdge.surfaces[0].close()
         shadowOf(getMainLooper()).idle()
@@ -110,8 +140,8 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect receives app Surface. CameraX receives effect Surface.
-        assertThat(surfaceEffectInternal.outputSurface).isEqualTo(appSurface)
-        assertThat(inputSurface.surface.get()).isEqualTo(surfaceEffectInternal.inputSurface)
+        assertThat(surfaceReceivedByEffect).isEqualTo(appSurface)
+        assertThat(inputSurface.surface.get()).isEqualTo(effectSurface)
     }
 
     @Test
@@ -126,8 +156,8 @@
         shadowOf(getMainLooper()).idle()
 
         // Assert: effect is released and has requested effect to close the SurfaceOutput
-        assertThat(surfaceEffectInternal.isReleased).isTrue()
-        assertThat(surfaceEffectInternal.isOutputSurfaceRequestedToClose).isTrue()
+        assertThat(isReleased).isTrue()
+        assertThat(surfaceOutputCloseRequested).isTrue()
     }
 
     private fun createInputEdge(): SurfaceEdge {
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
index 6a33afe..86b492e 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/processing/SurfaceEffectWithExecutorTest.kt
@@ -27,7 +27,6 @@
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.testing.fakes.FakeCamera
-import androidx.camera.testing.fakes.FakeSurfaceEffectInternal
 import com.google.common.truth.Truth.assertThat
 import java.lang.Thread.currentThread
 import java.util.concurrent.Executor
@@ -70,10 +69,13 @@
 
     @Test(expected = IllegalStateException::class)
     fun initWithSurfaceEffectInternal_throwsException() {
-        SurfaceEffectWithExecutor(
-            FakeSurfaceEffectInternal(mainThreadExecutor()),
-            mainThreadExecutor()
-        )
+        SurfaceEffectWithExecutor(object : SurfaceEffectInternal {
+            override fun onInputSurface(request: SurfaceRequest) {}
+
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {}
+
+            override fun release() {}
+        }, mainThreadExecutor())
     }
 
     @Test
diff --git a/camera/camera-extensions/lint-baseline.xml b/camera/camera-extensions/lint-baseline.xml
index 382b9d2..eb45276 100644
--- a/camera/camera-extensions/lint-baseline.xml
+++ b/camera/camera-extensions/lint-baseline.xml
@@ -4,51 +4,6 @@
     <issue
         id="UnsafeOptInUsageError"
         message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="        Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="            camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
-        errorLine2="                                        ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="            camera2ConfigurationBuilder.setCaptureRequestOption(captureParameter.first,"
-        errorLine2="                                                                ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="                    captureParameter.second);"
-        errorLine2="                    ~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="        captureConfigBuilder.addImplementationOptions(camera2ConfigurationBuilder.build());"
-        errorLine2="                                                                                  ~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
         errorLine1="                CaptureRequestOptions.Builder.from(parameters).build();"
         errorLine2="                                              ~~~~">
         <location
@@ -124,42 +79,6 @@
         errorLine1="                new Camera2ImplConfig.Extender&lt;>(builder).setCameraEventCallback("
         errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
         <location
-            file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="                new Camera2ImplConfig.Extender&lt;>(builder).setCameraEventCallback("
-        errorLine2="                                                 ~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="                new Camera2ImplConfig.Extender&lt;>(builder).setCameraEventCallback("
-        errorLine2="                                                          ~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="                        new CameraEventCallbacks(imageCaptureEventAdapter));"
-        errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
-            file="src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java"/>
-    </issue>
-
-    <issue
-        id="UnsafeOptInUsageError"
-        message="This declaration is opt-in and its usage should be marked with&#xA;&apos;@androidx.camera.camera2.interop.ExperimentalCamera2Interop&apos; or &apos;@OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)&apos;"
-        errorLine1="                new Camera2ImplConfig.Extender&lt;>(builder).setCameraEventCallback("
-        errorLine2="                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
-        <location
             file="src/main/java/androidx/camera/extensions/internal/PreviewConfigProvider.java"/>
     </issue>
 
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
index 225da9a..f9bc984 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/AdaptingCaptureStage.java
@@ -20,8 +20,10 @@
 import android.util.Pair;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.OptIn;
 import androidx.annotation.RequiresApi;
 import androidx.camera.camera2.impl.Camera2ImplConfig;
+import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
 import androidx.camera.core.impl.CaptureConfig;
 import androidx.camera.core.impl.CaptureStage;
 import androidx.camera.extensions.impl.CaptureStageImpl;
@@ -34,9 +36,9 @@
     private final int mId;
 
     @SuppressWarnings("unchecked")
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     public AdaptingCaptureStage(@NonNull CaptureStageImpl impl) {
         mId = impl.getId();
-
         Camera2ImplConfig.Builder camera2ConfigurationBuilder = new Camera2ImplConfig.Builder();
 
         for (Pair<CaptureRequest.Key, Object> captureParameter : impl.getParameters()) {
diff --git a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
index c280363..c752065 100644
--- a/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
+++ b/camera/camera-extensions/src/main/java/androidx/camera/extensions/internal/ImageCaptureConfigProvider.java
@@ -88,6 +88,7 @@
     /**
      * Update extension related configs to the builder.
      */
+    @OptIn(markerClass = ExperimentalCamera2Interop.class)
     void updateBuilderConfig(@NonNull ImageCapture.Builder builder,
             @ExtensionMode.Mode int effectMode, @NonNull VendorExtender vendorExtender,
             @NonNull Context context) {
diff --git a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
index 35e8e77..bf87a7f 100644
--- a/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
+++ b/camera/camera-lifecycle/src/androidTest/java/androidx/camera/lifecycle/ProcessCameraProviderTest.kt
@@ -26,7 +26,10 @@
 import androidx.camera.core.CameraXConfig
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.Preview
+import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCaseGroup
 import androidx.camera.core.impl.CameraFactory
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
@@ -37,7 +40,6 @@
 import androidx.camera.testing.fakes.FakeCameraFactory
 import androidx.camera.testing.fakes.FakeCameraInfoInternal
 import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSurfaceEffect
 import androidx.camera.testing.fakes.FakeUseCaseConfigFactory
 import androidx.concurrent.futures.await
 import androidx.test.core.app.ApplicationProvider
@@ -79,7 +81,12 @@
     fun bindUseCaseGroupWithEffect_effectIsSetOnUseCase() {
         // Arrange.
         ProcessCameraProvider.configureInstance(FakeAppConfig.create())
-        val surfaceEffect = FakeSurfaceEffect(mainThreadExecutor())
+        val surfaceEffect = object : SurfaceEffect {
+            override fun onInputSurface(request: SurfaceRequest) {}
+            override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+                surfaceOutput.close()
+            }
+        }
         val effectBundle =
             EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
         val preview = Preview.Builder().setSessionOptionUnpacker { _, _ -> }.build()
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
deleted file mode 100644
index 1287dee..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffect.java
+++ /dev/null
@@ -1,124 +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.camera.testing.fakes;
-
-import android.graphics.SurfaceTexture;
-import android.os.Build;
-import android.view.Surface;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.SurfaceEffect;
-import androidx.camera.core.SurfaceOutput;
-import androidx.camera.core.SurfaceRequest;
-import androidx.camera.core.impl.DeferrableSurface;
-
-import java.util.concurrent.Executor;
-
-/**
- * Fake {@link SurfaceEffect} used in tests.
- */
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class FakeSurfaceEffect implements SurfaceEffect {
-
-    final SurfaceTexture mSurfaceTexture;
-    final Surface mInputSurface;
-    private final Executor mExecutor;
-
-
-    @Nullable
-    private SurfaceRequest mSurfaceRequest;
-    @Nullable
-    private SurfaceOutput mSurfaceOutput;
-    boolean mIsInputSurfaceReleased;
-    boolean mIsOutputSurfaceRequestedToClose;
-
-    Surface mOutputSurface;
-
-    public FakeSurfaceEffect(@NonNull Executor executor) {
-        mSurfaceTexture = new SurfaceTexture(0);
-        mInputSurface = new Surface(mSurfaceTexture);
-        mExecutor = executor;
-        mIsInputSurfaceReleased = false;
-        mIsOutputSurfaceRequestedToClose = false;
-    }
-
-    @Override
-    public void onInputSurface(@NonNull SurfaceRequest request) {
-        mSurfaceRequest = request;
-        request.provideSurface(mInputSurface, mExecutor, result -> {
-            mSurfaceTexture.release();
-            mInputSurface.release();
-            mIsInputSurfaceReleased = true;
-        });
-    }
-
-    @Override
-    public void onOutputSurface(@NonNull SurfaceOutput surfaceOutput) {
-        mSurfaceOutput = surfaceOutput;
-        mOutputSurface = surfaceOutput.getSurface(mExecutor,
-                () -> mIsOutputSurfaceRequestedToClose = true);
-    }
-
-    @Nullable
-    public SurfaceRequest getSurfaceRequest() {
-        return mSurfaceRequest;
-    }
-
-    @Nullable
-    public SurfaceOutput getSurfaceOutput() {
-        return mSurfaceOutput;
-    }
-
-    @NonNull
-    public Surface getInputSurface() {
-        return mInputSurface;
-    }
-
-    @NonNull
-    public Surface getOutputSurface() {
-        return mOutputSurface;
-    }
-
-    public boolean isInputSurfaceReleased() {
-        return mIsInputSurfaceReleased;
-    }
-
-    public boolean isOutputSurfaceRequestedToClose() {
-        return mIsOutputSurfaceRequestedToClose;
-    }
-
-    /**
-     * Clear up the instance to avoid the "{@link DeferrableSurface} garbage collected" error.
-     */
-    public void cleanUp() {
-        if (mSurfaceRequest != null) {
-            mSurfaceRequest.willNotProvideSurface();
-        }
-        if (mSurfaceOutput != null) {
-            mSurfaceOutput.close();
-        }
-        mSurfaceTexture.release();
-        mInputSurface.release();
-    }
-
-    @Override
-    protected void finalize() {
-        cleanUp();
-    }
-}
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java b/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
deleted file mode 100644
index 47bd6f7..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/fakes/FakeSurfaceEffectInternal.java
+++ /dev/null
@@ -1,48 +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.camera.testing.fakes;
-
-import android.os.Build;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import androidx.camera.core.processing.SurfaceEffectInternal;
-
-import java.util.concurrent.Executor;
-
-/**
- * Fake {@link SurfaceEffectInternal} used in tests.
- */
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class FakeSurfaceEffectInternal extends FakeSurfaceEffect implements SurfaceEffectInternal {
-
-    private boolean mIsReleased;
-
-    public FakeSurfaceEffectInternal(@NonNull Executor executor) {
-        super(executor);
-        mIsReleased = false;
-    }
-
-    public boolean isReleased() {
-        return mIsReleased;
-    }
-
-    @Override
-    public void release() {
-        mIsReleased = true;
-    }
-}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
index 2e5c7b8..c478811 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/CameraControllerDeviceTest.kt
@@ -25,7 +25,10 @@
 import androidx.camera.core.CameraSelector.LENS_FACING_FRONT
 import androidx.camera.core.EffectBundle
 import androidx.camera.core.ImageCapture
+import androidx.camera.core.SurfaceEffect
 import androidx.camera.core.SurfaceEffect.PREVIEW
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.camera.testing.CameraUtil
@@ -33,7 +36,6 @@
 import androidx.camera.testing.CoreAppTestUtil
 import androidx.camera.testing.fakes.FakeActivity
 import androidx.camera.testing.fakes.FakeLifecycleOwner
-import androidx.camera.testing.fakes.FakeSurfaceEffect
 import androidx.test.annotation.UiThreadTest
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
@@ -103,10 +105,15 @@
 
         // Act: set an EffectBundle
         instrumentation.runOnMainSync {
+            val surfaceEffect = object : SurfaceEffect {
+                override fun onInputSurface(request: SurfaceRequest) {}
+
+                override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+                    surfaceOutput.close()
+                }
+            }
             controller.setEffectBundle(
-                EffectBundle.Builder(mainThreadExecutor())
-                    .addEffect(PREVIEW, FakeSurfaceEffect(mainThreadExecutor()))
-                    .build()
+                EffectBundle.Builder(mainThreadExecutor()).addEffect(PREVIEW, surfaceEffect).build()
             )
         }
 
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
index 74a5c35..6f91eca 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
@@ -16,7 +16,9 @@
 
 package androidx.camera.integration.avsync.model
 
+import android.content.Context
 import android.media.AudioTrack
+import androidx.test.core.app.ApplicationProvider
 import androidx.test.filters.LargeTest
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.google.common.truth.Truth.assertThat
@@ -33,6 +35,7 @@
 @RunWith(AndroidJUnit4::class)
 class AudioGeneratorDeviceTest {
 
+    private val context: Context = ApplicationProvider.getApplicationContext()
     private lateinit var audioGenerator: AudioGenerator
 
     @Before
@@ -42,12 +45,12 @@
 
     @Test(expected = IllegalArgumentException::class)
     fun initAudioTrack_throwExceptionWhenFrequencyNegative() = runTest {
-        audioGenerator.initAudioTrack(-5300, 11.0)
+        audioGenerator.initAudioTrack(context, -5300, 11.0)
     }
 
     @Test(expected = IllegalArgumentException::class)
     fun initAudioTrack_throwExceptionWhenLengthNegative() = runTest {
-        audioGenerator.initAudioTrack(5300, -11.0)
+        audioGenerator.initAudioTrack(context, 5300, -11.0)
     }
 
     @Test
@@ -71,7 +74,7 @@
     }
 
     private suspend fun initialAudioTrack(frequency: Int, beepLengthInSec: Double) {
-        val isInitialized = audioGenerator.initAudioTrack(frequency, beepLengthInSec)
+        val isInitialized = audioGenerator.initAudioTrack(context, frequency, beepLengthInSec)
         assertThat(isInitialized).isTrue()
         assertThat(audioGenerator.audioTrack!!.state).isEqualTo(AudioTrack.STATE_INITIALIZED)
         assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isEqualTo(0)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
index 2997a17..2762f2d 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -79,6 +79,7 @@
 
         withContext(Dispatchers.Default) {
             audioGenerator.initAudioTrack(
+                context = context,
                 frequency = beepFrequency,
                 beepLengthInSec = ACTIVE_LENGTH_SEC,
             )
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
index 80c3936..8e12eb2 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -16,6 +16,7 @@
 
 package androidx.camera.integration.avsync.model
 
+import android.content.Context
 import android.media.AudioAttributes
 import android.media.AudioFormat
 import android.media.AudioManager
@@ -30,8 +31,8 @@
 import kotlin.math.sin
 
 private const val TAG = "AudioGenerator"
+private const val DEFAULT_SAMPLE_RATE: Int = 44100
 private const val SAMPLE_WIDTH: Int = 2
-private const val SAMPLE_RATE: Int = 44100
 private const val MAGNITUDE = 0.5
 private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT
 private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO
@@ -46,26 +47,33 @@
     }
 
     fun stop() {
+        Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}")
+        Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}")
         audioTrack!!.stop()
     }
 
     suspend fun initAudioTrack(
+        context: Context,
         frequency: Int,
         beepLengthInSec: Double,
     ): Boolean {
         checkArgumentNonnegative(frequency, "The input frequency should not be negative.")
         checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.")
 
-        Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
-
-        val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, SAMPLE_RATE)
+        val sampleRate = getOutputSampleRate(context)
+        val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, sampleRate)
         val bufferSize = samples.size
+
+        Logger.i(TAG, "initAudioTrack with sample rate: $sampleRate")
+        Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
+        Logger.i(TAG, "initAudioTrack with buffer size: $bufferSize")
+
         val audioAttributes = AudioAttributes.Builder()
             .setUsage(AudioAttributes.USAGE_MEDIA)
             .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
             .build()
         val audioFormat = AudioFormat.Builder()
-            .setSampleRate(SAMPLE_RATE)
+            .setSampleRate(sampleRate)
             .setEncoding(ENCODING)
             .setChannelMask(CHANNEL)
             .build()
@@ -83,6 +91,13 @@
         return true
     }
 
+    private fun getOutputSampleRate(context: Context): Int {
+        val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+        val sampleRate: String? = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
+
+        return sampleRate?.toInt() ?: DEFAULT_SAMPLE_RATE
+    }
+
     @VisibleForTesting
     suspend fun generateSineSamples(
         frequency: Int,
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
index 4bab0a9..cb47a1d 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/CameraXActivityTestExtensions.kt
@@ -31,8 +31,6 @@
  */
 internal fun ActivityScenario<CameraXActivity>.waitForViewfinderIdle() {
     val idlingResource = withActivity {
-        // Make sure that the test target use case is not null
-        assertThat(preview).isNotNull()
         resetViewIdlingResource()
         viewIdlingResource
     }
@@ -50,8 +48,6 @@
  */
 internal fun ActivityScenario<CameraXActivity>.switchCameraAndWaitForViewfinderIdle() {
     val idlingResource = withActivity {
-        // Make sure that the test target use case is not null
-        assertThat(preview).isNotNull()
         resetViewIdlingResource()
         viewIdlingResource
     }
@@ -68,8 +64,6 @@
  */
 internal fun ActivityScenario<CameraXActivity>.takePictureAndWaitForImageSavedIdle() {
     val idlingResource = withActivity {
-        // Make sure that the test target use case is not null
-        assertThat(imageCapture).isNotNull()
         imageSavedIdlingResource
     }
     try {
@@ -88,8 +82,6 @@
  */
 internal fun ActivityScenario<CameraXActivity>.waitForImageAnalysisIdle() {
     val idlingResource = withActivity {
-        // Make sure that the test target use case is not null
-        assertThat(imageAnalysis).isNotNull()
         resetAnalysisIdlingResource()
         analysisIdlingResource
     }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 0636c9b..981b5d2 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,10 +18,13 @@
 import android.Manifest
 import android.app.Instrumentation
 import android.content.Context
+import android.content.Intent
 import android.os.Build
 import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.CameraSelector
 import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
 import androidx.camera.testing.CameraUtil
 import androidx.camera.testing.CameraUtil.PreTestCameraIdList
 import androidx.camera.testing.CoreAppTestUtil
@@ -29,10 +32,9 @@
 import androidx.lifecycle.Lifecycle.State.RESUMED
 import androidx.test.core.app.ActivityScenario
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
 import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.espresso.matcher.ViewMatchers.withId
 import androidx.test.filters.LargeTest
 import androidx.test.platform.app.InstrumentationRegistry
 import androidx.test.rule.GrantPermissionRule
@@ -42,20 +44,23 @@
 import java.util.concurrent.TimeUnit
 import kotlinx.coroutines.runBlocking
 import org.junit.After
-import org.junit.AfterClass
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
 
 private const val HOME_TIMEOUT_MS = 3000L
 private const val ROTATE_TIMEOUT_MS = 2000L
 
 // Test application lifecycle when using CameraX.
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
 @LargeTest
-class ExistingActivityLifecycleTest {
+class ExistingActivityLifecycleTest(
+    private val implName: String,
+    private val cameraConfig: String
+) {
     private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
 
     @get:Rule
@@ -73,14 +78,17 @@
     @get:Rule
     val repeatRule = RepeatRule()
 
-    companion object {
-        @AfterClass
-        @JvmStatic
-        fun shutdownCameraX() {
-            val context = ApplicationProvider.getApplicationContext<Context>()
-            val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
-            cameraProvider.shutdown()[10, TimeUnit.SECONDS]
-        }
+    @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+        forAllTests = true,
+    )
+
+    private val launchIntent = Intent(
+        ApplicationProvider.getApplicationContext(),
+        CameraXActivity::class.java
+    ).apply {
+        putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
     }
 
     @Before
@@ -108,13 +116,17 @@
         device.unfreezeRotation()
         device.pressHome()
         device.waitForIdle(HOME_TIMEOUT_MS)
+
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        cameraProvider.shutdown()[10, TimeUnit.SECONDS]
     }
 
     // Check if Preview screen is updated or not, after Destroy-Create lifecycle.
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkPreviewUpdatedAfterDestroyRecreate() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use { // Ensure ActivityScenario is cleaned up properly
                 // Wait for viewfinder to receive enough frames for its IdlingResource to idle.
                 waitForViewfinderIdle()
@@ -129,7 +141,7 @@
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkImageCaptureAfterDestroyRecreate() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use {
                 // Arrange.
                 // Ensure ActivityScenario is cleaned up properly
@@ -150,7 +162,7 @@
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkPreviewUpdatedAfterStopResume() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use { // Ensure ActivityScenario is cleaned up properly
                 // Wait for viewfinder to receive enough frames for its IdlingResource to idle.
                 waitForViewfinderIdle()
@@ -179,7 +191,7 @@
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkImageCaptureAfterStopResume() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use {
                 // Arrange.
                 // Ensure ActivityScenario is cleaned up properly
@@ -210,13 +222,13 @@
             )
         )
 
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use { // Ensure ActivityScenario is cleaned up properly
                 // Wait for viewfinder to receive enough frames for its IdlingResource to idle.
                 waitForViewfinderIdle()
 
                 // Switch camera.
-                Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+                onView(withId(R.id.direction_toggle))
                     .perform(ViewActions.click())
 
                 // Check front camera is now idle
@@ -244,7 +256,7 @@
             )
         )
 
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use {
                 // Arrange.
                 // Ensure ActivityScenario is cleaned up properly
@@ -252,7 +264,7 @@
                 waitForViewfinderIdle()
 
                 // Act. Switch camera.
-                Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+                onView(withId(R.id.direction_toggle))
                     .perform(ViewActions.click())
 
                 // Assert.
@@ -272,7 +284,7 @@
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkPreviewUpdatedAfterRotateDeviceAndStopResume() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use { // Ensure ActivityScenario is cleaned up properly
                 // Wait for viewfinder to receive enough frames for its IdlingResource to idle.
                 waitForViewfinderIdle()
@@ -298,7 +310,7 @@
     @Test
     @RepeatRule.Repeat(times = 5)
     fun checkImageCaptureAfterRotateDeviceAndStopResume() {
-        with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+        with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
             use {
                 // Arrange.
                 // Ensure ActivityScenario is cleaned up properly
@@ -337,4 +349,20 @@
         )
         InstrumentationRegistry.getInstrumentation().waitForIdleSync()
     }
+
+    companion object {
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(
+                Camera2Config::class.simpleName,
+                CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+            ),
+            arrayOf(
+                CameraPipeConfig::class.simpleName,
+                CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+            )
+        )
+    }
 }
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
deleted file mode 100644
index 7951333..0000000
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.java
+++ /dev/null
@@ -1,219 +0,0 @@
-/*
- * Copyright 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.core;
-
-import static androidx.test.espresso.Espresso.onView;
-import static androidx.test.espresso.action.ViewActions.click;
-import static androidx.test.espresso.assertion.ViewAssertions.matches;
-import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
-import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
-import static androidx.test.espresso.matcher.ViewMatchers.withId;
-
-import static junit.framework.TestCase.assertNotNull;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assume.assumeNotNull;
-import static org.junit.Assume.assumeTrue;
-
-import android.content.Intent;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraInfo;
-import androidx.camera.core.CameraSelector;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.TorchState;
-import androidx.camera.integration.core.idlingresource.ElapsedTimeIdlingResource;
-import androidx.camera.integration.core.idlingresource.WaitForViewToShow;
-import androidx.camera.lifecycle.ProcessCameraProvider;
-import androidx.camera.testing.CameraUtil;
-import androidx.camera.testing.CoreAppTestUtil;
-import androidx.test.core.app.ApplicationProvider;
-import androidx.test.espresso.Espresso;
-import androidx.test.espresso.IdlingRegistry;
-import androidx.test.espresso.IdlingResource;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.LargeTest;
-import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
-import androidx.test.rule.GrantPermissionRule;
-import androidx.test.uiautomator.UiDevice;
-
-import junit.framework.AssertionFailedError;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.TestRule;
-import org.junit.runner.RunWith;
-
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-/** Test toggle buttons in CoreTestApp. */
-@RunWith(AndroidJUnit4.class)
-@LargeTest
-public final class ToggleButtonUITest {
-
-    private static final int IDLE_TIMEOUT_MS = 1000;
-    private static final String BASIC_SAMPLE_PACKAGE = "androidx.camera.integration.core";
-
-    private final UiDevice mDevice =
-            UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
-    private final Intent mIntent = ApplicationProvider.getApplicationContext().getPackageManager()
-            .getLaunchIntentForPackage(BASIC_SAMPLE_PACKAGE);
-
-    @Rule
-    public ActivityTestRule<CameraXActivity> mActivityRule =
-            new ActivityTestRule<>(CameraXActivity.class, true,
-                    false);
-
-    @Rule
-    public TestRule mUseCamera = CameraUtil.grantCameraPermissionAndPreTest(
-            new CameraUtil.PreTestCameraIdList(Camera2Config.defaultConfig())
-    );
-    @Rule
-    public GrantPermissionRule mStoragePermissionRule =
-            GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
-    @Rule
-    public GrantPermissionRule mAudioPermissionRule =
-            GrantPermissionRule.grant(android.Manifest.permission.RECORD_AUDIO);
-
-    public static void waitFor(IdlingResource idlingResource) {
-        IdlingRegistry.getInstance().register(idlingResource);
-        Espresso.onIdle();
-        IdlingRegistry.getInstance().unregister(idlingResource);
-    }
-
-    @Before
-    public void setUp() throws CoreAppTestUtil.ForegroundOccupiedError {
-        assumeTrue(CameraUtil.deviceHasCamera());
-        CoreAppTestUtil.assumeCompatibleDevice();
-
-        // Clear the device UI and check if there is no dialog or lock screen on the top of the
-        // window before start the test.
-        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation());
-
-        // Launch Activity
-        mActivityRule.launchActivity(mIntent);
-    }
-
-    @After
-    public void tearDown() {
-        // Idles Espresso thread and make activity complete each action.
-        waitFor(new ElapsedTimeIdlingResource(IDLE_TIMEOUT_MS));
-
-        mActivityRule.finishActivity();
-
-        // Returns to Home to restart next test.
-        mDevice.pressHome();
-        mDevice.waitForIdle(IDLE_TIMEOUT_MS);
-    }
-
-    @AfterClass
-    public static void shutdownCameraX()
-            throws InterruptedException, ExecutionException, TimeoutException {
-        ProcessCameraProvider cameraProvider = ProcessCameraProvider.getInstance(
-                ApplicationProvider.getApplicationContext()).get(10, TimeUnit.SECONDS);
-        cameraProvider.shutdown().get(10, TimeUnit.SECONDS);
-    }
-
-
-    @Test
-    public void testFlashToggleButton() {
-        waitFor(new WaitForViewToShow(R.id.constraintLayout));
-        assumeTrue(isButtonEnabled(R.id.flash_toggle));
-
-        ImageCapture useCase = mActivityRule.getActivity().getImageCapture();
-        assertNotNull(useCase);
-
-        // There are 3 different states of flash mode: ON, OFF and AUTO.
-        // By pressing flash mode toggle button, the flash mode would switch to the next state.
-        // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
-        @ImageCapture.FlashMode int mode1 = useCase.getFlashMode();
-
-        onView(withId(R.id.flash_toggle)).perform(click());
-        @ImageCapture.FlashMode int mode2 = useCase.getFlashMode();
-        // After the switch, the mode2 should be different from mode1.
-        assertNotEquals(mode2, mode1);
-
-        onView(withId(R.id.flash_toggle)).perform(click());
-        @ImageCapture.FlashMode int mode3 = useCase.getFlashMode();
-        // The mode3 should be different from first and second time.
-        assertNotEquals(mode3, mode2);
-        assertNotEquals(mode3, mode1);
-    }
-
-    @Test
-    public void testTorchToggleButton() {
-        waitFor(new WaitForViewToShow(R.id.constraintLayout));
-        assumeTrue(isButtonEnabled(R.id.torch_toggle));
-
-        CameraInfo cameraInfo = mActivityRule.getActivity().getCameraInfo();
-        assertNotNull(cameraInfo);
-        boolean isTorchOn = isTorchOn(cameraInfo);
-
-        onView(withId(R.id.torch_toggle)).perform(click());
-        assertNotEquals(isTorchOn(cameraInfo), isTorchOn);
-
-        // By pressing the torch toggle button two times, it should switch back to original state.
-        onView(withId(R.id.torch_toggle)).perform(click());
-        assertEquals(isTorchOn(cameraInfo), isTorchOn);
-    }
-
-    @Test
-    public void testSwitchCameraToggleButton() {
-        assumeTrue(CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT));
-        waitFor(new WaitForViewToShow(R.id.direction_toggle));
-
-        assumeNotNull(mActivityRule.getActivity().getPreview());
-
-        for (int i = 0; i < 5; i++) {
-
-            // Wait for preview update.
-            mActivityRule.getActivity().resetViewIdlingResource();
-            IdlingRegistry.getInstance().register(
-                    mActivityRule.getActivity().getViewIdlingResource());
-            onView(withId(R.id.viewFinder)).check(matches(isDisplayed()));
-            IdlingRegistry.getInstance().unregister(
-                    mActivityRule.getActivity().getViewIdlingResource());
-
-            onView(withId(R.id.direction_toggle)).perform(click());
-        }
-    }
-
-    private boolean isTorchOn(CameraInfo cameraInfo) {
-        return cameraInfo.getTorchState().getValue() == TorchState.ON;
-    }
-
-    private boolean isButtonEnabled(int resource) {
-        try {
-            onView(withId(resource)).check(matches(isEnabled()));
-            // View is in hierarchy
-            return true;
-        } catch (AssertionFailedError e) {
-            // View is not in hierarchy
-            return false;
-        } catch (Exception e) {
-            // View is not in hierarchy
-            return false;
-        }
-    }
-}
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
new file mode 100644
index 0000000..d7fc670
--- /dev/null
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ToggleButtonUITest.kt
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.camera.integration.core
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
+import androidx.camera.core.CameraInfo
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.TorchState
+import androidx.camera.integration.core.idlingresource.WaitForViewToShow
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
+import androidx.camera.testing.CameraUtil
+import androidx.camera.testing.CoreAppTestUtil
+import androidx.test.core.app.ActivityScenario
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.espresso.Espresso.onIdle
+import androidx.test.espresso.Espresso.onView
+import androidx.test.espresso.IdlingRegistry
+import androidx.test.espresso.IdlingResource
+import androidx.test.espresso.action.ViewActions.click
+import androidx.test.espresso.assertion.ViewAssertions
+import androidx.test.espresso.matcher.ViewMatchers
+import androidx.test.espresso.matcher.ViewMatchers.withId
+import androidx.test.filters.LargeTest
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
+import androidx.test.uiautomator.UiDevice
+import androidx.testutils.withActivity
+import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.TimeUnit
+import junit.framework.AssertionFailedError
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import org.junit.After
+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
+
+/** Test toggle buttons in CoreTestApp. */
+@LargeTest
+@RunWith(Parameterized::class)
+class ToggleButtonUITest(
+    private val implName: String,
+    private val cameraConfig: String
+) {
+    private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
+
+    @get:Rule
+    val useCamera = CameraUtil.grantCameraPermissionAndPreTest(
+        CameraUtil.PreTestCameraIdList(
+            if (implName == Camera2Config::class.simpleName) {
+                Camera2Config.defaultConfig()
+            } else {
+                CameraPipeConfig.defaultConfig()
+            }
+        )
+    )
+
+    @get:Rule
+    val permissionRule: GrantPermissionRule =
+        GrantPermissionRule.grant(
+            Manifest.permission.WRITE_EXTERNAL_STORAGE,
+            Manifest.permission.RECORD_AUDIO
+        )
+
+    @get:Rule
+    val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+        active = implName == CameraPipeConfig::class.simpleName,
+        forAllTests = true,
+    )
+
+    private val launchIntent = Intent(
+        ApplicationProvider.getApplicationContext(),
+        CameraXActivity::class.java
+    ).apply {
+        putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
+    }
+
+    @Before
+    fun setUp() {
+        assumeTrue(CameraUtil.deviceHasCamera())
+        CoreAppTestUtil.assumeCompatibleDevice()
+        // Use the natural orientation throughout these tests to ensure the activity isn't
+        // recreated unexpectedly. This will also freeze the sensors until
+        // mDevice.unfreezeRotation() in the tearDown() method. Any simulated rotations will be
+        // explicitly initiated from within the test.
+        device.setOrientationNatural()
+        // Clear the device UI and check if there is no dialog or lock screen on the top of the
+        // window before start the test.
+        CoreAppTestUtil.prepareDeviceUI(InstrumentationRegistry.getInstrumentation())
+    }
+
+    @After
+    fun tearDown(): Unit = runBlocking(Dispatchers.Main) {
+        // Returns to Home to restart next test.
+        device.pressHome()
+        device.waitForIdle(IDLE_TIMEOUT_MS)
+        // Unfreeze rotation so the device can choose the orientation via its own policy. Be nice
+        // to other tests :)
+        device.unfreezeRotation()
+
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+        cameraProvider.shutdown()[10, TimeUnit.SECONDS]
+    }
+
+    @Test
+    fun testFlashToggleButton() {
+        ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+            // Arrange.
+            WaitForViewToShow(R.id.constraintLayout).wait()
+            assumeTrue(isButtonEnabled(R.id.flash_toggle))
+            val useCase = scenario.withActivity { imageCapture }
+            // There are 3 different states of flash mode: ON, OFF and AUTO.
+            // By pressing flash mode toggle button, the flash mode would switch to the next state.
+            // The flash mode would loop in following sequence: OFF -> AUTO -> ON -> OFF.
+            // Act.
+            @ImageCapture.FlashMode val mode1 = useCase.flashMode
+            onView(withId(R.id.flash_toggle)).perform(click())
+            @ImageCapture.FlashMode val mode2 = useCase.flashMode
+            onView(withId(R.id.flash_toggle)).perform(click())
+            @ImageCapture.FlashMode val mode3 = useCase.flashMode
+
+            // Assert.
+            // After the switch, the mode2 should be different from mode1.
+            assertThat(mode2).isNotEqualTo(mode1)
+            // The mode3 should be different from first and second time.
+            assertThat(mode3).isNoneOf(mode2, mode1)
+        }
+    }
+
+    @Test
+    fun testTorchToggleButton() {
+        ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+            WaitForViewToShow(R.id.constraintLayout).wait()
+            assumeTrue(isButtonEnabled(R.id.torch_toggle))
+            val cameraInfo = scenario.withActivity { cameraInfo!! }
+            val isTorchOn = cameraInfo.isTorchOn()
+            onView(withId(R.id.torch_toggle)).perform(click())
+            assertThat(cameraInfo.isTorchOn()).isNotEqualTo(isTorchOn)
+            // By pressing the torch toggle button two times, it should switch back to original
+            // state.
+            onView(withId(R.id.torch_toggle)).perform(click())
+            assertThat(cameraInfo.isTorchOn()).isEqualTo(isTorchOn)
+        }
+    }
+
+    @Test
+    fun testSwitchCameraToggleButton() {
+        assumeTrue(
+            "Ignore the camera switch test since there's no front camera.",
+            CameraUtil.hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)
+        )
+        ActivityScenario.launch<CameraXActivity>(launchIntent).use { scenario ->
+            WaitForViewToShow(R.id.direction_toggle).wait()
+            assertThat(scenario.withActivity { preview }).isNotNull()
+            for (i in 0..4) {
+                scenario.waitForViewfinderIdle()
+                // Click switch camera button.
+                onView(withId(R.id.direction_toggle)).perform(click())
+            }
+        }
+    }
+
+    private fun CameraInfo.isTorchOn(): Boolean = torchState.value == TorchState.ON
+
+    private fun isButtonEnabled(resource: Int): Boolean {
+        return try {
+            onView(withId(resource))
+                .check(ViewAssertions.matches(ViewMatchers.isEnabled()))
+            // View is in hierarchy
+            true
+        } catch (e: AssertionFailedError) {
+            // View is not in hierarchy
+            false
+        } catch (e: Exception) {
+            // View is not in hierarchy
+            false
+        }
+    }
+
+    private fun IdlingResource.wait() {
+        IdlingRegistry.getInstance().register(this)
+        onIdle()
+        IdlingRegistry.getInstance().unregister(this)
+    }
+
+    companion object {
+        private const val IDLE_TIMEOUT_MS = 1_000L
+
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(
+                Camera2Config::class.simpleName,
+                CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+            ),
+            arrayOf(
+                CameraPipeConfig::class.simpleName,
+                CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+            )
+        )
+    }
+}
diff --git a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
similarity index 93%
rename from camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
rename to camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
index a351980..f61bdc6 100644
--- a/camera/camera-camera2/src/androidTest/java/androidx/camera/camera2/PreviewTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/camera2/PreviewTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -13,18 +13,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package androidx.camera.camera2
+package androidx.camera.integration.core.camera2
 
 import android.content.Context
 import android.graphics.SurfaceTexture
 import android.util.Size
 import android.view.Surface
+import androidx.camera.camera2.Camera2Config
 import androidx.camera.camera2.internal.DisplayInfoManager
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
 import androidx.camera.core.AspectRatio
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.CameraXConfig
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.Preview
-import androidx.camera.core.SurfaceRequest
 import androidx.camera.core.UseCase
 import androidx.camera.core.impl.ImageOutputConfig
 import androidx.camera.core.impl.utils.executor.CameraXExecutors
@@ -37,7 +39,6 @@
 import androidx.camera.testing.SurfaceTextureProvider.SurfaceTextureCallback
 import androidx.core.util.Consumer
 import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import androidx.test.filters.SdkSuppress
 import androidx.test.platform.app.InstrumentationRegistry
@@ -50,24 +51,40 @@
 import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeoutException
 import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withTimeout
 import org.junit.After
 import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
-import org.mockito.Mockito
-import org.mockito.invocation.InvocationOnMock
+import org.junit.runners.Parameterized
 
 @LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
 @SdkSuppress(minSdkVersion = 21)
-class PreviewTest {
+class PreviewTest(
+    private val implName: String,
+    private val cameraConfig: CameraXConfig
+) {
     @get:Rule
     val cameraRule = CameraUtil.grantCameraPermissionAndPreTest(
-        PreTestCameraIdList(Camera2Config.defaultConfig())
+        PreTestCameraIdList(cameraConfig)
     )
+
+    companion object {
+        private const val ANY_THREAD_NAME = "any-thread-name"
+        private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun data() = listOf(
+            arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()),
+            arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig())
+        )
+    }
+
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
     private var defaultBuilder: Preview.Builder? = null
@@ -81,8 +98,7 @@
     @Throws(ExecutionException::class, InterruptedException::class)
     fun setUp() {
         context = ApplicationProvider.getApplicationContext()
-        val cameraXConfig = Camera2Config.defaultConfig()
-        CameraXUtil.initialize(context!!, cameraXConfig).get()
+        CameraXUtil.initialize(context!!, cameraConfig).get()
 
         // init CameraX before creating Preview to get preview size with CameraX's context
         defaultBuilder = Preview.Builder.fromConfig(Preview.DEFAULT_CONFIG.config)
@@ -106,38 +122,30 @@
     }
 
     @Test
-    fun surfaceProvider_isUsedAfterSetting() {
-        val surfaceProvider = Mockito.mock(
-            Preview.SurfaceProvider::class.java
-        )
-        Mockito.doAnswer { args: InvocationOnMock ->
-            val surfaceTexture = SurfaceTexture(0)
-            surfaceTexture.setDefaultBufferSize(640, 480)
-            val surface = Surface(surfaceTexture)
-            (args.getArgument<Any>(0) as SurfaceRequest).provideSurface(
-                surface,
-                CameraXExecutors.directExecutor()
-            ) {
-                surfaceTexture.release()
-                surface.release()
-            }
-            null
-        }.`when`(surfaceProvider).onSurfaceRequested(
-            ArgumentMatchers.any(
-                SurfaceRequest::class.java
-            )
-        )
+    fun surfaceProvider_isUsedAfterSetting() = runBlocking {
         val preview = defaultBuilder!!.build()
+        val completableDeferred = CompletableDeferred<Unit>()
 
         // TODO(b/160261462) move off of main thread when setSurfaceProvider does not need to be
         //  done on the main thread
-        instrumentation.runOnMainSync { preview.setSurfaceProvider(surfaceProvider) }
-        camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
-        Mockito.verify(surfaceProvider, Mockito.timeout(3000)).onSurfaceRequested(
-            ArgumentMatchers.any(
-                SurfaceRequest::class.java
+        instrumentation.runOnMainSync { preview.setSurfaceProvider { request ->
+            val surfaceTexture = SurfaceTexture(0)
+            surfaceTexture.setDefaultBufferSize(
+                request.resolution.width,
+                request.resolution.height
             )
-        )
+            surfaceTexture.detachFromGLContext()
+            val surface = Surface(surfaceTexture)
+            request.provideSurface(surface, CameraXExecutors.directExecutor()) {
+                surface.release()
+                surfaceTexture.release()
+            }
+            completableDeferred.complete(Unit)
+        } }
+        camera = CameraUtil.createCameraAndAttachUseCase(context!!, cameraSelector, preview)
+        withTimeout(3_000) {
+            completableDeferred.await()
+        }
     }
 
     @Test
@@ -564,9 +572,4 @@
         } while (totalCheckTime < timeoutMs)
         return false
     }
-
-    companion object {
-        private const val ANY_THREAD_NAME = "any-thread-name"
-        private val DEFAULT_RESOLUTION: Size by lazy { Size(640, 480) }
-    }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
index 38b9612..0eb3827 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
@@ -19,7 +19,6 @@
 import android.content.Context
 import android.os.Build
 import android.util.Log
-import android.widget.Toast
 import java.io.File
 import java.io.FileOutputStream
 import java.util.zip.ZipEntry
@@ -30,8 +29,8 @@
  */
 class Diagnosis {
 
-    // TODO: convert to async function
-    fun collectDeviceInfo(context: Context) {
+    // TODO: convert to a suspend function for running different tasks within this function
+    fun collectDeviceInfo(context: Context): File {
         Log.d(TAG, "calling collectDeviceInfo()")
 
         // TODO: verify if external storage is available
@@ -57,12 +56,7 @@
         zout.close()
         fout.close()
 
-        Log.d(TAG, "file at ${tempFile.path}")
-        if (tempFile.exists()) {
-            val msg = "Successfully collected information"
-            Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
-            Log.d(TAG, msg)
-        }
+        return tempFile
     }
 
     private fun createTemp(context: Context, filename: String): File {
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index 519dad6..f195557 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,7 +32,6 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
 import androidx.camera.view.CameraController
 import androidx.camera.view.CameraController.IMAGE_CAPTURE
 import androidx.camera.view.CameraController.VIDEO_CAPTURE
@@ -46,13 +45,21 @@
 import androidx.core.content.ContextCompat
 import java.text.SimpleDateFormat
 import java.util.Locale
-import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
 import androidx.camera.mlkit.vision.MlKitAnalyzer
 import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
 import com.google.android.material.tabs.TabLayout
 import com.google.mlkit.vision.barcode.BarcodeScanner
 import com.google.mlkit.vision.barcode.BarcodeScanning
 import java.io.IOException
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import kotlinx.coroutines.ExecutorCoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 
 class MainActivity : AppCompatActivity() {
 
@@ -65,6 +72,9 @@
     private lateinit var barcodeScanner: BarcodeScanner
     private lateinit var analyzer: MlKitAnalyzer
     private lateinit var diagnoseBtn: Button
+    private lateinit var calibrationExecutor: ExecutorService
+    private var calibrationThreadId: Long = -1
+    private lateinit var diagnosisDispatcher: ExecutorCoroutineDispatcher
 
     override fun onCreate(savedInstanceState: Bundle?) {
         super.onCreate(savedInstanceState)
@@ -79,6 +89,13 @@
         diagnosis = Diagnosis()
         barcodeScanner = BarcodeScanning.getClient()
         diagnoseBtn = findViewById(R.id.diagnose_btn)
+        calibrationExecutor = Executors.newSingleThreadExecutor() { runnable ->
+            val thread = Executors.defaultThreadFactory().newThread(runnable)
+            thread.name = "CalibrationThread"
+            calibrationThreadId = thread.id
+            return@newSingleThreadExecutor thread
+        }
+        diagnosisDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
 
         // Request CAMERA permission and fail gracefully if not granted.
         if (allPermissionsGranted()) {
@@ -125,12 +142,20 @@
         })
 
         diagnoseBtn.setOnClickListener {
-            try {
-                diagnosis.collectDeviceInfo(baseContext)
-            } catch (e: IOException) {
-                val msg = "Failed to collect information"
-                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
-                Log.e(TAG, "IOException caught: ${e.message}")
+            lifecycleScope.launch {
+                try {
+                    val tempFile = withContext(diagnosisDispatcher) {
+                        Log.i(TAG, "dispatcher: ${Thread.currentThread().name}")
+                        diagnosis.collectDeviceInfo(baseContext)
+                    }
+                    Log.d(TAG, "file at ${tempFile.path}")
+                    val msg = "Successfully collected device info"
+                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+                } catch (e: IOException) {
+                    val msg = "Failed to collect information"
+                    Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+                    Log.e(TAG, "IOException caught: ${e.message}")
+                }
             }
         }
     }
@@ -287,20 +312,25 @@
         analyzer = MlKitAnalyzer(
             listOf(barcodeScanner),
             CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
-            CameraXExecutors.mainThreadExecutor()
+            calibrationExecutor
         ) { result ->
+            // validating thread
+            checkCalibrationThread()
             val barcodes = result.getValue(barcodeScanner)
             if (barcodes != null && barcodes.size > 0) {
                 calibrate.analyze(barcodes)
-                // gives overlayView access to Calibration
-                overlayView.setCalibrationResult(calibrate)
-                // enable diagnose button when alignment is successful
-                diagnoseBtn.isEnabled = calibrate.isAligned
-                overlayView.invalidate()
+                // run UI on main thread
+                lifecycleScope.launch {
+                    // gives overlayView access to Calibration
+                    overlayView.setCalibrationResult(calibrate)
+                    // enable diagnose button when alignment is successful
+                    diagnoseBtn.isEnabled = calibrate.isAligned
+                    overlayView.invalidate()
+                }
             }
         }
         cameraController.setImageAnalysisAnalyzer(
-            CameraXExecutors.mainThreadExecutor(), analyzer)
+            calibrationExecutor, analyzer)
     }
 
     private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -309,6 +339,17 @@
         ) == PackageManager.PERMISSION_GRANTED
     }
 
+    private fun checkCalibrationThread() {
+        Preconditions.checkState(calibrationThreadId == Thread.currentThread().id,
+            "Not working on Calibration Thread")
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        calibrationExecutor.shutdown()
+        diagnosisDispatcher.close()
+    }
+
     companion object {
         private const val TAG = "DiagnoseApp"
         private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 5aa38fc..48ca50a 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
         <activity
             android:name=".CameraExtensionsActivity"
             android:exported="true"
-            android:label="Camera Extensions">
+            android:label="CameraX Extensions">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
@@ -35,6 +35,16 @@
         </activity>
 
         <activity
+            android:name=".Camera2ExtensionsActivity"
+            android:exported="false"
+            android:label="Camera2 Extensions">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <activity
             android:name=".validation.CameraValidationResultActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:exported="false">
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
new file mode 100644
index 0000000..d682e82
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -0,0 +1,661 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.ExtensionSessionConfiguration
+import android.hardware.camera2.params.OutputConfiguration
+import android.media.ImageReader
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.Surface
+import android.view.TextureView
+import android.view.ViewStub
+import android.widget.Button
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.calculateRelativeImageRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.createExtensionCaptureCallback
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getDisplayRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getLensFacingCameraId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickPreviewResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickStillImageResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.transformPreview
+import androidx.camera.integration.extensions.utils.FileUtil
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
+import com.google.common.util.concurrent.ListenableFuture
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.concurrent.Executors
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "Camera2ExtensionsAct~"
+private const val EXTENSION_MODE_INVALID = -1
+
+@RequiresApi(31)
+class Camera2ExtensionsActivity : AppCompatActivity() {
+
+    private lateinit var cameraManager: CameraManager
+
+    /**
+     * A reference to the opened [CameraDevice].
+     */
+    private var cameraDevice: CameraDevice? = null
+
+    /**
+     * The current camera extension session.
+     */
+    private var cameraExtensionSession: CameraExtensionSession? = null
+
+    private var currentCameraId = "0"
+
+    private lateinit var backCameraId: String
+    private lateinit var frontCameraId: String
+
+    private var cameraSensorRotationDegrees = 0
+
+    /**
+     * Still capture image reader
+     */
+    private var stillImageReader: ImageReader? = null
+
+    /**
+     * Camera extension characteristics for the current camera device.
+     */
+    private lateinit var extensionCharacteristics: CameraExtensionCharacteristics
+
+    /**
+     * Flag whether we should restart preview after an extension switch.
+     */
+    private var restartPreview = false
+
+    /**
+     * Flag whether we should restart after an camera switch.
+     */
+    private var restartCamera = false
+
+    /**
+     * Track current extension type and index.
+     */
+    private var currentExtensionMode = EXTENSION_MODE_INVALID
+    private var currentExtensionIdx = -1
+    private val supportedExtensionModes = mutableListOf<Int>()
+
+    private lateinit var textureView: TextureView
+
+    private lateinit var previewSurface: Surface
+
+    private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+        override fun onSurfaceTextureAvailable(
+            surfaceTexture: SurfaceTexture,
+            with: Int,
+            height: Int
+        ) {
+            previewSurface = Surface(surfaceTexture)
+            openCameraWithExtensionMode(currentCameraId)
+        }
+
+        override fun onSurfaceTextureSizeChanged(
+            surfaceTexture: SurfaceTexture,
+            with: Int,
+            height: Int
+        ) {
+        }
+
+        override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+            return true
+        }
+
+        override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
+        }
+    }
+
+    private val captureCallbacks = createExtensionCaptureCallback()
+
+    private var restartOnStart = false
+
+    private var activityStopped = false
+
+    private val cameraTaskDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+    private var imageSaveTerminationFuture: ListenableFuture<Any?> = Futures.immediateFuture(null)
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        Log.d(TAG, "onCreate()")
+        setContentView(R.layout.activity_camera_extensions)
+
+        cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
+        backCameraId = getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_BACK)
+        frontCameraId =
+            getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_FRONT)
+
+        currentCameraId = if (isCameraSupportExtensions(backCameraId)) {
+            backCameraId
+        } else if (isCameraSupportExtensions(frontCameraId)) {
+            frontCameraId
+        } else {
+            Toast.makeText(
+                this,
+                "Can't find camera supporting Camera2 extensions.",
+                Toast.LENGTH_SHORT
+            ).show()
+            closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+            return
+        }
+
+        updateExtensionInfo()
+
+        setupTextureView()
+        enableUiControl(false)
+        setupUiControl()
+    }
+
+    private fun isCameraSupportExtensions(cameraId: String): Boolean {
+        val characteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+        return characteristics.supportedExtensions.isNotEmpty()
+    }
+
+    private fun updateExtensionInfo() {
+        Log.d(
+            TAG,
+            "updateExtensionInfo() - camera Id: $currentCameraId, ${
+                getExtensionModeStringFromId(currentExtensionMode)
+            }"
+        )
+        extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(currentCameraId)
+        supportedExtensionModes.clear()
+        supportedExtensionModes.addAll(extensionCharacteristics.supportedExtensions)
+
+        cameraSensorRotationDegrees = cameraManager.getCameraCharacteristics(
+            currentCameraId)[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0
+
+        currentExtensionIdx = -1
+
+        // Checks whether the original selected extension mode is supported by the new target camera
+        if (currentExtensionMode != EXTENSION_MODE_INVALID) {
+            for (i in 0..supportedExtensionModes.size) {
+                if (supportedExtensionModes[i] == currentExtensionMode) {
+                    currentExtensionIdx = i
+                    break
+                }
+            }
+        }
+
+        // Switches to the first supported extension mode if the original selected mode is not
+        // supported
+        if (currentExtensionIdx == -1) {
+            currentExtensionIdx = 0
+            currentExtensionMode = supportedExtensionModes[0]
+        }
+    }
+
+    private fun setupTextureView() {
+        val viewFinderStub = findViewById<ViewStub>(R.id.viewFinderStub)
+        viewFinderStub.layoutResource = R.layout.full_textureview
+        textureView = viewFinderStub.inflate() as TextureView
+        textureView.surfaceTextureListener = surfaceTextureListener
+    }
+
+    private fun enableUiControl(enabled: Boolean) {
+        findViewById<Button>(R.id.PhotoToggle).isEnabled = enabled
+        findViewById<Button>(R.id.Switch).isEnabled = enabled
+        findViewById<Button>(R.id.Picture).isEnabled = enabled
+    }
+
+    private fun setupUiControl() {
+        val extensionModeToggleButton = findViewById<Button>(R.id.PhotoToggle)
+        extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+        extensionModeToggleButton.setOnClickListener {
+            enableUiControl(false)
+            currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensionModes.size
+            currentExtensionMode = supportedExtensionModes[currentExtensionIdx]
+            restartPreview = true
+            extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+
+            closeCaptureSession()
+        }
+
+        val cameraSwitchButton = findViewById<Button>(R.id.Switch)
+        cameraSwitchButton.setOnClickListener {
+            val newCameraId = if (currentCameraId == backCameraId) frontCameraId else backCameraId
+
+            if (!isCameraSupportExtensions(newCameraId)) {
+                Toast.makeText(
+                    this,
+                    "Camera of the other lens facing doesn't support Camera2 extensions.",
+                    Toast.LENGTH_SHORT
+                ).show()
+                return@setOnClickListener
+            }
+
+            enableUiControl(false)
+            currentCameraId = newCameraId
+            restartCamera = true
+
+            closeCamera()
+        }
+
+        val captureButton = findViewById<Button>(R.id.Picture)
+        captureButton.setOnClickListener {
+            enableUiControl(false)
+            takePicture()
+        }
+    }
+
+    override fun onStart() {
+        super.onStart()
+        Log.d(TAG, "onStart()")
+        activityStopped = false
+        if (restartOnStart) {
+            restartOnStart = false
+            openCameraWithExtensionMode(currentCameraId)
+        }
+    }
+
+    override fun onStop() {
+        Log.d(TAG, "onStop()++")
+        super.onStop()
+        // Needs to close the camera first. Otherwise, the next activity might be failed to open
+        // the camera and configure the capture session.
+        runBlocking {
+            closeCaptureSession().await()
+            closeCamera().await()
+        }
+        restartOnStart = true
+        activityStopped = true
+        Log.d(TAG, "onStop()--")
+    }
+
+    override fun onDestroy() {
+        Log.d(TAG, "onDestroy()++")
+        super.onDestroy()
+        previewSurface.release()
+
+        imageSaveTerminationFuture.addListener({ stillImageReader?.close() }, mainExecutor)
+        Log.d(TAG, "onDestroy()--")
+    }
+
+    private fun closeCamera(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+        Log.d(TAG, "closeCamera()++")
+        cameraDevice?.close()
+        cameraDevice = null
+        Log.d(TAG, "closeCamera()--")
+    }
+
+    private fun closeCaptureSession(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+        Log.d(TAG, "closeCaptureSession()++")
+        try {
+            cameraExtensionSession?.close()
+            cameraExtensionSession = null
+        } catch (e: Exception) {
+            Log.e(TAG, e.toString())
+        }
+        Log.d(TAG, "closeCaptureSession()--")
+    }
+
+    private fun openCameraWithExtensionMode(cameraId: String) =
+        lifecycleScope.launch(cameraTaskDispatcher) {
+            Log.d(TAG, "openCameraWithExtensionMode()++ cameraId: $cameraId")
+            cameraDevice = openCamera(cameraManager, cameraId)
+            cameraExtensionSession = openCaptureSession()
+
+            lifecycleScope.launch(Dispatchers.Main) {
+                if (activityStopped) {
+                    closeCaptureSession()
+                    closeCamera()
+                }
+            }
+            Log.d(TAG, "openCameraWithExtensionMode()--")
+        }
+
+    /**
+     * Opens and returns the camera (as the result of the suspend coroutine)
+     */
+    @SuppressLint("MissingPermission")
+    suspend fun openCamera(
+        manager: CameraManager,
+        cameraId: String,
+    ): CameraDevice = suspendCancellableCoroutine { cont ->
+        Log.d(TAG, "openCamera(): $cameraId")
+        manager.openCamera(
+            cameraId,
+            cameraTaskDispatcher.asExecutor(),
+            object : CameraDevice.StateCallback() {
+                override fun onOpened(device: CameraDevice) = cont.resume(device)
+
+                override fun onDisconnected(device: CameraDevice) {
+                    Log.w(TAG, "Camera $cameraId has been disconnected")
+                    finish()
+                }
+
+                override fun onClosed(camera: CameraDevice) {
+                    Log.d(TAG, "Camera - onClosed: $cameraId")
+                    lifecycleScope.launch(Dispatchers.Main) {
+                        if (restartCamera) {
+                            restartCamera = false
+                            updateExtensionInfo()
+                            openCameraWithExtensionMode(currentCameraId)
+                        }
+                    }
+                }
+
+                override fun onError(device: CameraDevice, error: Int) {
+                    Log.d(TAG, "Camera - onError: $cameraId")
+                    val msg = when (error) {
+                        ERROR_CAMERA_DEVICE -> "Fatal (device)"
+                        ERROR_CAMERA_DISABLED -> "Device policy"
+                        ERROR_CAMERA_IN_USE -> "Camera in use"
+                        ERROR_CAMERA_SERVICE -> "Fatal (service)"
+                        ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+                        else -> "Unknown"
+                    }
+                    val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
+                    Log.e(TAG, exc.message, exc)
+                    cont.resumeWithException(exc)
+                }
+            })
+    }
+
+    /**
+     * Opens and returns the extensions session (as the result of the suspend coroutine)
+     */
+    private suspend fun openCaptureSession(): CameraExtensionSession =
+        suspendCancellableCoroutine { cont ->
+            Log.d(TAG, "openCaptureSession")
+            setupPreview()
+
+            if (stillImageReader != null) {
+                val imageReaderToClose = stillImageReader!!
+                imageSaveTerminationFuture.addListener(
+                    { imageReaderToClose.close() },
+                    mainExecutor
+                )
+            }
+
+            stillImageReader = setupImageReader()
+
+            val outputConfig = ArrayList<OutputConfiguration>()
+            outputConfig.add(OutputConfiguration(stillImageReader!!.surface))
+            outputConfig.add(OutputConfiguration(previewSurface))
+            val extensionConfiguration = ExtensionSessionConfiguration(
+                currentExtensionMode, outputConfig,
+                cameraTaskDispatcher.asExecutor(), object : CameraExtensionSession.StateCallback() {
+                    override fun onClosed(session: CameraExtensionSession) {
+                        Log.d(TAG, "CaptureSession - onClosed: $session")
+
+                        lifecycleScope.launch(Dispatchers.Main) {
+                            if (restartPreview) {
+                                restartPreview = false
+
+                                lifecycleScope.launch(cameraTaskDispatcher) {
+                                    cameraExtensionSession = openCaptureSession()
+                                }
+                            }
+                        }
+                    }
+
+                    override fun onConfigured(session: CameraExtensionSession) {
+                        Log.d(TAG, "CaptureSession - onConfigured: $session")
+                        try {
+                            val captureBuilder =
+                                session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+                            captureBuilder.addTarget(previewSurface)
+                            session.setRepeatingRequest(
+                                captureBuilder.build(),
+                                cameraTaskDispatcher.asExecutor(), captureCallbacks
+                            )
+                            cont.resume(session)
+                            runOnUiThread { enableUiControl(true) }
+                        } catch (e: CameraAccessException) {
+                            Log.e(TAG, e.toString())
+                            cont.resumeWithException(
+                                RuntimeException("Failed to create capture session.")
+                            )
+                        }
+                    }
+
+                    override fun onConfigureFailed(session: CameraExtensionSession) {
+                        Log.e(TAG, "CaptureSession - onConfigureFailed: $session")
+                        cont.resumeWithException(
+                            RuntimeException("Configure failed when creating capture session.")
+                        )
+                    }
+                }
+            )
+            try {
+                cameraDevice!!.createExtensionSession(extensionConfiguration)
+            } catch (e: CameraAccessException) {
+                Log.e(TAG, e.toString())
+                cont.resumeWithException(RuntimeException("Failed to create capture session."))
+            }
+        }
+
+    @Suppress("DEPRECATION") /* defaultDisplay */
+    private fun setupPreview() {
+        if (!textureView.isAvailable) {
+            Toast.makeText(
+                this, "TextureView is invalid!!",
+                Toast.LENGTH_SHORT
+            ).show()
+            finish()
+            return
+        }
+
+        val previewResolution = pickPreviewResolution(
+            cameraManager,
+            currentCameraId,
+            resources.displayMetrics,
+            currentExtensionMode
+        )
+
+        if (previewResolution == null) {
+            Toast.makeText(
+                this,
+                "Invalid preview extension sizes!.",
+                Toast.LENGTH_SHORT
+            ).show()
+            finish()
+            return
+        }
+
+        textureView.surfaceTexture?.setDefaultBufferSize(
+            previewResolution.width,
+            previewResolution.height
+        )
+        transformPreview(textureView, previewResolution, windowManager.defaultDisplay.rotation)
+    }
+
+    private fun setupImageReader(): ImageReader {
+        val (size, format) = pickStillImageResolution(
+            extensionCharacteristics,
+            currentExtensionMode
+        )
+
+        return ImageReader.newInstance(size.width, size.height, format, 1)
+    }
+
+    /**
+     * Takes a picture.
+     */
+    private fun takePicture() = lifecycleScope.launch(cameraTaskDispatcher) {
+        Preconditions.checkState(
+            cameraExtensionSession != null,
+            "take picture button is only enabled when session is configured successfully"
+        )
+        val session = cameraExtensionSession!!
+
+        var takePictureCompleter: Completer<Any?>? = null
+
+        imageSaveTerminationFuture = CallbackToFutureAdapter.getFuture<Any?> {
+            takePictureCompleter = it
+            "imageSaveTerminationFuture"
+        }
+
+        stillImageReader!!.setOnImageAvailableListener(
+            { reader: ImageReader ->
+                lifecycleScope.launch(cameraTaskDispatcher) {
+                    acquireImageAndSave(reader)
+                    stillImageReader!!.setOnImageAvailableListener(null, null)
+                    takePictureCompleter?.set(null)
+                    lifecycleScope.launch(Dispatchers.Main) {
+                        enableUiControl(true)
+                    }
+                }
+            }, Handler(Looper.getMainLooper())
+        )
+
+        val captureBuilder = session.device.createCaptureRequest(
+            CameraDevice.TEMPLATE_STILL_CAPTURE
+        )
+        captureBuilder.addTarget(stillImageReader!!.surface)
+
+        session.capture(
+            captureBuilder.build(),
+            cameraTaskDispatcher.asExecutor(),
+            object : CameraExtensionSession.ExtensionCaptureCallback() {
+                override fun onCaptureFailed(
+                    session: CameraExtensionSession,
+                    request: CaptureRequest
+                ) {
+                    takePictureCompleter?.set(null)
+                    Log.e(TAG, "Failed to take picture.")
+                }
+
+                override fun onCaptureSequenceCompleted(
+                    session: CameraExtensionSession,
+                    sequenceId: Int
+                ) {
+                    Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+                }
+            }
+        )
+    }
+
+    /**
+     * Acquires the latest image from the image reader and save it to the Pictures folder
+     */
+    private fun acquireImageAndSave(imageReader: ImageReader) {
+        try {
+            val formatter: Format =
+                SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+            val fileName =
+                "[${formatter.format(Calendar.getInstance().time)}][Camera2]${
+                    getExtensionModeStringFromId(currentExtensionMode)
+                }"
+
+            val rotationDegrees = calculateRelativeImageRotationDegrees(
+                (getDisplayRotationDegrees(display!!.rotation)),
+                cameraSensorRotationDegrees,
+                currentCameraId == backCameraId
+            )
+
+            imageReader.acquireLatestImage().let { image ->
+                val uri = FileUtil.saveImage(
+                    image,
+                    fileName,
+                    ".jpg",
+                    "Pictures/ExtensionsPictures",
+                    contentResolver,
+                    rotationDegrees
+                )
+
+                image.close()
+
+                val msg = if (uri != null) {
+                    "Saved image to $fileName.jpg"
+                } else {
+                    "Failed to save image."
+                }
+
+                lifecycleScope.launch(Dispatchers.Main) {
+                    Toast.makeText(this@Camera2ExtensionsActivity, msg, Toast.LENGTH_SHORT).show()
+                }
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, e.toString())
+        }
+    }
+
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        val inflater = menuInflater
+        inflater.inflate(R.menu.main_menu_camera2_extensions_activity, menu)
+
+        return true
+    }
+
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        when (item.itemId) {
+            R.id.menu_camerax_extensions -> {
+                closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+                return true
+            }
+            R.id.menu_validation_tool -> {
+                closeCameraAndStartActivity(CameraValidationResultActivity::class.java.name)
+                return true
+            }
+        }
+        return super.onOptionsItemSelected(item)
+    }
+
+    private fun closeCameraAndStartActivity(className: String) {
+        // Needs to close the camera first. Otherwise, the next activity might be failed to open
+        // the camera and configure the capture session.
+        runBlocking {
+            closeCaptureSession().await()
+            closeCamera().await()
+        }
+
+        val intent = Intent()
+        intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+        intent.setClassName(this, className)
+        startActivity(intent)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5a0c0ce..47d80f8 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -35,6 +35,7 @@
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
+import android.view.ViewStub;
 import android.widget.Button;
 import android.widget.TextView;
 import android.widget.Toast;
@@ -273,8 +274,8 @@
         captureButton.setOnClickListener((view) -> {
             resetTakePictureIdlingResource();
 
-            String fileName = formatter.format(Calendar.getInstance().getTime())
-                    + extensionModeString + ".jpg";
+            String fileName = "[" + formatter.format(Calendar.getInstance().getTime())
+                    + "][CameraX]" + extensionModeString + ".jpg";
             File saveFile = new File(dir, fileName);
             ImageCapture.OutputFileOptions outputFileOptions;
             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -332,9 +333,9 @@
                                     sendBroadcast(intent);
                                 }
 
-                                Toast.makeText(getApplicationContext(),
-                                        "Saved image to " + saveFile,
-                                        Toast.LENGTH_SHORT).show();
+                                Toast.makeText(CameraExtensionsActivity.this,
+                                        "Saved image to " + fileName,
+                                        Toast.LENGTH_LONG).show();
                             }
                         }
 
@@ -383,7 +384,9 @@
         StrictMode.VmPolicy policy =
                 new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
         StrictMode.setVmPolicy(policy);
-        mPreviewView = findViewById(R.id.previewView);
+        ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
+        viewFinderStub.setLayoutResource(R.layout.full_previewview);
+        mPreviewView = (PreviewView) viewFinderStub.inflate();
         mFrameInfo = findViewById(R.id.frameInfo);
         mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
         setupPinchToZoomAndTapToFocus(mPreviewView);
@@ -427,19 +430,36 @@
 
     @Override
     public boolean onCreateOptionsMenu(@Nullable Menu menu) {
-        MenuInflater inflater = getMenuInflater();
-        inflater.inflate(R.menu.main_menu, menu);
+        if (menu != null) {
+            MenuInflater inflater = getMenuInflater();
+            inflater.inflate(R.menu.main_menu, menu);
+
+            // Remove Camera2Extensions implementation entry if the device API level is less than 32
+            if (Build.VERSION.SDK_INT < 31) {
+                menu.removeItem(R.id.menu_camera2_extensions);
+            }
+        }
         return true;
     }
 
     @Override
     public boolean onOptionsItemSelected(@NonNull MenuItem item) {
-        if (item.getItemId() == R.id.menu_validation_tool) {
-            Intent intent = new Intent(this, CameraValidationResultActivity.class);
-            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
-            startActivity(intent);
-            finish();
-            return true;
+        Intent intent = new Intent();
+        intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+        switch (item.getItemId()) {
+            case R.id.menu_camera2_extensions:
+                if (Build.VERSION.SDK_INT >= 31) {
+                    mCameraProvider.unbindAll();
+                    intent.setClassName(this, Camera2ExtensionsActivity.class.getName());
+                    startActivity(intent);
+                    finish();
+                }
+                return true;
+            case R.id.menu_validation_tool:
+                intent.setClassName(this, CameraValidationResultActivity.class.getName());
+                startActivity(intent);
+                finish();
+                return true;
         }
 
         return super.onOptionsItemSelected(item);
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
new file mode 100644
index 0000000..8bbd835
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
@@ -0,0 +1,347 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.annotation.SuppressLint
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.Size
+import android.view.Surface
+import android.view.TextureView
+import androidx.annotation.RequiresApi
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.stream.Collectors
+
+private const val TAG = "Camera2ExtensionsUtil"
+
+/**
+ * Util functions for Camera2 Extensions implementation
+ */
+object Camera2ExtensionsUtil {
+
+    /**
+     * Converts extension mode from integer to string.
+     */
+    @Suppress("DEPRECATION") // EXTENSION_BEAUTY
+    @JvmStatic
+    fun getExtensionModeStringFromId(extension: Int): String {
+        return when (extension) {
+            CameraExtensionCharacteristics.EXTENSION_HDR -> "HDR"
+            CameraExtensionCharacteristics.EXTENSION_NIGHT -> "NIGHT"
+            CameraExtensionCharacteristics.EXTENSION_BOKEH -> "BOKEH"
+            CameraExtensionCharacteristics.EXTENSION_BEAUTY -> "FACE RETOUCH"
+            else -> "AUTO"
+        }
+    }
+
+    /**
+     * Gets the first camera id of the specified lens facing.
+     */
+    @JvmStatic
+    fun getLensFacingCameraId(cameraManager: CameraManager, lensFacing: Int): String {
+        cameraManager.cameraIdList.forEach { cameraId ->
+            val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+            if (characteristics[CameraCharacteristics.LENS_FACING] == lensFacing) {
+                characteristics[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]?.let {
+                    if (it.contains(
+                            CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
+                        )
+                    ) {
+                        return cameraId
+                    }
+                }
+            }
+        }
+
+        throw IllegalArgumentException("Can't find camera of lens facing $lensFacing")
+    }
+
+    /**
+     * Creates a default extension capture callback implementation.
+     */
+    @RequiresApi(Build.VERSION_CODES.S)
+    @JvmStatic
+    fun createExtensionCaptureCallback(): ExtensionCaptureCallback {
+        return object : ExtensionCaptureCallback() {
+            override fun onCaptureStarted(
+                session: CameraExtensionSession,
+                request: CaptureRequest,
+                timestamp: Long
+            ) {
+            }
+
+            override fun onCaptureProcessStarted(
+                session: CameraExtensionSession,
+                request: CaptureRequest
+            ) {
+            }
+
+            override fun onCaptureFailed(
+                session: CameraExtensionSession,
+                request: CaptureRequest
+            ) {
+                Log.v(TAG, "onCaptureProcessFailed")
+            }
+
+            override fun onCaptureSequenceCompleted(
+                session: CameraExtensionSession,
+                sequenceId: Int
+            ) {
+                Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+            }
+
+            override fun onCaptureSequenceAborted(
+                session: CameraExtensionSession,
+                sequenceId: Int
+            ) {
+                Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId")
+            }
+        }
+    }
+
+    /**
+     * Picks a preview resolution that is both close/same as the display size and supported by camera
+     * and extensions.
+     */
+    @SuppressLint("ClassVerificationFailure")
+    @RequiresApi(Build.VERSION_CODES.S)
+    @JvmStatic
+    fun pickPreviewResolution(
+        cameraManager: CameraManager,
+        cameraId: String,
+        displayMetrics: DisplayMetrics,
+        extensionMode: Int
+    ): Size? {
+        val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+        val map = characteristics.get(
+            CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+        )
+        val textureSizes = map!!.getOutputSizes(
+            SurfaceTexture::class.java
+        )
+        val displaySize = Point()
+        displaySize.x = displayMetrics.widthPixels
+        displaySize.y = displayMetrics.heightPixels
+        if (displaySize.x < displaySize.y) {
+            displaySize.x = displayMetrics.heightPixels
+            displaySize.y = displayMetrics.widthPixels
+        }
+        val displayArRatio = displaySize.x.toFloat() / displaySize.y
+        val previewSizes = ArrayList<Size>()
+        for (sz in textureSizes) {
+            val arRatio = sz.width.toFloat() / sz.height
+            if (Math.abs(arRatio - displayArRatio) <= .2f) {
+                previewSizes.add(sz)
+            }
+        }
+        val extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+        val extensionSizes = extensionCharacteristics.getExtensionSupportedSizes(
+            extensionMode, SurfaceTexture::class.java
+        )
+        if (extensionSizes.isEmpty()) {
+            return null
+        }
+
+        var previewSize = extensionSizes[0]
+        val supportedPreviewSizes =
+            previewSizes.stream().distinct().filter { o: Size -> extensionSizes.contains(o) }
+                .collect(Collectors.toList())
+        if (supportedPreviewSizes.isNotEmpty()) {
+            var currentDistance = Int.MAX_VALUE
+            for (sz in supportedPreviewSizes) {
+                val distance = Math.abs(sz.width * sz.height - displaySize.x * displaySize.y)
+                if (currentDistance > distance) {
+                    currentDistance = distance
+                    previewSize = sz
+                }
+            }
+        } else {
+            Log.w(
+                TAG, "No overlap between supported camera and extensions preview sizes using" +
+                    " first available!"
+            )
+        }
+
+        return previewSize
+    }
+
+    /**
+     * Picks a resolution for still image capture.
+     */
+    @SuppressLint("ClassVerificationFailure")
+    @RequiresApi(Build.VERSION_CODES.S)
+    @JvmStatic
+    fun pickStillImageResolution(
+        extensionCharacteristics: CameraExtensionCharacteristics,
+        extensionMode: Int
+    ): Pair<Size, Int> {
+        val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes(
+            extensionMode, ImageFormat.YUV_420_888
+        )
+        val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes(
+            extensionMode, ImageFormat.JPEG
+        )
+        val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG
+        val stillCaptureSize =
+            if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0]
+
+        return Pair(stillCaptureSize, stillFormat)
+    }
+
+    /**
+     * Transforms the texture view to display the content of resolution in correct direction and
+     * aspect ratio.
+     */
+    @JvmStatic
+    fun transformPreview(textureView: TextureView, resolution: Size, displayRotation: Int) {
+        if (resolution.width == 0 || resolution.height == 0) {
+            return
+        }
+        if (textureView.width == 0 || textureView.height == 0) {
+            return
+        }
+        val matrix = Matrix()
+        val left: Int = textureView.left
+        val right: Int = textureView.right
+        val top: Int = textureView.top
+        val bottom: Int = textureView.bottom
+
+        // Compute the preview ui size based on the available width, height, and ui orientation.
+        val viewWidth = right - left
+        val viewHeight = bottom - top
+        val displayRotationDegrees: Int = getDisplayRotationDegrees(displayRotation)
+        val scaled: Size = calculatePreviewViewDimens(
+            resolution, viewWidth, viewHeight, displayRotation
+        )
+
+        // Compute the center of the view.
+        val centerX = (viewWidth / 2).toFloat()
+        val centerY = (viewHeight / 2).toFloat()
+
+        // Do corresponding rotation to correct the preview direction
+        matrix.postRotate((-displayRotationDegrees).toFloat(), centerX, centerY)
+
+        // Compute the scale value for center crop mode
+        var xScale = scaled.width / viewWidth.toFloat()
+        var yScale = scaled.height / viewHeight.toFloat()
+        if (displayRotationDegrees % 180 == 90) {
+            xScale = scaled.width / viewHeight.toFloat()
+            yScale = scaled.height / viewWidth.toFloat()
+        }
+
+        // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+        // two digits floating value to do the scale operation. Otherwise, the result may be scaled
+        // not large enough and will have some blank lines on the screen.
+        xScale = BigDecimal(xScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+        yScale = BigDecimal(yScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+
+        // Do corresponding scale to resolve the deformation problem
+        matrix.postScale(xScale, yScale, centerX, centerY)
+        textureView.setTransform(matrix)
+    }
+
+    /**
+     * Converts the display rotation to degrees value.
+     *
+     * @return One of 0, 90, 180, 270.
+     */
+    @JvmStatic
+    fun getDisplayRotationDegrees(displayRotation: Int): Int = when (displayRotation) {
+        Surface.ROTATION_0 -> 0
+        Surface.ROTATION_90 -> 90
+        Surface.ROTATION_180 -> 180
+        Surface.ROTATION_270 -> 270
+        else -> throw UnsupportedOperationException(
+            "Unsupported display rotation: $displayRotation"
+        )
+    }
+
+    /**
+     * Calculates the delta between a source rotation and destination rotation.
+     *
+     * <p>A typical use of this method would be calculating the angular difference between the
+     * display orientation (destRotationDegrees) and camera sensor orientation
+     * (sourceRotationDegrees).
+     *
+     * @param destRotationDegrees   The destination rotation relative to the device's natural
+     *                              rotation.
+     * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+     * @param isOppositeFacing      Whether the source and destination planes are facing opposite
+     *                              directions.
+     */
+    @JvmStatic
+    fun calculateRelativeImageRotationDegrees(
+        destRotationDegrees: Int,
+        sourceRotationDegrees: Int,
+        isOppositeFacing: Boolean
+    ): Int {
+        val result: Int = if (isOppositeFacing) {
+            (sourceRotationDegrees - destRotationDegrees + 360) % 360
+        } else {
+            (sourceRotationDegrees + destRotationDegrees) % 360
+        }
+
+        return result
+    }
+
+    /**
+     * Calculates the preview size which can display the source image in correct aspect ratio.
+     */
+    @JvmStatic
+    private fun calculatePreviewViewDimens(
+        srcSize: Size,
+        parentWidth: Int,
+        parentHeight: Int,
+        displayRotation: Int
+    ): Size {
+        var inWidth = srcSize.width
+        var inHeight = srcSize.height
+        if (displayRotation == 0 || displayRotation == 180) {
+            // Need to reverse the width and height since we're in landscape orientation.
+            inWidth = srcSize.height
+            inHeight = srcSize.width
+        }
+        var outWidth = parentWidth
+        var outHeight = parentHeight
+        if (inWidth != 0 && inHeight != 0) {
+            val vfRatio = inWidth / inHeight.toFloat()
+            val parentRatio = parentWidth / parentHeight.toFloat()
+
+            // Match shortest sides together.
+            if (vfRatio < parentRatio) {
+                outWidth = parentWidth
+                outHeight = Math.round(parentWidth / vfRatio)
+            } else {
+                outWidth = Math.round(parentHeight * vfRatio)
+                outHeight = parentHeight
+            }
+        }
+        return Size(outWidth, outHeight)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
new file mode 100644
index 0000000..dbc661d
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.ImageFormat
+import android.media.Image
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import androidx.camera.core.impl.utils.Exif
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+private const val TAG = "FileUtil"
+
+/**
+ * File util functions
+ */
+object FileUtil {
+
+    /**
+     * Saves an [Image] to the specified file path. The format of the input [Image] must be JPEG or
+     * YUV_420_888 format.
+     */
+    @JvmStatic
+    fun saveImage(
+        image: Image,
+        fileNamePrefix: String,
+        fileNameSuffix: String,
+        relativePath: String,
+        contentResolver: ContentResolver,
+        rotationDegrees: Int
+    ): Uri? {
+        require((image.format == ImageFormat.JPEG) or (image.format == ImageFormat.YUV_420_888)) {
+            "Incorrect image format of the input image proxy: ${image.format}"
+        }
+
+        val fileName = if (fileNameSuffix.isNotEmpty() && fileNameSuffix[0] == '.') {
+            fileNamePrefix + fileNameSuffix
+        } else {
+            "$fileNamePrefix.$fileNameSuffix"
+        }
+
+        // Saves the image to the temp file
+        val tempFileUri =
+            saveImageToTempFile(image, fileNamePrefix, fileNameSuffix) ?: return null
+
+        // Updates Exif rotation tag info
+        val exif = Exif.createFromFile(tempFileUri.toFile())
+        exif.rotate(rotationDegrees)
+        exif.save()
+
+        val contentValues = ContentValues().apply {
+            put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+            put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+        }
+
+        // Copies the temp file to the final output path
+        return copyTempFileToOutputLocation(
+            contentResolver,
+            tempFileUri,
+            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+            contentValues
+        )
+    }
+
+    /**
+     * Saves an [Image] to a temp file.
+     */
+    @JvmStatic
+    fun saveImageToTempFile(
+        image: Image,
+        prefix: String,
+        suffix: String,
+        cacheDir: File? = null
+    ): Uri? {
+        val tempFile = File.createTempFile(
+            prefix,
+            suffix,
+            cacheDir
+        )
+
+        val byteArray = when (image.format) {
+            ImageFormat.JPEG -> {
+                ImageUtil.jpegImageToJpegByteArray(image)
+            }
+            ImageFormat.YUV_420_888 -> {
+                ImageUtil.yuvImageToJpegByteArray(image, 100)
+            }
+            else -> {
+                Log.e(TAG, "Incorrect image format of the input image proxy: ${image.format}")
+                return null
+            }
+        }
+
+        val outputStream = FileOutputStream(tempFile)
+        outputStream.write(byteArray)
+        outputStream.close()
+
+        return tempFile.toUri()
+    }
+
+    /**
+     * Copies temp file to the destination location.
+     *
+     * @return null if the copy process is failed.
+     */
+    @JvmStatic
+    fun copyTempFileToOutputLocation(
+        contentResolver: ContentResolver,
+        tempFileUri: Uri,
+        targetUrl: Uri,
+        contentValues: ContentValues,
+    ): Uri? {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+            Log.e(TAG, "The known devices which support Extensions should be at least" +
+                " Android Q!")
+            return null
+        }
+
+        contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+        val outputUri = contentResolver.insert(targetUrl, contentValues) ?: return null
+
+        if (copyTempFileByteArrayToOutputLocation(
+                contentResolver,
+                tempFileUri,
+                outputUri
+            )
+        ) {
+            contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+            contentResolver.update(outputUri, contentValues, null, null)
+            return outputUri
+        } else {
+            Log.e(TAG, "Failed to copy the temp file to the output path!")
+        }
+
+        return null
+    }
+
+    /**
+     * Copies temp file byte array to output [Uri].
+     *
+     * @return false if the [Uri] is not writable.
+     */
+    @JvmStatic
+    private fun copyTempFileByteArrayToOutputLocation(
+        contentResolver: ContentResolver,
+        tempFileUri: Uri,
+        uri: Uri
+    ): Boolean {
+        contentResolver.openOutputStream(uri).use { outputStream ->
+            if (tempFileUri.path == null || outputStream == null) {
+                return false
+            }
+
+            val tempFile = File(tempFileUri.path!!)
+
+            FileInputStream(tempFile).use { inputStream ->
+                val buf = ByteArray(1024)
+                var len: Int
+                while (inputStream.read(buf).also { len = it } > 0) {
+                    outputStream.write(buf, 0, len)
+                }
+            }
+        }
+        return true
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
new file mode 100644
index 0000000..70c1cb5
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.extensions.utils
+
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.Image
+import androidx.annotation.IntRange
+import androidx.camera.core.ImageProxy
+import java.io.ByteArrayOutputStream
+
+/**
+ * Image util functions
+ */
+object ImageUtil {
+
+    /**
+     * Converts JPEG [Image] to [ByteArray]
+     */
+    @JvmStatic
+    fun jpegImageToJpegByteArray(image: Image): ByteArray {
+        require(image.format == ImageFormat.JPEG) {
+            "Incorrect image format of the input image proxy: ${image.format}"
+        }
+        val planes = image.planes
+        val buffer = planes[0].buffer
+        val data = ByteArray(buffer.capacity())
+        buffer.rewind()
+        buffer[data]
+        return data
+    }
+
+    /**
+     * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image
+     * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
+     * be compressed by the specified quality value.
+     */
+    @JvmStatic
+    fun yuvImageToJpegByteArray(
+        image: Image,
+        @IntRange(from = 1, to = 100) jpegQuality: Int
+    ): ByteArray {
+        require(image.format == ImageFormat.YUV_420_888) {
+            "Incorrect image format of the input image proxy: ${image.format}"
+        }
+        return nv21ToJpeg(
+            yuv_420_888toNv21(image),
+            image.width,
+            image.height,
+            jpegQuality
+        )
+    }
+
+    /**
+     * Converts nv21 byte array to JPEG format.
+     */
+    @JvmStatic
+    private fun nv21ToJpeg(
+        nv21: ByteArray,
+        width: Int,
+        height: Int,
+        @IntRange(from = 1, to = 100) jpegQuality: Int
+    ): ByteArray {
+        val out = ByteArrayOutputStream()
+        val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+        val success = yuv.compressToJpeg(Rect(0, 0, width, height), jpegQuality, out)
+
+        if (!success) {
+            throw RuntimeException("YuvImage failed to encode jpeg.")
+        }
+        return out.toByteArray()
+    }
+
+    /**
+     * Converts a YUV [Image] to NV21 byte array.
+     */
+    @JvmStatic
+    private fun yuv_420_888toNv21(image: Image): ByteArray {
+        require(image.format == ImageFormat.YUV_420_888) {
+            "Incorrect image format of the input image proxy: ${image.format}"
+        }
+
+        val yPlane = image.planes[0]
+        val uPlane = image.planes[1]
+        val vPlane = image.planes[2]
+        val yBuffer = yPlane.buffer
+        val uBuffer = uPlane.buffer
+        val vBuffer = vPlane.buffer
+        yBuffer.rewind()
+        uBuffer.rewind()
+        vBuffer.rewind()
+        val ySize = yBuffer.remaining()
+        var position = 0
+        // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+        val nv21 = ByteArray(ySize + image.width * image.height / 2)
+
+        // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+        for (row in 0 until image.height) {
+            yBuffer[nv21, position, image.width]
+            position += image.width
+            yBuffer.position(
+                Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride)
+            )
+        }
+        val chromaHeight = image.height / 2
+        val chromaWidth = image.width / 2
+        val vRowStride = vPlane.rowStride
+        val uRowStride = uPlane.rowStride
+        val vPixelStride = vPlane.pixelStride
+        val uPixelStride = uPlane.pixelStride
+
+        // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+        // perform faster bulk gets from the byte buffers.
+        val vLineBuffer = ByteArray(vRowStride)
+        val uLineBuffer = ByteArray(uRowStride)
+        for (row in 0 until chromaHeight) {
+            vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
+            uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
+            var vLineBufferPosition = 0
+            var uLineBufferPosition = 0
+            for (col in 0 until chromaWidth) {
+                nv21[position++] = vLineBuffer[vLineBufferPosition]
+                nv21[position++] = uLineBuffer[uLineBufferPosition]
+                vLineBufferPosition += vPixelStride
+                uLineBufferPosition += uPixelStride
+            }
+        }
+        return nv21
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 33c44a1..013ef13 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -19,7 +19,6 @@
 import android.annotation.SuppressLint
 import android.content.Intent
 import android.content.res.Configuration
-import android.graphics.ImageFormat
 import android.os.Bundle
 import android.util.Log
 import android.view.GestureDetector
@@ -30,11 +29,13 @@
 import android.widget.Button
 import android.widget.ImageButton
 import android.widget.Toast
+import androidx.annotation.OptIn
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.core.Camera
 import androidx.camera.core.CameraControl
 import androidx.camera.core.CameraInfo
 import androidx.camera.core.DisplayOrientedMeteringPointFactory
+import androidx.camera.core.ExperimentalGetImage
 import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.FocusMeteringResult
 import androidx.camera.core.ImageCapture
@@ -51,6 +52,7 @@
 import androidx.camera.integration.extensions.R
 import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -66,14 +68,11 @@
 import androidx.concurrent.futures.await
 import androidx.core.content.ContextCompat
 import androidx.core.math.MathUtils
-import androidx.core.net.toUri
 import androidx.lifecycle.lifecycleScope
 import com.google.common.util.concurrent.FutureCallback
 import com.google.common.util.concurrent.Futures
 import com.google.common.util.concurrent.ListenableFuture
 import kotlinx.coroutines.launch
-import java.io.File
-import java.io.FileOutputStream
 
 private const val TAG = "ImageCaptureActivity"
 
@@ -196,6 +195,7 @@
         }
     }
 
+    @OptIn(markerClass = [ExperimentalGetImage::class])
     private fun setupUiControls() {
         // Sets up the flash toggle button
         setUpFlashButton()
@@ -227,21 +227,22 @@
                         } else {
                             "$filenamePrefix[Disabled]"
                         }
-                        val tempFile = File.createTempFile(
-                            filename,
-                            "",
-                            codeCacheDir
-                        )
-                        val outputStream = FileOutputStream(tempFile)
-                        val byteArray = jpegImageToJpegByteArray(image)
-                        outputStream.write(byteArray)
-                        outputStream.close()
 
-                        result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, tempFile.toUri())
-                        result.putExtra(
-                            INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
-                            image.imageInfo.rotationDegrees
-                        )
+                        val uri =
+                            FileUtil.saveImageToTempFile(image.image!!, filename, "", cacheDir)
+
+                        if (uri == null) {
+                            result.putExtra(
+                                INTENT_EXTRA_KEY_ERROR_CODE,
+                                ERROR_CODE_SAVE_IMAGE_FAILED
+                            )
+                        } else {
+                            result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, uri)
+                            result.putExtra(
+                                INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
+                                image.imageInfo.rotationDegrees
+                            )
+                        }
                         finish()
                     }
 
@@ -456,25 +457,11 @@
         extensionToggleButton.setImageResource(resourceId)
     }
 
-    /**
-     * Converts JPEG [ImageProxy] to JPEG byte array.
-     */
-    internal fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray {
-        require(image.format == ImageFormat.JPEG) {
-            "Incorrect image format of the input image proxy: ${image.format}"
-        }
-        val planes = image.planes
-        val buffer = planes[0].buffer
-        val data = ByteArray(buffer.capacity())
-        buffer.rewind()
-        buffer[data]
-        return data
-    }
-
     companion object {
         const val ERROR_CODE_NONE = 0
         const val ERROR_CODE_BIND_FAIL = 1
         const val ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT = 2
         const val ERROR_CODE_TAKE_PICTURE_FAILED = 3
+        const val ERROR_CODE_SAVE_IMAGE_FAILED = 4
     }
 }
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 5e01f7a..2782d05 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -35,6 +35,7 @@
 import androidx.appcompat.app.AppCompatActivity
 import androidx.camera.integration.extensions.R
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
 import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -48,13 +49,13 @@
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
+import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_SAVE_IMAGE_FAILED
 import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
 import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
 import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
 import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
-import androidx.camera.integration.extensions.validation.TestResults.Companion.copyTempFileToOutputLocation
 import androidx.core.app.ActivityCompat
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.FragmentActivity
@@ -127,7 +128,8 @@
         // Returns with error
         if (errorCode == ERROR_CODE_BIND_FAIL ||
             errorCode == ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT ||
-            errorCode == ERROR_CODE_TAKE_PICTURE_FAILED
+            errorCode == ERROR_CODE_TAKE_PICTURE_FAILED ||
+            errorCode == ERROR_CODE_SAVE_IMAGE_FAILED
         ) {
             result.putExtra(INTENT_EXTRA_KEY_TEST_RESULT, TEST_RESULT_FAILED)
             Log.e(TAG, "Failed to take a picture with error code: $errorCode")
@@ -196,13 +198,14 @@
             put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/ExtensionsValidation")
         }
 
-        if (copyTempFileToOutputLocation(
-                contentResolver,
-                imageUris[viewPager.currentItem].first,
-                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
-                contentValues
-            )
-        ) {
+        val outputUri = copyTempFileToOutputLocation(
+            contentResolver,
+            imageUris[viewPager.currentItem].first,
+            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+            contentValues
+        )
+
+        if (outputUri != null) {
             Toast.makeText(
                 this,
                 "Image is saved as Pictures/ExtensionsValidation/$savedFileName.",
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
index c4becdf4..1a6fa1b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
@@ -20,8 +20,6 @@
 import android.content.ContentValues
 import android.content.Context
 import android.hardware.camera2.CameraCharacteristics
-import android.net.Uri
-import android.os.Build
 import android.os.Environment.DIRECTORY_DOCUMENTS
 import android.provider.MediaStore
 import android.util.Log
@@ -33,6 +31,7 @@
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeIdFromString
 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
 import androidx.camera.lifecycle.ProcessCameraProvider
 import androidx.core.net.toUri
 import java.io.BufferedReader
@@ -127,7 +126,7 @@
                 testResultsFile.toUri(),
                 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
                 contentValues
-            )
+            ) != null
         ) {
             return "$DIRECTORY_DOCUMENTS/ExtensionsValidation/$savedFileName"
         }
@@ -240,72 +239,6 @@
     }
 
     companion object {
-
-        /**
-         * Copies temp file to the destination location.
-         *
-         * @return false if the copy process is failed.
-         */
-        fun copyTempFileToOutputLocation(
-            contentResolver: ContentResolver,
-            tempFileUri: Uri,
-            targetUrl: Uri,
-            contentValues: ContentValues,
-        ): Boolean {
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
-                Log.e(TAG, "The known devices which support Extensions should be at least" +
-                    " Android Q!")
-                return false
-            }
-
-            contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
-
-            val outputUri = contentResolver.insert(targetUrl, contentValues)
-
-            if (outputUri != null && copyTempFileToOutputLocation(
-                    contentResolver,
-                    tempFileUri,
-                    outputUri
-                )
-            ) {
-                contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
-                contentResolver.update(outputUri, contentValues, null, null)
-                return true
-            } else {
-                Log.e(TAG, "Failed to copy the temp file to the output path!")
-            }
-
-            return false
-        }
-
-        /**
-         * Copies temp file to output [Uri].
-         *
-         * @return false if the [Uri] is not writable.
-         */
-        private fun copyTempFileToOutputLocation(
-            contentResolver: ContentResolver,
-            tempFileUri: Uri,
-            uri: Uri
-        ): Boolean {
-            contentResolver.openOutputStream(uri).use { outputStream ->
-                if (tempFileUri.path == null || outputStream == null) {
-                    return false
-                }
-
-                val tempFile = File(tempFileUri.path!!)
-
-                FileInputStream(tempFile).use { `in` ->
-                    val buf = ByteArray(1024)
-                    var len: Int
-                    while (`in`.read(buf).also { len = it } > 0) {
-                        outputStream.write(buf, 0, len)
-                    }
-                }
-            }
-            return true
-        }
-
         const val INVALID_EXTENSION_MODE = -1
 
         const val TEST_RESULT_NOT_SUPPORTED = -1
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
index 9ed60c5..1a8adfa 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
@@ -24,14 +24,14 @@
     android:layout_height="match_parent"
     tools:context="androidx.camera.integration.extensions.CameraExtensionsActivity">
 
-    <androidx.camera.view.PreviewView
-        android:id="@+id/previewView"
+    <ViewStub
+        android:id="@+id/viewFinderStub"
         android:layout_width="0dp"
         android:layout_height="0dp"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintStart_toStartOf="parent"/>
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent" />
 
     <androidx.constraintlayout.widget.Guideline
         android:id="@+id/guideline"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
new file mode 100644
index 0000000..4c0f530
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
new file mode 100644
index 0000000..18ebf88
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2022 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<TextureView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
index dc9d084..7fe8504 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
@@ -16,6 +16,9 @@
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
     <item
+        android:id="@+id/menu_camera2_extensions"
+        android:title="Camera2 Extensions" />
+    <item
         android:id="@+id/menu_validation_tool"
         android:title="Validation Tool" />
 </menu>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
new file mode 100644
index 0000000..bc8bdba
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 2021 The Android Open Source Project
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_camerax_extensions"
+        android:title="CameraX Extensions" />
+    <item
+        android:id="@+id/menu_validation_tool"
+        android:title="Validation Tool" />
+</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 072b0f5..9c8eef1 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -67,6 +67,7 @@
     implementation(project(":camera:camera-camera2"))
     implementation(project(":camera:camera-lifecycle"))
     implementation(project(":camera:camera-view"))
+    implementation(project(":camera:camera-video"))
 
     // Android Support Library
     implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
index cc6c150..0f0927f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
@@ -16,7 +16,6 @@
 
 package androidx.camera.integration.uiwidgets.compose.ui.navigation
 
-import androidx.camera.integration.uiwidgets.compose.ui.screen.gallery.GalleryScreen
 import androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture.ImageCaptureScreen
 import androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture.VideoCaptureScreen
 import androidx.compose.runtime.Composable
@@ -42,9 +41,5 @@
         composable(ComposeCameraScreen.VideoCapture.name) {
             VideoCaptureScreen()
         }
-
-        composable(ComposeCameraScreen.Gallery.name) {
-            GalleryScreen()
-        }
     }
 }
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
index 8923506..124fe4f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
@@ -18,7 +18,6 @@
 
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.CameraAlt
-import androidx.compose.material.icons.filled.PhotoLibrary
 import androidx.compose.material.icons.filled.Videocam
 import androidx.compose.ui.graphics.vector.ImageVector
 
@@ -32,9 +31,6 @@
     ),
     VideoCapture(
         icon = Icons.Filled.Videocam
-    ),
-    Gallery(
-        icon = Icons.Filled.PhotoLibrary
     );
 
     companion object {
@@ -42,7 +38,6 @@
             return when (route?.substringBefore("/")) {
                 ImageCapture.name -> ImageCapture
                 VideoCapture.name -> VideoCapture
-                Gallery.name -> Gallery
                 null -> defaultRoute
                 else -> throw IllegalArgumentException("Route $route is not recognized.")
             }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
index 37c0d18..0f237e5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
@@ -20,8 +20,10 @@
 import androidx.compose.foundation.layout.size
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
+import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
 
@@ -32,6 +34,7 @@
     imageVector: ImageVector,
     contentDescription: String,
     modifier: Modifier = Modifier,
+    tint: Color = Color.Unspecified,
     onClick: () -> Unit
 ) {
     IconButton(
@@ -41,12 +44,24 @@
         Icon(
             imageVector = imageVector,
             contentDescription = contentDescription,
-            modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+            modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE),
+            tint = tint
         )
     }
 }
 
 @Composable
+fun CameraControlText(
+    text: String,
+    modifier: Modifier = Modifier
+) {
+    Text(
+        text = text,
+        modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+    )
+}
+
+@Composable
 fun CameraControlButtonPlaceholder(modifier: Modifier = Modifier) {
     Spacer(modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE))
 }
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
index 866ca73..914a04e 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreen.kt
@@ -17,16 +17,24 @@
 package androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture
 
 import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
 import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
 import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButtonPlaceholder
 import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
 import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
 import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.CircleShape
 import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
+import androidx.compose.material.Text
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.sharp.FlipCameraAndroid
 import androidx.compose.material.icons.sharp.Lens
@@ -35,7 +43,9 @@
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalLifecycleOwner
 import androidx.compose.ui.unit.dp
@@ -44,10 +54,54 @@
 @Composable
 fun ImageCaptureScreen(
     modifier: Modifier = Modifier,
-    stateHolder: ImageCaptureScreenStateHolder = rememberImageCaptureScreenStateHolder()
+    state: ImageCaptureScreenState = rememberImageCaptureScreenState()
 ) {
     val lifecycleOwner = LocalLifecycleOwner.current
     val localContext = LocalContext.current
+
+    LaunchedEffect(key1 = state.lensFacing) {
+        state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+    }
+
+    ImageCaptureScreen(
+        modifier = modifier,
+        zoomRatio = state.zoomRatio,
+        linearZoom = state.linearZoom,
+        onLinearZoomChange = state::setLinearZoom,
+        isCameraReady = state.isCameraReady,
+        hasFlashUnit = state.hasFlashUnit,
+        flashModeIcon = state.flashModeIcon,
+        onFlashModeIconClicked = state::toggleFlashMode,
+        onFlipCameraIconClicked = state::toggleLensFacing,
+        onImageCaptureIconClicked = {
+            state.takePhoto(localContext)
+        },
+        onSurfaceProviderReady = state::setSurfaceProvider,
+        onTouch = state::startTapToFocus
+    )
+}
+
+@Composable
+fun ImageCaptureScreen(
+    modifier: Modifier,
+    zoomRatio: Float,
+    linearZoom: Float,
+    onLinearZoomChange: (Float) -> Unit,
+    isCameraReady: Boolean,
+    hasFlashUnit: Boolean,
+    flashModeIcon: ImageVector,
+    onFlashModeIconClicked: () -> Unit,
+    onFlipCameraIconClicked: () -> Unit,
+    onImageCaptureIconClicked: () -> Unit,
+    onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+    onTouch: (MeteringPoint) -> Unit
+) {
+    val localContext = LocalContext.current
+
+    // Saving an instance of PreviewView outside of AndroidView
+    // This allows us to access properties of PreviewView (e.g. ViewPort and OutputTransform)
+    // Allows us to support functionalities such as UseCaseGroup in bindToLifecycle()
+    // This instance needs to be carefully used in controlled environments (e.g. LaunchedEffect)
     val previewView = remember {
         PreviewView(localContext).apply {
             layoutParams = ViewGroup.LayoutParams(
@@ -55,51 +109,78 @@
                 ViewGroup.LayoutParams.MATCH_PARENT
             )
 
-            stateHolder.setSurfaceProvider(this.surfaceProvider)
-        }
-    }
+            onSurfaceProviderReady(this.surfaceProvider)
 
-    LaunchedEffect(key1 = stateHolder.lensFacing) {
-        stateHolder.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+            setOnTouchListener { view, motionEvent ->
+                val meteringPointFactory = (view as PreviewView).meteringPointFactory
+                val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+                onTouch(meteringPoint)
+
+                return@setOnTouchListener true
+            }
+        }
     }
 
     Box(modifier = modifier.fillMaxSize()) {
         AndroidView(
-            factory = {
-                previewView
-            }
+            factory = { previewView }
         )
 
-        CameraControlRow(modifier = Modifier.align(Alignment.BottomCenter)) {
-            CameraControlButton(
-                imageVector = Icons.Sharp.FlipCameraAndroid,
-                contentDescription = "Toggle Camera Lens",
-            ) {
-                stateHolder.toggleLensFacing()
+        Column(
+            modifier = Modifier.align(Alignment.BottomCenter),
+            verticalArrangement = Arrangement.Bottom
+        ) {
+
+            // Display Zoom Slider only when Camera is ready
+            if (isCameraReady) {
+                Row(
+                    modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Row(modifier = Modifier.weight(1f)) {
+                        Slider(
+                            value = linearZoom,
+                            onValueChange = onLinearZoomChange
+                        )
+                    }
+
+                    Text(
+                        text = "%.2f x".format(zoomRatio),
+                        modifier = Modifier
+                            .padding(horizontal = 10.dp)
+                            .background(Color.White)
+                    )
+                }
             }
 
-            CameraControlButton(
-                imageVector = Icons.Sharp.Lens,
-                contentDescription = "Image Capture",
-                modifier = Modifier
-                    .padding(1.dp)
-                    .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape)
-            ) {
-                stateHolder.takePhoto(localContext)
-            }
-
-            if (stateHolder.hasFlashMode) {
+            CameraControlRow {
                 CameraControlButton(
-                    imageVector = stateHolder.flashModeIcon,
-                    contentDescription = "Toggle Flash Mode",
+                    imageVector = Icons.Sharp.FlipCameraAndroid,
+                    contentDescription = "Toggle Camera Lens",
+                    onClick = onFlipCameraIconClicked
+                )
+
+                CameraControlButton(
+                    imageVector = Icons.Sharp.Lens,
+                    contentDescription = "Image Capture",
                     modifier = Modifier
                         .padding(1.dp)
-                        .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape)
-                ) {
-                    stateHolder.toggleFlashMode()
+                        .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+                    onClick = onImageCaptureIconClicked
+                )
+
+                if (hasFlashUnit) {
+                    CameraControlButton(
+                        imageVector = flashModeIcon,
+                        contentDescription = "Toggle Flash Mode",
+                        modifier = Modifier
+                            .padding(1.dp)
+                            .border(1.dp, MaterialTheme.colors.onSecondary, RectangleShape),
+                        onClick = onFlashModeIconClicked
+                    )
+                } else {
+                    CameraControlButtonPlaceholder()
                 }
-            } else {
-                CameraControlButtonPlaceholder()
             }
         }
     }
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
similarity index 71%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
rename to camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
index a3d1349..ee8bd3c 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenStateHolder.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/imagecapture/ImageCaptureScreenState.kt
@@ -22,9 +22,13 @@
 import android.provider.MediaStore
 import android.util.Log
 import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl.OperationCanceledException
 import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
 import androidx.camera.core.ImageCapture
 import androidx.camera.core.ImageCaptureException
+import androidx.camera.core.MeteringPoint
 import androidx.camera.core.Preview
 import androidx.camera.core.Preview.SurfaceProvider
 import androidx.camera.lifecycle.ProcessCameraProvider
@@ -40,22 +44,28 @@
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.concurrent.futures.await
 import androidx.core.content.ContextCompat
 import androidx.lifecycle.LifecycleOwner
 import java.text.SimpleDateFormat
 import java.util.Locale
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
 
 private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
 private const val DEFAULT_FLASH_MODE = ImageCapture.FLASH_MODE_OFF
 
-class ImageCaptureScreenStateHolder(
+class ImageCaptureScreenState(
     initialLensFacing: Int = DEFAULT_LENS_FACING,
     initialFlashMode: Int = DEFAULT_FLASH_MODE
 ) {
     var lensFacing by mutableStateOf(initialLensFacing)
         private set
 
-    var hasFlashMode by mutableStateOf(false)
+    var hasFlashUnit by mutableStateOf(false)
+        private set
+
+    var isCameraReady by mutableStateOf(false)
         private set
 
     var flashMode: Int by mutableStateOf(getValidInitialFlashMode(initialFlashMode))
@@ -65,17 +75,49 @@
         private set
         get() = getFlashModeImageVector()
 
+    var linearZoom by mutableStateOf(0f)
+        private set
+
+    var zoomRatio by mutableStateOf(1f)
+        private set
+
     private val preview = Preview.Builder().build()
     private val imageCapture = ImageCapture
         .Builder()
         .setFlashMode(flashMode)
         .build()
 
+    private var camera: Camera? = null
+
+    private val mainScope = MainScope()
+
     fun setSurfaceProvider(surfaceProvider: SurfaceProvider) {
         Log.d(TAG, "Setting Surface Provider")
         preview.setSurfaceProvider(surfaceProvider)
     }
 
+    @JvmName("setLinearZoomFunction")
+    fun setLinearZoom(linearZoom: Float) {
+        Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not ready to set Linear Zoom")
+            return
+        }
+
+        val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+        mainScope.launch {
+            try {
+                future.await()
+            } catch (exc: Exception) {
+                // Log errors not related to CameraControl.OperationCanceledException
+                if (exc !is OperationCanceledException) {
+                    Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+                }
+            }
+        }
+    }
+
     fun toggleLensFacing() {
         Log.d(TAG, "Toggling Lens")
         lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
@@ -104,8 +146,14 @@
         imageCapture.flashMode = flashMode
     }
 
+    fun startTapToFocus(meteringPoint: MeteringPoint) {
+        val action = FocusMeteringAction.Builder(meteringPoint).build()
+        camera?.cameraControl?.startFocusAndMetering(action)
+    }
+
     fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
         Log.d(TAG, "Starting Camera")
+
         val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
 
         cameraProviderFuture.addListener({
@@ -116,6 +164,14 @@
                 .requireLensFacing(lensFacing)
                 .build()
 
+            // Remove observers from the old camera instance
+            removeZoomStateObservers(lifecycleOwner)
+
+            // Reset internal State of Camera
+            camera = null
+            hasFlashUnit = false
+            isCameraReady = false
+
             try {
                 cameraProvider.unbindAll()
                 val camera = cameraProvider.bindToLifecycle(
@@ -126,7 +182,10 @@
                 )
 
                 // Setup components that require Camera
-                this.hasFlashMode = camera.cameraInfo.hasFlashUnit()
+                this.camera = camera
+                setupZoomStateObserver(lifecycleOwner)
+                hasFlashUnit = camera.cameraInfo.hasFlashUnit()
+                isCameraReady = true
             } catch (exc: Exception) {
                 Log.e(TAG, "Use Cases binding failed", exc)
             }
@@ -201,6 +260,32 @@
         }
     }
 
+    private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+        Log.d(TAG, "Setting up Zoom State Observer")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not ready to set up observer")
+            return
+        }
+
+        removeZoomStateObservers(lifecycleOwner)
+        camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+            linearZoom = state.linearZoom
+            zoomRatio = state.zoomRatio
+        }
+    }
+
+    private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+        Log.d(TAG, "Removing Observers")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not present to remove observers")
+            return
+        }
+
+        camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+    }
+
     companion object {
         private const val TAG = "ImageCaptureScreenState"
         private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
@@ -209,12 +294,12 @@
             ImageCapture.FLASH_MODE_OFF,
             ImageCapture.FLASH_MODE_AUTO
         )
-        val saver: Saver<ImageCaptureScreenStateHolder, *> = listSaver(
+        val saver: Saver<ImageCaptureScreenState, *> = listSaver(
             save = {
                 listOf(it.lensFacing, it.flashMode)
             },
             restore = {
-                ImageCaptureScreenStateHolder(
+                ImageCaptureScreenState(
                     initialLensFacing = it[0],
                     initialFlashMode = it[1]
                 )
@@ -224,16 +309,16 @@
 }
 
 @Composable
-fun rememberImageCaptureScreenStateHolder(
+fun rememberImageCaptureScreenState(
     initialLensFacing: Int = DEFAULT_LENS_FACING,
     initialFlashMode: Int = DEFAULT_FLASH_MODE
-): ImageCaptureScreenStateHolder {
+): ImageCaptureScreenState {
     return rememberSaveable(
         initialLensFacing,
         initialFlashMode,
-        saver = ImageCaptureScreenStateHolder.saver
+        saver = ImageCaptureScreenState.saver
     ) {
-        ImageCaptureScreenStateHolder(
+        ImageCaptureScreenState(
             initialLensFacing = initialLensFacing,
             initialFlashMode = initialFlashMode
         )
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 726739f..acc60e6 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -16,10 +16,171 @@
 
 package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
 
+import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlText
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.FlipCameraAndroid
+import androidx.compose.material.icons.sharp.Lens
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
 
 @Composable
-fun VideoCaptureScreen() {
-    Text("Video Capture Screen")
+fun VideoCaptureScreen(
+    modifier: Modifier = Modifier,
+    state: VideoCaptureScreenState = rememberVideoCaptureScreenState()
+) {
+    val lifecycleOwner = LocalLifecycleOwner.current
+    val localContext = LocalContext.current
+
+    LaunchedEffect(key1 = state.lensFacing) {
+        state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+    }
+
+    VideoCaptureScreen(
+        modifier = modifier,
+        zoomRatio = state.zoomRatio,
+        linearZoom = state.linearZoom,
+        onLinearZoomChange = state::setLinearZoom,
+        isCameraReady = state.isCameraReady,
+        recordState = state.recordState,
+        recordingStatsMsg = state.recordingStatsMsg,
+        onFlipCameraIconClicked = state::toggleLensFacing,
+        onVideoCaptureIconClicked = {
+            state.captureVideo(localContext)
+        },
+        onSurfaceProviderReady = state::setSurfaceProvider,
+        onTouch = state::startTapToFocus
+    )
+}
+
+@Composable
+fun VideoCaptureScreen(
+    modifier: Modifier = Modifier,
+    zoomRatio: Float,
+    linearZoom: Float,
+    onLinearZoomChange: (Float) -> Unit,
+    isCameraReady: Boolean,
+    recordState: VideoCaptureScreenState.RecordState,
+    recordingStatsMsg: String,
+    onFlipCameraIconClicked: () -> Unit,
+    onVideoCaptureIconClicked: () -> Unit,
+    onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+    onTouch: (MeteringPoint) -> Unit
+) {
+    val localContext = LocalContext.current
+
+    val previewView = remember {
+        PreviewView(localContext).apply {
+            layoutParams = ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT
+            )
+
+            onSurfaceProviderReady(this.surfaceProvider)
+
+            setOnTouchListener { view, motionEvent ->
+                val meteringPointFactory = (view as PreviewView).meteringPointFactory
+                val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+                onTouch(meteringPoint)
+
+                return@setOnTouchListener true
+            }
+        }
+    }
+
+    Box(modifier = modifier.fillMaxSize()) {
+        AndroidView(
+            factory = { previewView }
+        )
+
+        Column(
+            modifier = Modifier.align(Alignment.BottomCenter),
+            verticalArrangement = Arrangement.Bottom
+        ) {
+
+            // Display Zoom Slider only when Camera is ready
+            if (isCameraReady) {
+                Row(
+                    modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+                    verticalAlignment = Alignment.CenterVertically
+                ) {
+                    Row(modifier = Modifier.weight(1f)) {
+                        Slider(
+                            value = linearZoom,
+                            onValueChange = onLinearZoomChange
+                        )
+                    }
+
+                    Text(
+                        text = "%.2f x".format(zoomRatio),
+                        modifier = Modifier
+                            .padding(horizontal = 10.dp)
+                            .background(Color.White)
+                    )
+                }
+            }
+
+            CameraControlRow {
+                CameraControlButton(
+                    imageVector = Icons.Sharp.FlipCameraAndroid,
+                    contentDescription = "Toggle Camera Lens",
+                    onClick = onFlipCameraIconClicked
+                )
+
+                VideoRecordButton(
+                    recordState = recordState,
+                    onVideoCaptureIconClicked = onVideoCaptureIconClicked
+                )
+
+                CameraControlText(text = recordingStatsMsg)
+            }
+        }
+    }
+}
+
+@Composable
+private fun VideoRecordButton(
+    recordState: VideoCaptureScreenState.RecordState,
+    onVideoCaptureIconClicked: () -> Unit
+) {
+    val iconColor = when (recordState) {
+        VideoCaptureScreenState.RecordState.IDLE -> Color.Black
+        VideoCaptureScreenState.RecordState.RECORDING -> Color.Red
+        VideoCaptureScreenState.RecordState.STOPPING -> Color.Gray
+    }
+
+    CameraControlButton(
+        imageVector = Icons.Sharp.Lens,
+        contentDescription = "Video Capture",
+        modifier = Modifier
+            .padding(1.dp)
+            .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+        tint = iconColor,
+        onClick = onVideoCaptureIconClicked
+    )
 }
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
new file mode 100644
index 0000000..bd6b3cc
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
@@ -0,0 +1,322 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
+import androidx.lifecycle.LifecycleOwner
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
+
+class VideoCaptureScreenState(
+    initialLensFacing: Int = DEFAULT_LENS_FACING
+) {
+    var lensFacing by mutableStateOf(initialLensFacing)
+        private set
+
+    var isCameraReady by mutableStateOf(false)
+        private set
+
+    var linearZoom by mutableStateOf(0f)
+        private set
+
+    var zoomRatio by mutableStateOf(1f)
+        private set
+
+    private var recording: Recording? = null
+
+    var recordState by mutableStateOf(RecordState.IDLE)
+        private set
+
+    var recordingStatsMsg by mutableStateOf("")
+        private set
+
+    private val preview = Preview.Builder().build()
+    private lateinit var recorder: Recorder
+    private lateinit var videoCapture: VideoCapture<Recorder>
+
+    private var camera: Camera? = null
+
+    private val mainScope = MainScope()
+
+    fun setSurfaceProvider(surfaceProvider: Preview.SurfaceProvider) {
+        Log.d(TAG, "Setting Surface Provider")
+        preview.setSurfaceProvider(surfaceProvider)
+    }
+
+    @JvmName("setLinearZoomFunction")
+    fun setLinearZoom(linearZoom: Float) {
+        Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not ready to set Linear Zoom")
+            return
+        }
+
+        val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+        mainScope.launch {
+            try {
+                future.await()
+            } catch (exc: Exception) {
+                // Log errors not related to CameraControl.OperationCanceledException
+                if (exc !is CameraControl.OperationCanceledException) {
+                    Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+                }
+            }
+        }
+    }
+
+    fun toggleLensFacing() {
+        Log.d(TAG, "Toggling Lens")
+        lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
+            CameraSelector.LENS_FACING_FRONT
+        } else {
+            CameraSelector.LENS_FACING_BACK
+        }
+    }
+
+    fun startTapToFocus(meteringPoint: MeteringPoint) {
+        val action = FocusMeteringAction.Builder(meteringPoint).build()
+        camera?.cameraControl?.startFocusAndMetering(action)
+    }
+
+    fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
+        Log.d(TAG, "Starting Camera")
+        val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+
+        cameraProviderFuture.addListener({
+            val cameraProvider = cameraProviderFuture.get()
+
+            // Create a new recorder. CameraX currently does not support re-use of Recorder
+            recorder =
+                Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()
+            videoCapture = VideoCapture.withOutput(recorder)
+
+            val cameraSelector = CameraSelector
+                .Builder()
+                .requireLensFacing(lensFacing)
+                .build()
+
+            // Remove observers from the old camera instance
+            removeZoomStateObservers(lifecycleOwner)
+
+            // Reset internal State of Camera
+            camera = null
+            isCameraReady = false
+
+            try {
+                cameraProvider.unbindAll()
+                val camera = cameraProvider.bindToLifecycle(
+                    lifecycleOwner,
+                    cameraSelector,
+                    preview,
+                    videoCapture
+                )
+
+                this.camera = camera
+                setupZoomStateObserver(lifecycleOwner)
+                isCameraReady = true
+            } catch (exc: Exception) {
+                Log.e(TAG, "Use Cases binding failed", exc)
+            }
+        }, ContextCompat.getMainExecutor(context))
+    }
+
+    fun captureVideo(context: Context) {
+        Log.d(TAG, "Capture Video")
+
+        // Disable button if CameraX is already stopping the recording
+        if (recordState == RecordState.STOPPING) {
+            return
+        }
+
+        // Stop current recording session
+        val curRecording = recording
+        if (curRecording != null) {
+            Log.d(TAG, "Recording session exists. Stop recording")
+            recordState = RecordState.STOPPING
+            curRecording.stop()
+            return
+        }
+
+        Log.d(TAG, "Start recording video")
+        val mediaStoreOutputOptions = getMediaStoreOutputOptions(context)
+
+        recording = videoCapture.output
+            .prepareRecording(context, mediaStoreOutputOptions)
+            .apply {
+                val recordAudioPermission = PermissionChecker.checkSelfPermission(
+                    context,
+                    Manifest.permission.RECORD_AUDIO
+                )
+
+                if (recordAudioPermission == PermissionChecker.PERMISSION_GRANTED) {
+                    withAudioEnabled()
+                }
+            }
+            .start(ContextCompat.getMainExecutor(context)) { recordEvent ->
+                // Update record stats
+                val recordingStats = recordEvent.recordingStats
+                val durationMs = TimeUnit.NANOSECONDS.toMillis(recordingStats.recordedDurationNanos)
+                val sizeMb = recordingStats.numBytesRecorded / (1000f * 1000f)
+                val msg = "%.2f s\n%.2f MB".format(durationMs / 1000f, sizeMb)
+                recordingStatsMsg = msg
+
+                when (recordEvent) {
+                    is VideoRecordEvent.Start -> {
+                        recordState = RecordState.RECORDING
+                    }
+                    is VideoRecordEvent.Finalize -> {
+                        // Once finalized, save the file if it is created
+                        val cause = recordEvent.cause
+                        when (val errorCode = recordEvent.error) {
+                            ERROR_NONE, ERROR_SOURCE_INACTIVE -> { // Save Output
+                                val uri = recordEvent.outputResults.outputUri
+                                val successMsg = "Video saved at $uri. Code: $errorCode"
+                                Log.d(TAG, successMsg, cause)
+                                Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show()
+                            }
+                            else -> { // Handle Error
+                                val failureMsg = "VideoCapture Error($errorCode): $cause"
+                                Log.e(TAG, failureMsg, cause)
+                            }
+                        }
+
+                        // Tear down recording
+                        recordState = RecordState.IDLE
+                        recording = null
+                        recordingStatsMsg = ""
+                    }
+                }
+            }
+    }
+
+    private fun getMediaStoreOutputOptions(context: Context): MediaStoreOutputOptions {
+        val contentResolver = context.contentResolver
+        val displayName = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
+            .format(System.currentTimeMillis())
+        val contentValues = ContentValues().apply {
+            put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+            put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+                put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
+            }
+        }
+
+        return MediaStoreOutputOptions
+            .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+            .setContentValues(contentValues)
+            .build()
+    }
+
+    private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+        Log.d(TAG, "Setting up Zoom State Observer")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not ready to set up observer")
+            return
+        }
+
+        removeZoomStateObservers(lifecycleOwner)
+        camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+            linearZoom = state.linearZoom
+            zoomRatio = state.zoomRatio
+        }
+    }
+
+    private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+        Log.d(TAG, "Removing Observers")
+
+        if (camera == null) {
+            Log.d(TAG, "Camera is not present to remove observers")
+            return
+        }
+
+        camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+    }
+
+    enum class RecordState {
+        IDLE,
+        RECORDING,
+        STOPPING
+    }
+
+    companion object {
+        private const val TAG = "VideoCaptureScreenState"
+        private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
+        val saver: Saver<VideoCaptureScreenState, *> = listSaver(
+            save = {
+                listOf(it.lensFacing)
+            },
+            restore = {
+                VideoCaptureScreenState(
+                    initialLensFacing = it[0]
+                )
+            }
+        )
+    }
+}
+
+@Composable
+fun rememberVideoCaptureScreenState(
+    initialLensFacing: Int = DEFAULT_LENS_FACING
+): VideoCaptureScreenState {
+    return rememberSaveable(
+        initialLensFacing,
+        saver = VideoCaptureScreenState.saver
+    ) {
+        VideoCaptureScreenState(
+            initialLensFacing = initialLensFacing
+        )
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 28fe8db..408f301 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -135,6 +135,21 @@
     }
 
     @Test
+    fun enableEffect_effectIsEnabled() {
+        // Arrange: launch app and verify effect is inactive.
+        fragment.assertPreviewIsStreaming()
+        assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isFalse()
+
+        // Act: turn on effect.
+        val effectToggleId = "androidx.camera.integration.view:id/effect_toggle"
+        uiDevice.findObject(UiSelector().resourceId(effectToggleId)).click()
+        instrumentation.waitForIdleSync()
+
+        // Assert: verify that effect is active.
+        assertThat(fragment.mSurfaceEffect.isSurfaceRequestedAndProvided()).isTrue()
+    }
+
+    @Test
     fun controllerBound_canGetCameraControl() {
         fragment.assertPreviewIsStreaming()
         instrumentation.runOnMainSync {
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index e765274..14833f1 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -16,6 +16,8 @@
 
 package androidx.camera.integration.view;
 
+import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+
 import android.annotation.SuppressLint;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -40,12 +42,13 @@
 import androidx.annotation.RestrictTo;
 import androidx.annotation.VisibleForTesting;
 import androidx.camera.core.CameraSelector;
+import androidx.camera.core.EffectBundle;
 import androidx.camera.core.ImageAnalysis;
 import androidx.camera.core.ImageCapture;
 import androidx.camera.core.ImageCaptureException;
 import androidx.camera.core.Logger;
+import androidx.camera.core.SurfaceEffect;
 import androidx.camera.core.ZoomState;
-import androidx.camera.core.impl.utils.executor.CameraXExecutors;
 import androidx.camera.core.impl.utils.futures.FutureCallback;
 import androidx.camera.core.impl.utils.futures.Futures;
 import androidx.camera.view.CameraController;
@@ -86,6 +89,7 @@
     private FrameLayout mContainer;
     private Button mFlashMode;
     private ToggleButton mCameraToggle;
+    private ToggleButton mEffectToggle;
     private ExecutorService mExecutorService;
     private ToggleButton mCaptureEnabledToggle;
     private ToggleButton mAnalysisEnabledToggle;
@@ -106,6 +110,9 @@
     @Nullable
     private ImageAnalysis.Analyzer mWrappedAnalyzer;
 
+    @VisibleForTesting
+    ToneMappingSurfaceEffect mSurfaceEffect;
+
     private final ImageAnalysis.Analyzer mAnalyzer = image -> {
         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
         image.getPlanes()[0].getBuffer().get(bytes);
@@ -134,7 +141,7 @@
         mExecutorService = Executors.newSingleThreadExecutor();
         mRotationProvider = new RotationProvider(requireContext());
         boolean canDetectRotation = mRotationProvider.addListener(
-                CameraXExecutors.mainThreadExecutor(), mRotationListener);
+                mainThreadExecutor(), mRotationListener);
         if (!canDetectRotation) {
             Logger.e(TAG, "The device cannot detect rotation with motion sensor.");
         }
@@ -159,6 +166,12 @@
             }
         });
 
+        // Set up post-processing effects.
+        mSurfaceEffect = new ToneMappingSurfaceEffect();
+        mEffectToggle = view.findViewById(R.id.effect_toggle);
+        mEffectToggle.setOnCheckedChangeListener((compoundButton, isChecked) -> onEffectsToggled());
+        onEffectsToggled();
+
         // Set up the button to change the PreviewView's size.
         view.findViewById(R.id.shrink).setOnClickListener(v -> {
             // Shrinks PreviewView by 10% each time it's clicked.
@@ -341,6 +354,17 @@
             mExecutorService.shutdown();
         }
         mRotationProvider.removeListener(mRotationListener);
+        mSurfaceEffect.release();
+    }
+
+    private void onEffectsToggled() {
+        if (mEffectToggle.isChecked()) {
+            mCameraController.setEffectBundle(new EffectBundle.Builder(mainThreadExecutor())
+                    .addEffect(SurfaceEffect.PREVIEW, mSurfaceEffect)
+                    .build());
+        } else if (mSurfaceEffect != null) {
+            mCameraController.setEffectBundle(null);
+        }
     }
 
     void checkFailedFuture(ListenableFuture<Void> voidFuture) {
@@ -355,7 +379,7 @@
             public void onFailure(@NonNull Throwable t) {
                 toast(t.getMessage());
             }
-        }, CameraXExecutors.mainThreadExecutor());
+        }, mainThreadExecutor());
     }
 
     // Synthetic access
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
new file mode 100644
index 0000000..2ed13ae
--- /dev/null
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/ToneMappingSurfaceEffect.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.view
+
+import android.graphics.SurfaceTexture
+import android.graphics.SurfaceTexture.OnFrameAvailableListener
+import android.os.Handler
+import android.os.Looper
+import android.view.Surface
+import androidx.annotation.VisibleForTesting
+import androidx.camera.core.SurfaceEffect
+import androidx.camera.core.SurfaceOutput
+import androidx.camera.core.SurfaceRequest
+import androidx.camera.core.impl.utils.Threads.checkMainThread
+import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
+import androidx.camera.core.processing.OpenGlRenderer
+import androidx.camera.core.processing.ShaderProvider
+
+/**
+ * A effect that applies tone mapping on camera output.
+ *
+ * <p>The thread safety is guaranteed by using the main thread.
+ */
+class ToneMappingSurfaceEffect : SurfaceEffect, OnFrameAvailableListener {
+
+    companion object {
+        // A fragment shader that applies a yellow hue.
+        private val TONE_MAPPING_SHADER_PROVIDER = object : ShaderProvider {
+            override fun createFragmentShader(sampler: String, fragCoords: String): String {
+                return """
+                    #extension GL_OES_EGL_image_external : require
+                    precision mediump float;
+                    uniform samplerExternalOES $sampler;
+                    varying vec2 $fragCoords;
+                    void main() {
+                      vec4 sampleColor = texture2D($sampler, $fragCoords);
+                      gl_FragColor = vec4(
+                           sampleColor.r * 0.5 + sampleColor.g * 0.8 + sampleColor.b * 0.3,
+                           sampleColor.r * 0.4 + sampleColor.g * 0.7 + sampleColor.b * 0.2,
+                           sampleColor.r * 0.3 + sampleColor.g * 0.5 + sampleColor.b * 0.1,
+                           1.0);
+                     }
+                    """
+            }
+        }
+    }
+
+    private val mainThreadHandler: Handler = Handler(Looper.getMainLooper())
+    private val glRenderer: OpenGlRenderer = OpenGlRenderer()
+    private val outputSurfaces: MutableMap<SurfaceOutput, Surface> = mutableMapOf()
+    private val textureTransform: FloatArray = FloatArray(16)
+    private val surfaceTransform: FloatArray = FloatArray(16)
+    private var isReleased = false
+
+    // For testing.
+    private var surfaceRequested = false
+    // For testing.
+    private var outputSurfaceProvided = false
+
+    init {
+        mainThreadExecutor().execute {
+            glRenderer.init(TONE_MAPPING_SHADER_PROVIDER)
+        }
+    }
+
+    override fun onInputSurface(surfaceRequest: SurfaceRequest) {
+        checkMainThread()
+        if (isReleased) {
+            surfaceRequest.willNotProvideSurface()
+            return
+        }
+        surfaceRequested = true
+        val surfaceTexture = SurfaceTexture(glRenderer.textureName)
+        surfaceTexture.setDefaultBufferSize(
+            surfaceRequest.resolution.width, surfaceRequest.resolution.height
+        )
+        val surface = Surface(surfaceTexture)
+        surfaceRequest.provideSurface(surface, mainThreadExecutor()) {
+            surfaceTexture.setOnFrameAvailableListener(null)
+            surfaceTexture.release()
+            surface.release()
+        }
+        surfaceTexture.setOnFrameAvailableListener(this, mainThreadHandler)
+    }
+
+    override fun onOutputSurface(surfaceOutput: SurfaceOutput) {
+        checkMainThread()
+        outputSurfaceProvided = true
+        if (isReleased) {
+            surfaceOutput.close()
+            return
+        }
+        outputSurfaces[surfaceOutput] = surfaceOutput.getSurface(mainThreadExecutor()) {
+            surfaceOutput.close()
+            outputSurfaces.remove(surfaceOutput)
+        }
+    }
+
+    @VisibleForTesting
+    fun isSurfaceRequestedAndProvided(): Boolean {
+        return surfaceRequested && outputSurfaceProvided
+    }
+
+    fun release() {
+        checkMainThread()
+        if (isReleased) {
+            return
+        }
+        glRenderer.release()
+        isReleased = true
+    }
+
+    override fun onFrameAvailable(surfaceTexture: SurfaceTexture) {
+        checkMainThread()
+        if (isReleased) {
+            return
+        }
+        surfaceTexture.updateTexImage()
+        surfaceTexture.getTransformMatrix(textureTransform)
+        for (entry in outputSurfaces.entries.iterator()) {
+            val surface = entry.value
+            val surfaceOutput = entry.key
+            glRenderer.setOutputSurface(surface)
+            surfaceOutput.updateTransformMatrix(surfaceTransform, textureTransform)
+            glRenderer.render(surfaceTexture.timestamp, surfaceTransform)
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
index 1bce16f..47fad34 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout-land/camera_controller_view.xml
@@ -51,6 +51,12 @@
                 android:layout_height="wrap_content"
                 android:textOff="@string/toggle_camera_front"
                 android:textOn="@string/toggle_camera_back" />
+            <ToggleButton
+                android:id="@+id/effect_toggle"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textOff="@string/toggle_effect_off"
+                android:textOn="@string/toggle_effect_on" />
         </LinearLayout>
 
         <LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
index 9b97b3f..e7680b2 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/layout/camera_controller_view.xml
@@ -48,6 +48,12 @@
             android:layout_height="wrap_content"
             android:textOff="@string/toggle_camera_front"
             android:textOn="@string/toggle_camera_back" />
+        <ToggleButton
+            android:id="@+id/effect_toggle"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:textOff="@string/toggle_effect_off"
+            android:textOn="@string/toggle_effect_on" />
     </LinearLayout>
 
     <LinearLayout
diff --git a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
index d4d6509..01ea7da 100644
--- a/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
+++ b/camera/integration-tests/viewtestapp/src/main/res/values/donottranslate-strings.xml
@@ -36,6 +36,8 @@
     <string name="toggle_analyzer_not_set">Analyzer not set</string>
     <string name="toggle_camera_front">Front</string>
     <string name="toggle_camera_back">Back</string>
+    <string name="toggle_effect_on">Effect On</string>
+    <string name="toggle_effect_off">Effect Off</string>
     <string name="btn_remove_or_add">Remove/Add</string>
     <string name="btn_shrink">Shrink</string>
     <string name="btn_switch">Switch</string>
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 6be5b38..ca257d7 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -984,4 +984,93 @@
             @Composable fun Text(s: String) {}
         """
     )
+
+    @Test
+    fun memoizeLambdaInsideFunctionReturningValue() = verifyComposeIrTransform(
+        """
+            import androidx.compose.runtime.Composable
+
+            @Composable
+            fun Test(foo: Foo): Int =
+              Consume { foo.value }
+        """,
+        """
+            @Composable
+            fun Test(foo: Foo, %composer: Composer?, %changed: Int): Int {
+              %composer.startReplaceableGroup(<>)
+              sourceInformation(%composer, "C(Test)<{>,<Consum...>:Test.kt")
+              if (isTraceInProgress()) {
+                traceEventStart(<>, %changed, -1, <>)
+              }
+              val tmp0 = Consume(remember(foo, {
+                {
+                  foo.value
+                }
+              }, %composer, 0b1110 and %changed), %composer, 0)
+              if (isTraceInProgress()) {
+                traceEventEnd()
+              }
+              %composer.endReplaceableGroup()
+              return tmp0
+            }
+
+        """.trimIndent(),
+        """
+            import androidx.compose.runtime.Composable
+            import androidx.compose.runtime.Stable
+
+            @Composable
+            fun Consume(block: () -> Int): Int = block()
+
+            @Stable
+            class Foo {
+                val value: Int = 0
+            }
+        """.trimIndent()
+    )
+
+    @Test
+    fun testComposableCaptureInDelegates() = verifyComposeIrTransform(
+        """
+            import androidx.compose.runtime.*
+
+            class Test(val value: Int) : Delegate by Impl({
+                value
+            })
+        """,
+        """
+            @StabilityInferred(parameters = 0)
+            class Test(val value: Int) : Delegate {
+              private val %%delegate_0: Impl = Impl(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
+                sourceInformation(%composer, "C:Test.kt")
+                if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+                  if (isTraceInProgress()) {
+                    traceEventStart(<>, %changed, -1, <>)
+                  }
+                  value
+                  if (isTraceInProgress()) {
+                    traceEventEnd()
+                  }
+                } else {
+                  %composer.skipToGroupEnd()
+                }
+              }
+              )
+              val content: Function2<Composer, Int, Unit>
+                get() {
+                  return <this>.%%delegate_0.content
+                }
+              static val %stable: Int = 0
+            }
+        """,
+        """
+            import androidx.compose.runtime.Composable
+
+            interface Delegate {
+                val content: @Composable () -> Unit
+            }
+
+            class Impl(override val content: @Composable () -> Unit) : Delegate
+        """
+    )
 }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
index 53460d2..c642dff 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/VersionChecker.kt
@@ -82,7 +82,7 @@
             7100 to "1.2.0-rc01",
             7101 to "1.2.0-rc02",
             7102 to "1.2.0-rc03",
-            7103 to "1.2.0-rc04",
+            7103 to "1.2.0",
             8000 to "1.3.0-alpha01",
             8100 to "1.3.0-alpha02",
         )
@@ -97,7 +97,7 @@
          * The maven version string of this compiler. This string should be updated before/after every
          * release.
          */
-        const val compilerVersion: String = "1.3.0-beta01"
+        const val compilerVersion: String = "1.3.0-rc01"
         private val minimumRuntimeVersion: String
             get() = versionTable[minimumRuntimeVersionInt] ?: "unknown"
     }
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index f47eedb..c3410b9 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -259,15 +259,16 @@
     override fun recordCapture(local: IrValueDeclaration?): Boolean {
         val isThis = local == thisParam
         val isCtorParam = (local?.parent as? IrConstructor)?.parent === declaration
-        if (local != null && collectors.isNotEmpty() && isThis) {
+        val isClassParam = isThis || isCtorParam
+        if (local != null && collectors.isNotEmpty() && isClassParam) {
             for (collector in collectors) {
                 collector.recordCapture(local)
             }
         }
-        if (local != null && declaration.isLocal && !isThis && !isCtorParam) {
+        if (local != null && declaration.isLocal && !isClassParam) {
             captures.add(local)
         }
-        return isThis || isCtorParam
+        return isClassParam
     }
     override fun recordCapture(local: IrSymbolOwner?) { }
     override fun pushCollector(collector: CaptureCollector) {
@@ -415,10 +416,7 @@
         val composable = declaration.allowsComposableCalls
         val canRemember = composable &&
             // Don't use remember in an inline function
-            !descriptor.isInline &&
-            // Don't use remember if in a composable that returns a value
-            // TODO(b/150390108): Consider allowing remember in effects
-            descriptor.returnType.let { it != null && it.isUnit() }
+            !descriptor.isInline
 
         val context = FunctionContext(declaration, composable, canRemember)
         declarationContextStack.push(context)
diff --git a/compose/foundation/foundation/api/current.ignore b/compose/foundation/foundation/api/current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/current.ignore
+++ b/compose/foundation/foundation/api/current.ignore
@@ -1,7 +1,5 @@
 // Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
-    Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
-    Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
-    Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+    Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+    Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -36,6 +36,7 @@
   }
 
   public final class CheckScrollableContainerConstraintsKt {
+    method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class ClickableKt {
@@ -47,6 +48,7 @@
   }
 
   public final class ClipScrollableContainerKt {
+    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class DarkThemeKt {
@@ -231,6 +233,7 @@
 
   public final class ScrollableDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+    method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
     field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
   }
 
@@ -446,7 +449,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -581,7 +584,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
   public final class IntervalListKt {
   }
 
+  public final class LazyLayoutItemProviderKt {
+  }
+
   public final class LazyLayoutKt {
   }
 
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index 27223c7..091d626 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -37,7 +37,7 @@
   }
 
   public final class CheckScrollableContainerConstraintsKt {
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
+    method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class ClickableKt {
@@ -51,7 +51,7 @@
   }
 
   public final class ClipScrollableContainerKt {
-    method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
+    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class DarkThemeKt {
@@ -285,7 +285,7 @@
   public final class ScrollableDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public androidx.compose.foundation.OverscrollEffect overscrollEffect();
-    method @androidx.compose.foundation.ExperimentalFoundationApi public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
+    method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
     field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
   }
 
@@ -503,7 +503,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -639,7 +639,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -731,7 +731,7 @@
 package androidx.compose.foundation.lazy.layout {
 
   @androidx.compose.foundation.ExperimentalFoundationApi public sealed interface IntervalList<T> {
-    method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+    method public void forEach(optional int fromIndex, optional int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
     method public operator androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
     method public int getSize();
     property public abstract int size;
@@ -749,6 +749,13 @@
   public final class IntervalListKt {
   }
 
+  @androidx.compose.foundation.ExperimentalFoundationApi public interface LazyLayoutIntervalContent {
+    method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? getKey();
+    method public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> getType();
+    property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object>? key;
+    property public default kotlin.jvm.functions.Function1<java.lang.Integer,java.lang.Object> type;
+  }
+
   @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Stable public interface LazyLayoutItemProvider {
     method @androidx.compose.runtime.Composable public void Item(int index);
     method public default Object? getContentType(int index);
@@ -759,6 +766,12 @@
     property public default java.util.Map<java.lang.Object,java.lang.Integer> keyToIndexMap;
   }
 
+  public final class LazyLayoutItemProviderKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider DelegatingLazyLayoutItemProvider(androidx.compose.runtime.State<? extends androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider> delegate);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static <T extends androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent> androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider LazyLayoutItemProvider(androidx.compose.foundation.lazy.layout.IntervalList<? extends T> intervals, kotlin.ranges.IntRange nearestItemsRange, kotlin.jvm.functions.Function2<? super T,? super java.lang.Integer,kotlin.Unit> itemContent);
+    method @androidx.compose.foundation.ExperimentalFoundationApi public static Object! getDefaultLazyLayoutKey(int index);
+  }
+
   public final class LazyLayoutKt {
     method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static void LazyLayout(androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider itemProvider, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState? prefetchState, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.lazy.layout.LazyLayoutMeasureScope,? super androidx.compose.ui.unit.Constraints,? extends androidx.compose.ui.layout.MeasureResult> measurePolicy);
   }
@@ -788,6 +801,7 @@
   }
 
   public final class LazyNearestItemsRangeKt {
+    method @androidx.compose.foundation.ExperimentalFoundationApi @androidx.compose.runtime.Composable public static androidx.compose.runtime.State<kotlin.ranges.IntRange> rememberLazyNearestItemsRangeState(kotlin.jvm.functions.Function0<java.lang.Integer> firstVisibleItemIndex, kotlin.jvm.functions.Function0<java.lang.Integer> slidingWindowSize, kotlin.jvm.functions.Function0<java.lang.Integer> extraItemCount);
   }
 
   public final class Lazy_androidKt {
@@ -797,7 +811,7 @@
   @androidx.compose.foundation.ExperimentalFoundationApi public final class MutableIntervalList<T> implements androidx.compose.foundation.lazy.layout.IntervalList<T> {
     ctor public MutableIntervalList();
     method public void addInterval(int size, T? value);
-    method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<T>,kotlin.Unit> block);
+    method public void forEach(int fromIndex, int toIndex, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.lazy.layout.IntervalList.Interval<? extends T>,kotlin.Unit> block);
     method public androidx.compose.foundation.lazy.layout.IntervalList.Interval<T> get(int index);
     method public int getSize();
     property public int size;
diff --git a/compose/foundation/foundation/api/restricted_current.ignore b/compose/foundation/foundation/api/restricted_current.ignore
index 358fd62..a0279de 100644
--- a/compose/foundation/foundation/api/restricted_current.ignore
+++ b/compose/foundation/foundation/api/restricted_current.ignore
@@ -1,7 +1,5 @@
 // Baseline format: 1.0
-RemovedClass: androidx.compose.foundation.gestures.AndroidOverScrollKt:
-    Removed class androidx.compose.foundation.gestures.AndroidOverScrollKt
-RemovedClass: androidx.compose.foundation.gestures.OverScrollConfigurationKt:
-    Removed class androidx.compose.foundation.gestures.OverScrollConfigurationKt
-RemovedClass: androidx.compose.foundation.lazy.LazyGridDeprecatedKt:
-    Removed class androidx.compose.foundation.lazy.LazyGridDeprecatedKt
+RemovedClass: androidx.compose.foundation.lazy.LazyListItemProviderImplKt:
+    Removed class androidx.compose.foundation.lazy.LazyListItemProviderImplKt
+RemovedClass: androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt:
+    Removed class androidx.compose.foundation.lazy.grid.LazyGridItemProviderImplKt
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index b57caaa..0ea30cf 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -36,6 +36,7 @@
   }
 
   public final class CheckScrollableContainerConstraintsKt {
+    method public static void checkScrollableContainerConstraints(long constraints, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class ClickableKt {
@@ -47,6 +48,7 @@
   }
 
   public final class ClipScrollableContainerKt {
+    method public static androidx.compose.ui.Modifier clipScrollableContainer(androidx.compose.ui.Modifier, androidx.compose.foundation.gestures.Orientation orientation);
   }
 
   public final class DarkThemeKt {
@@ -231,6 +233,7 @@
 
   public final class ScrollableDefaults {
     method @androidx.compose.runtime.Composable public androidx.compose.foundation.gestures.FlingBehavior flingBehavior();
+    method public boolean reverseDirection(androidx.compose.ui.unit.LayoutDirection layoutDirection, androidx.compose.foundation.gestures.Orientation orientation, boolean reverseScrolling);
     field public static final androidx.compose.foundation.gestures.ScrollableDefaults INSTANCE;
   }
 
@@ -446,7 +449,7 @@
   public final class LazyListItemPlacementAnimatorKt {
   }
 
-  public final class LazyListItemProviderImplKt {
+  public final class LazyListItemProviderKt {
   }
 
   public final class LazyListKt {
@@ -581,7 +584,7 @@
   public final class LazyGridItemPlacementAnimatorKt {
   }
 
-  public final class LazyGridItemProviderImplKt {
+  public final class LazyGridItemProviderKt {
   }
 
   @androidx.compose.foundation.lazy.grid.LazyGridScopeMarker @androidx.compose.runtime.Stable public sealed interface LazyGridItemScope {
@@ -674,6 +677,9 @@
   public final class IntervalListKt {
   }
 
+  public final class LazyLayoutItemProviderKt {
+  }
+
   public final class LazyLayoutKt {
   }
 
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
index 0c76fe5..0750a5c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/CheckScrollableContainerConstraints.kt
@@ -27,7 +27,6 @@
  * @param constraints [Constraints] used to measure the scrollable container
  * @param orientation orientation of the scrolling
  */
-@ExperimentalFoundationApi
 fun checkScrollableContainerConstraints(
     constraints: Constraints,
     orientation: Orientation
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
index 8add015..b779125 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ClipScrollableContainer.kt
@@ -33,7 +33,6 @@
  *
  * @param orientation orientation of the scrolling
  */
-@ExperimentalFoundationApi
 fun Modifier.clipScrollableContainer(orientation: Orientation) =
     then(
         if (orientation == Orientation.Vertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
index 3428f11..8b10b1a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Scroll.kt
@@ -321,7 +321,6 @@
     val isVertical: Boolean,
     val overscrollEffect: OverscrollEffect
 ) : LayoutModifier {
-    @OptIn(ExperimentalFoundationApi::class)
     override fun MeasureScope.measure(
         measurable: Measurable,
         constraints: Constraints
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
index c882690..02e20cc 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt
@@ -224,7 +224,6 @@
      *
      * @return `true` if scroll direction should be reversed, `false` otherwise.
      */
-    @ExperimentalFoundationApi
     fun reverseDirection(
         layoutDirection: LayoutDirection,
         orientation: Orientation,
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
index b918123..de9fa2e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt
@@ -74,7 +74,7 @@
     content: LazyListScope.() -> Unit
 ) {
     val overscrollEffect = ScrollableDefaults.overscrollEffect()
-    val itemProvider = rememberItemProvider(state, content)
+    val itemProvider = rememberLazyListItemProvider(state, content)
     val beyondBoundsInfo = remember { LazyListBeyondBoundsInfo() }
     val scope = rememberCoroutineScope()
     val placementAnimator = remember(state, isVertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index 75d380a..8c28355 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -17,7 +17,14 @@
 package androidx.compose.foundation.lazy
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 
 @ExperimentalFoundationApi
 internal interface LazyListItemProvider : LazyLayoutItemProvider {
@@ -26,3 +33,59 @@
     /** The scope used by the item content lambdas */
     val itemScope: LazyItemScopeImpl
 }
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyListItemProvider(
+    state: LazyListState,
+    content: LazyListScope.() -> Unit
+): LazyListItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+        firstVisibleItemIndex = remember(state) { { state.firstVisibleItemIndex } },
+        slidingWindowSize = { NearestItemsSlidingWindowSize },
+        extraItemCount = { NearestItemsExtraItemCount }
+    )
+
+    return remember(nearestItemsRangeState) {
+        val itemProviderState = derivedStateOf {
+            val listScope = LazyListScopeImpl().apply(latestContent.value)
+            LazyListItemProviderImpl(
+                listScope.intervals,
+                nearestItemsRangeState.value,
+                listScope.headerIndexes,
+            )
+        }
+        object : LazyListItemProvider,
+            LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+            override val headerIndexes: List<Int> get() = itemProviderState.value.headerIndexes
+            override val itemScope: LazyItemScopeImpl get() = itemProviderState.value.itemScope
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+private class LazyListItemProviderImpl(
+    intervals: IntervalList<LazyListIntervalContent>,
+    nearestItemsRange: IntRange,
+    override val headerIndexes: List<Int>,
+    override val itemScope: LazyItemScopeImpl = LazyItemScopeImpl()
+) : LazyListItemProvider,
+    LazyLayoutItemProvider by LazyLayoutItemProvider(
+        intervals = intervals,
+        nearestItemsRange = nearestItemsRange,
+        itemContent = { interval: LazyListIntervalContent, index: Int ->
+            interval.item.invoke(itemScope, index)
+        }
+    )
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 30
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 100
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
deleted file mode 100644
index 2229a03..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProviderImpl.kt
+++ /dev/null
@@ -1,159 +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.foundation.lazy
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
-
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
-    state: LazyListState,
-    content: LazyListScope.() -> Unit
-): LazyListItemProvider {
-    val latestContent = rememberUpdatedState(content)
-
-    val nearestItemsRangeState = remember(state) {
-        derivedStateOf(structuralEqualityPolicy()) {
-            calculateNearestItemsRange(
-                slidingWindowSize = NearestItemsSlidingWindowSize,
-                extraItemCount = NearestItemsExtraItemCount,
-                firstVisibleItem = state.firstVisibleItemIndex
-            )
-        }
-    }
-
-    return remember(nearestItemsRangeState) {
-        LazyListItemProviderImpl(
-            derivedStateOf {
-                val listScope = LazyListScopeImpl().apply(latestContent.value)
-                LazyListItemsSnapshot(
-                    listScope.intervals,
-                    listScope.headerIndexes,
-                    nearestItemsRangeState.value
-                )
-            }
-        )
-    }
-}
-
-@ExperimentalFoundationApi
-internal class LazyListItemsSnapshot(
-    private val intervals: IntervalList<LazyListIntervalContent>,
-    val headerIndexes: List<Int>,
-    nearestItemsRange: IntRange
-) {
-    val itemsCount get() = intervals.size
-
-    fun getKey(index: Int): Any {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        val key = interval.value.key?.invoke(localIntervalIndex)
-        return key ?: getDefaultLazyLayoutKey(index)
-    }
-
-    @Composable
-    fun Item(scope: LazyItemScope, index: Int) {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        interval.value.item.invoke(scope, localIntervalIndex)
-    }
-
-    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
-    fun getContentType(index: Int): Any? {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.type.invoke(localIntervalIndex)
-    }
-}
-
-@ExperimentalFoundationApi
-internal class LazyListItemProviderImpl(
-    private val itemsSnapshot: State<LazyListItemsSnapshot>
-) : LazyListItemProvider {
-
-    override val itemScope = LazyItemScopeImpl()
-
-    override val headerIndexes: List<Int> get() = itemsSnapshot.value.headerIndexes
-
-     override val itemCount get() = itemsSnapshot.value.itemsCount
-
-    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
-    @Composable
-    override fun Item(index: Int) {
-        itemsSnapshot.value.Item(itemScope, index)
-    }
-
-    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
-    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
-    range: IntRange,
-    list: IntervalList<LazyListIntervalContent>
-): Map<Any, Int> {
-    val first = range.first
-    check(first >= 0)
-    val last = minOf(range.last, list.size - 1)
-    return if (last < first) {
-        emptyMap()
-    } else {
-        hashMapOf<Any, Int>().also { map ->
-            list.forEach(
-                fromIndex = first,
-                toIndex = last,
-            ) {
-                if (it.value.key != null) {
-                    val keyFactory = requireNotNull(it.value.key)
-                    val start = maxOf(first, it.startIndex)
-                    val end = minOf(last, it.startIndex + it.size - 1)
-                    for (i in start..end) {
-                        map[keyFactory(i - it.startIndex)] = i
-                    }
-                }
-            }
-        }
-    }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 30
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private const val NearestItemsExtraItemCount = 100
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
index 4ea8125..ef1c2f2 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListScopeImpl.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.lazy.layout.IntervalList
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
 import androidx.compose.foundation.lazy.layout.MutableIntervalList
 import androidx.compose.runtime.Composable
 
@@ -72,8 +73,9 @@
     }
 }
 
+@OptIn(ExperimentalFoundationApi::class)
 internal class LazyListIntervalContent(
-    val key: ((index: Int) -> Any)?,
-    val type: ((index: Int) -> Any?),
+    override val key: ((index: Int) -> Any)?,
+    override val type: ((index: Int) -> Any?),
     val item: @Composable LazyItemScope.(index: Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
index d785a83..719d441 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazySemantics.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
         userScrollEnabled
     ) {
         val indexForKeyMapping: (Any) -> Int = { needle ->
-            val key = itemProvider::getKey
             var result = -1
             for (index in 0 until itemProvider.itemCount) {
-                if (key(index) == needle) {
+                if (itemProvider.getKey(index) == needle) {
                     result = index
                     break
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
index 16f698d..a114e0a 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt
@@ -75,7 +75,7 @@
 ) {
     val overscrollEffect = ScrollableDefaults.overscrollEffect()
 
-    val itemProvider = rememberItemProvider(state, content)
+    val itemProvider = rememberLazyGridItemProvider(state, content)
 
     val scope = rememberCoroutineScope()
     val placementAnimator = remember(state, isVertical) {
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
index df4d695..4b27068 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProvider.kt
@@ -17,9 +17,94 @@
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.DelegatingLazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.IntervalList
 import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider
+import androidx.compose.foundation.lazy.layout.rememberLazyNearestItemsRangeState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberUpdatedState
 
 @ExperimentalFoundationApi
 internal interface LazyGridItemProvider : LazyLayoutItemProvider {
     val spanLayoutProvider: LazyGridSpanLayoutProvider
+    val hasCustomSpans: Boolean
+
+    fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan
 }
+
+@ExperimentalFoundationApi
+@Composable
+internal fun rememberLazyGridItemProvider(
+    state: LazyGridState,
+    content: LazyGridScope.() -> Unit,
+): LazyGridItemProvider {
+    val latestContent = rememberUpdatedState(content)
+    val nearestItemsRangeState = rememberLazyNearestItemsRangeState(
+        firstVisibleItemIndex = remember(state) {
+            { state.firstVisibleItemIndex }
+        },
+        slidingWindowSize = { NearestItemsSlidingWindowSize },
+        extraItemCount = { NearestItemsExtraItemCount }
+    )
+
+    return remember(nearestItemsRangeState) {
+        val itemProviderState: State<LazyGridItemProvider> = derivedStateOf {
+            val gridScope = LazyGridScopeImpl().apply(latestContent.value)
+            LazyGridItemProviderImpl(
+                gridScope.intervals,
+                gridScope.hasCustomSpans,
+                nearestItemsRangeState.value
+            )
+        }
+
+        object : LazyGridItemProvider,
+            LazyLayoutItemProvider by DelegatingLazyLayoutItemProvider(itemProviderState) {
+            override val spanLayoutProvider: LazyGridSpanLayoutProvider
+                get() = itemProviderState.value.spanLayoutProvider
+
+            override val hasCustomSpans: Boolean
+                get() = itemProviderState.value.hasCustomSpans
+
+            override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan =
+                with(itemProviderState.value) {
+                    getSpan(index)
+                }
+        }
+    }
+}
+
+@ExperimentalFoundationApi
+private class LazyGridItemProviderImpl(
+    private val intervals: IntervalList<LazyGridIntervalContent>,
+    override val hasCustomSpans: Boolean,
+    nearestItemsRange: IntRange
+) : LazyGridItemProvider, LazyLayoutItemProvider by LazyLayoutItemProvider(
+    intervals = intervals,
+    nearestItemsRange = nearestItemsRange,
+    itemContent = { interval, index ->
+        interval.item.invoke(LazyGridItemScopeImpl, index)
+    }
+) {
+    override val spanLayoutProvider: LazyGridSpanLayoutProvider =
+        LazyGridSpanLayoutProvider(this)
+
+    override fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return interval.value.span.invoke(this, localIntervalIndex)
+    }
+}
+
+/**
+ * We use the idea of sliding window as an optimization, so user can scroll up to this number of
+ * items until we have to regenerate the key to index map.
+ */
+private const val NearestItemsSlidingWindowSize = 90
+
+/**
+ * The minimum amount of items near the current first visible item we want to have mapping for.
+ */
+private const val NearestItemsExtraItemCount = 200
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
deleted file mode 100644
index 9c2d01e..0000000
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridItemProviderImpl.kt
+++ /dev/null
@@ -1,164 +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.foundation.lazy.grid
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.lazy.layout.IntervalList
-import androidx.compose.foundation.lazy.layout.calculateNearestItemsRange
-import androidx.compose.foundation.lazy.layout.getDefaultLazyLayoutKey
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.derivedStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberUpdatedState
-import androidx.compose.runtime.structuralEqualityPolicy
-
-@ExperimentalFoundationApi
-@Composable
-internal fun rememberItemProvider(
-    state: LazyGridState,
-    content: LazyGridScope.() -> Unit,
-): LazyGridItemProvider {
-    val latestContent = rememberUpdatedState(content)
-    val nearestItemsRangeState = remember(state) {
-        derivedStateOf(structuralEqualityPolicy()) {
-            calculateNearestItemsRange(
-                slidingWindowSize = NearestItemsSlidingWindowSize,
-                extraItemCount = NearestItemsExtraItemCount,
-                firstVisibleItem = state.firstVisibleItemIndex
-            )
-        }
-    }
-
-    return remember(nearestItemsRangeState) {
-        LazyGridItemProviderImpl(
-            derivedStateOf {
-                val listScope = LazyGridScopeImpl().apply(latestContent.value)
-                LazyGridItemsSnapshot(
-                    listScope.intervals,
-                    listScope.hasCustomSpans,
-                    nearestItemsRangeState.value
-                )
-            }
-        )
-    }
-}
-
-@ExperimentalFoundationApi
-internal class LazyGridItemsSnapshot(
-    private val intervals: IntervalList<LazyGridIntervalContent>,
-    val hasCustomSpans: Boolean,
-    nearestItemsRange: IntRange
-) {
-    val itemsCount get() = intervals.size
-
-    val spanLayoutProvider = LazyGridSpanLayoutProvider(this)
-
-    fun getKey(index: Int): Any {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        val key = interval.value.key?.invoke(localIntervalIndex)
-        return key ?: getDefaultLazyLayoutKey(index)
-    }
-
-    fun LazyGridItemSpanScope.getSpan(index: Int): GridItemSpan {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.span.invoke(this, localIntervalIndex)
-    }
-
-    @Composable
-    fun Item(index: Int) {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        interval.value.item.invoke(LazyGridItemScopeImpl, localIntervalIndex)
-    }
-
-    val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
-
-    fun getContentType(index: Int): Any? {
-        val interval = intervals[index]
-        val localIntervalIndex = index - interval.startIndex
-        return interval.value.type.invoke(localIntervalIndex)
-    }
-}
-
-@ExperimentalFoundationApi
-internal class LazyGridItemProviderImpl(
-    private val itemsSnapshot: State<LazyGridItemsSnapshot>
-) : LazyGridItemProvider {
-    override val itemCount get() = itemsSnapshot.value.itemsCount
-
-    override fun getKey(index: Int) = itemsSnapshot.value.getKey(index)
-
-    @Composable
-    override fun Item(index: Int) {
-        itemsSnapshot.value.Item(index)
-    }
-
-    override val keyToIndexMap: Map<Any, Int> get() = itemsSnapshot.value.keyToIndexMap
-
-    override fun getContentType(index: Int) = itemsSnapshot.value.getContentType(index)
-
-    override val spanLayoutProvider: LazyGridSpanLayoutProvider
-        get() = itemsSnapshot.value.spanLayoutProvider
-}
-
-/**
- * Traverses the interval [list] in order to create a mapping from the key to the index for all
- * the indexes in the passed [range].
- * The returned map will not contain the values for intervals with no key mapping provided.
- */
-@ExperimentalFoundationApi
-internal fun generateKeyToIndexMap(
-    range: IntRange,
-    list: IntervalList<LazyGridIntervalContent>
-): Map<Any, Int> {
-    val first = range.first
-    check(first >= 0)
-    val last = minOf(range.last, list.size - 1)
-    return if (last < first) {
-        emptyMap()
-    } else {
-        hashMapOf<Any, Int>().also { map ->
-            list.forEach(
-                fromIndex = first,
-                toIndex = last,
-            ) {
-                if (it.value.key != null) {
-                    val keyFactory = requireNotNull(it.value.key)
-                    val start = maxOf(first, it.startIndex)
-                    val end = minOf(last, it.startIndex + it.size - 1)
-                    for (i in start..end) {
-                        map[keyFactory(i - it.startIndex)] = i
-                    }
-                }
-            }
-        }
-    }
-}
-
-/**
- * We use the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- */
-private const val NearestItemsSlidingWindowSize = 90
-
-/**
- * The minimum amount of items near the current first visible item we want to have mapping for.
- */
-private const val NearestItemsExtraItemCount = 200
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
index 281996a..e05ff92 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridScopeImpl.kt
@@ -17,6 +17,7 @@
 package androidx.compose.foundation.lazy.grid
 
 import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.lazy.layout.LazyLayoutIntervalContent
 import androidx.compose.foundation.lazy.layout.MutableIntervalList
 import androidx.compose.runtime.Composable
 
@@ -67,8 +68,8 @@
 
 @OptIn(ExperimentalFoundationApi::class)
 internal class LazyGridIntervalContent(
-    val key: ((index: Int) -> Any)?,
+    override val key: ((index: Int) -> Any)?,
     val span: LazyGridItemSpanScope.(Int) -> GridItemSpan,
-    val type: ((index: Int) -> Any?),
+    override val type: ((index: Int) -> Any?),
     val item: @Composable LazyGridItemScope.(Int) -> Unit
-)
+) : LazyLayoutIntervalContent
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
index 18b32e7..6c0f594 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridSpanLayoutProvider.kt
@@ -21,7 +21,7 @@
 import kotlin.math.sqrt
 
 @OptIn(ExperimentalFoundationApi::class)
-internal class LazyGridSpanLayoutProvider(private val itemsSnapshot: LazyGridItemsSnapshot) {
+internal class LazyGridSpanLayoutProvider(private val itemProvider: LazyGridItemProvider) {
     class LineConfiguration(val firstItemIndex: Int, val spans: List<GridItemSpan>)
 
     /** Caches the bucket info on lines 0, [bucketSize], 2 * [bucketSize], etc. */
@@ -60,7 +60,7 @@
             List(currentSlotsPerLine) { GridItemSpan(1) }.also { previousDefaultSpans = it }
         }
 
-    val totalSize get() = itemsSnapshot.itemsCount
+    val totalSize get() = itemProvider.itemCount
 
     /** The number of slots on one grid line e.g. the number of columns of a vertical grid. */
     var slotsPerLine = 0
@@ -72,7 +72,7 @@
         }
 
     fun getLineConfiguration(lineIndex: Int): LineConfiguration {
-        if (!itemsSnapshot.hasCustomSpans) {
+        if (!itemProvider.hasCustomSpans) {
             // Quick return when all spans are 1x1 - in this case we can easily calculate positions.
             val firstItemIndex = lineIndex * slotsPerLine
             return LineConfiguration(
@@ -172,7 +172,7 @@
             return LineIndex(0)
         }
         require(itemIndex < totalSize)
-        if (!itemsSnapshot.hasCustomSpans) {
+        if (!itemProvider.hasCustomSpans) {
             return LineIndex(itemIndex / slotsPerLine)
         }
 
@@ -210,7 +210,7 @@
         return LineIndex(currentLine)
     }
 
-    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemsSnapshot) {
+    private fun spanOf(itemIndex: Int, maxSpan: Int) = with(itemProvider) {
         with(LazyGridItemSpanScopeImpl) {
             maxCurrentLineSpan = maxSpan
             maxLineSpan = slotsPerLine
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
index 54cafcd..b331a9e 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazySemantics.kt
@@ -20,6 +20,7 @@
 import androidx.compose.foundation.gestures.ScrollableState
 import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
 import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.semantics.CollectionInfo
@@ -53,10 +54,9 @@
         userScrollEnabled
     ) {
         val indexForKeyMapping: (Any) -> Int = { needle ->
-            val key = itemProvider::getKey
             var result = -1
             for (index in 0 until itemProvider.itemCount) {
-                if (key(index) == needle) {
+                if (itemProvider.getKey(index) == needle) {
                     result = index
                     break
                 }
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
index f855bea..0a6ca89 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/IntervalList.kt
@@ -31,7 +31,7 @@
  * @param T type of values each interval contains in [Interval.value].
  */
 @ExperimentalFoundationApi
-sealed interface IntervalList<T> {
+sealed interface IntervalList<out T> {
 
     /**
      * The total amount of items in all the intervals.
@@ -69,7 +69,7 @@
      *
      * @see get
      */
-    class Interval<T> internal constructor(
+    class Interval<out T> internal constructor(
         /**
          * The index of the first item in the interval.
          */
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
index 9534b0e..828bdfe 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutItemProvider.kt
@@ -19,6 +19,7 @@
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
+import androidx.compose.runtime.State
 
 /**
  * Provides all the needed info about the items which could be later composed and displayed as
@@ -67,4 +68,138 @@
  * 3) This objects can't be equals to any object which could be provided by a user as a custom key.
  */
 @ExperimentalFoundationApi
+@Suppress("MissingNullability")
 expect fun getDefaultLazyLayoutKey(index: Int): Any
+
+/**
+ * Common content holder to back interval-based `item` DSL of lazy layouts.
+ */
+@ExperimentalFoundationApi
+interface LazyLayoutIntervalContent {
+    /**
+     * Returns item key based on a local index for the current interval.
+     */
+    val key: ((index: Int) -> Any)? get() = null
+
+    /**
+     * Returns item type based on a local index for the current interval.
+     */
+    val type: ((index: Int) -> Any?) get() = { null }
+}
+
+/**
+ * Default implementation of [LazyLayoutItemProvider] shared by lazy layout implementations.
+ *
+ * @param intervals [IntervalList] of [LazyLayoutIntervalContent] defined by lazy list DSL
+ * @param nearestItemsRange range of indices considered near current viewport
+ * @param itemContent composable content based on index inside provided interval
+ */
+@ExperimentalFoundationApi
+fun <T : LazyLayoutIntervalContent> LazyLayoutItemProvider(
+    intervals: IntervalList<T>,
+    nearestItemsRange: IntRange,
+    itemContent: @Composable (interval: T, index: Int) -> Unit,
+): LazyLayoutItemProvider =
+    DefaultLazyLayoutItemsProvider(itemContent, intervals, nearestItemsRange)
+
+@ExperimentalFoundationApi
+private class DefaultLazyLayoutItemsProvider<IntervalContent : LazyLayoutIntervalContent>(
+    val itemContentProvider: @Composable IntervalContent.(index: Int) -> Unit,
+    val intervals: IntervalList<IntervalContent>,
+    nearestItemsRange: IntRange
+) : LazyLayoutItemProvider {
+    override val itemCount get() = intervals.size
+
+    override val keyToIndexMap: Map<Any, Int> = generateKeyToIndexMap(nearestItemsRange, intervals)
+
+    @Composable
+    override fun Item(index: Int) {
+        withLocalIntervalIndex(index) { localIndex, content ->
+            content.itemContentProvider(localIndex)
+        }
+    }
+
+    override fun getKey(index: Int): Any =
+        withLocalIntervalIndex(index) { localIndex, content ->
+            content.key?.invoke(localIndex) ?: getDefaultLazyLayoutKey(index)
+        }
+
+    override fun getContentType(index: Int): Any? =
+        withLocalIntervalIndex(index) { localIndex, content ->
+            content.type.invoke(localIndex)
+        }
+
+    private inline fun <T> withLocalIntervalIndex(
+        index: Int,
+        block: (localIndex: Int, content: IntervalContent) -> T
+    ): T {
+        val interval = intervals[index]
+        val localIntervalIndex = index - interval.startIndex
+        return block(localIntervalIndex, interval.value)
+    }
+
+    /**
+     * Traverses the interval [list] in order to create a mapping from the key to the index for all
+     * the indexes in the passed [range].
+     * The returned map will not contain the values for intervals with no key mapping provided.
+     */
+    @ExperimentalFoundationApi
+    private fun generateKeyToIndexMap(
+        range: IntRange,
+        list: IntervalList<LazyLayoutIntervalContent>
+    ): Map<Any, Int> {
+        val first = range.first
+        check(first >= 0)
+        val last = minOf(range.last, list.size - 1)
+        return if (last < first) {
+            emptyMap()
+        } else {
+            hashMapOf<Any, Int>().also { map ->
+                list.forEach(
+                    fromIndex = first,
+                    toIndex = last,
+                ) {
+                    if (it.value.key != null) {
+                        val keyFactory = requireNotNull(it.value.key)
+                        val start = maxOf(first, it.startIndex)
+                        val end = minOf(last, it.startIndex + it.size - 1)
+                        for (i in start..end) {
+                            map[keyFactory(i - it.startIndex)] = i
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+/**
+ * Delegating version of [LazyLayoutItemProvider], abstracting internal [State] access.
+ * This way, passing [LazyLayoutItemProvider] will not trigger recomposition unless
+ * its methods are called within composable functions.
+ *
+ * @param delegate [State] to delegate [LazyLayoutItemProvider] functionality to.
+ */
+@ExperimentalFoundationApi
+fun DelegatingLazyLayoutItemProvider(
+    delegate: State<LazyLayoutItemProvider>
+): LazyLayoutItemProvider =
+    DefaultDelegatingLazyLayoutItemProvider(delegate)
+
+@ExperimentalFoundationApi
+private class DefaultDelegatingLazyLayoutItemProvider(
+    private val delegate: State<LazyLayoutItemProvider>
+) : LazyLayoutItemProvider {
+    override val itemCount: Int get() = delegate.value.itemCount
+
+    @Composable
+    override fun Item(index: Int) {
+        delegate.value.Item(index)
+    }
+
+    override val keyToIndexMap: Map<Any, Int> get() = delegate.value.keyToIndexMap
+
+    override fun getKey(index: Int): Any = delegate.value.getKey(index)
+
+    override fun getContentType(index: Int): Any? = delegate.value.getContentType(index)
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
index f34ae9f..c602a6c 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyNearestItemsRange.kt
@@ -16,19 +16,48 @@
 
 package androidx.compose.foundation.lazy.layout
 
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.structuralEqualityPolicy
+
+/**
+ * Calculate and memoize range of indexes which contains at least [extraItemCount] items near
+ * the first visible item. It is optimized to return the same range for small changes in the
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
+ *
+ * @param firstVisibleItemIndex Provider of the first item index currently visible on screen.
+ * @param slidingWindowSize Number items user can scroll up to this number of items until we have to
+ * regenerate item mapping.
+ * @param extraItemCount  The minimum amount of items near the first visible item we want
+ * to have mapping for.
+ * @return range of indexes with items near current the first visible position.
+ */
+@ExperimentalFoundationApi
+@Composable
+fun rememberLazyNearestItemsRangeState(
+    firstVisibleItemIndex: () -> Int,
+    slidingWindowSize: () -> Int,
+    extraItemCount: () -> Int
+): State<IntRange> =
+    remember(firstVisibleItemIndex, slidingWindowSize, extraItemCount) {
+        derivedStateOf(structuralEqualityPolicy()) {
+            calculateNearestItemsRange(
+                firstVisibleItemIndex(),
+                slidingWindowSize(),
+                extraItemCount()
+            )
+        }
+    }
+
 /**
  * Returns a range of indexes which contains at least [extraItemCount] items near
  * the first visible item. It is optimized to return the same range for small changes in the
- * [firstVisibleItem] value so we do not regenerate the map on each scroll.
- *
- * It uses the idea of sliding window as an optimization, so user can scroll up to this number of
- * items until we have to regenerate the key to index map.
- *
- * @param firstVisibleItem currently visible item
- * @param slidingWindowSize size of the sliding window for the nearest item calculation
- * @param extraItemCount minimum amount of items near the first item we want to have mapping for.
+ * firstVisibleItem value so we do not regenerate the map on each scroll.
  */
-internal fun calculateNearestItemsRange(
+private fun calculateNearestItemsRange(
     firstVisibleItem: Int,
     slidingWindowSize: Int,
     extraItemCount: Int
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 56e0e66..29a5f39 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -16,8 +16,10 @@
 
 package androidx.compose.foundation.text
 
+import androidx.compose.ui.text.AnnotatedString
 import com.google.common.truth.Truth.assertThat
 import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.whenever
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
@@ -27,8 +29,14 @@
 class TextControllerTest {
     @Test
     fun `semantics modifier recreated when TextDelegate is set`() {
-        val textDelegateBefore = mock<TextDelegate>()
-        val textDelegateAfter = mock<TextDelegate>()
+        val textDelegateBefore = mock<TextDelegate>() {
+            whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+        }
+
+        val textDelegateAfter = mock<TextDelegate>() {
+            whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+        }
+
         // Make sure that mock doesn't do smart memory management:
         assertThat(textDelegateAfter).isNotSameInstanceAs(textDelegateBefore)
 
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9d55522..05d810f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -23,6 +23,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.layout.LayoutCoordinates
 import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.TextLayoutInput
 import androidx.compose.ui.text.TextLayoutResult
 import androidx.compose.ui.text.TextStyle
@@ -68,6 +69,7 @@
     private lateinit var gesture: TextDragObserver
     private lateinit var layoutCoordinates: LayoutCoordinates
     private lateinit var state: TextState
+    private lateinit var fontFamilyResolver: FontFamily.Resolver
 
     @Before
     fun setup() {
@@ -76,8 +78,15 @@
         layoutCoordinates = mock {
             on { isAttached } doReturn true
         }
+        fontFamilyResolver = mock()
 
-        state = TextState(mock(), selectableId)
+        val delegate = TextDelegate(
+            text = AnnotatedString(""),
+            style = TextStyle(),
+            density = Density(1.0f),
+            fontFamilyResolver = fontFamilyResolver
+        )
+        state = TextState(delegate, selectableId)
         state.layoutCoordinates = layoutCoordinates
         state.layoutResult = TextLayoutResult(
             TextLayoutInput(
diff --git a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
index db0d20d..6c3d32d 100644
--- a/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
+++ b/compose/integration-tests/macrobenchmark/src/androidTest/java/androidx/compose/integration/macrobenchmark/TrivialTracingBenchmark.kt
@@ -23,29 +23,30 @@
 import androidx.benchmark.macro.TraceSectionMetric
 import androidx.benchmark.macro.junit4.MacrobenchmarkRule
 import androidx.benchmark.perfetto.PerfettoCapture
-import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.LargeTest
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameters
 
 @LargeTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
 /**
  * End-to-end test for compose-runtime-tracing verifying that names of Composables show up in
  * a Perfetto trace.
  */
-class TrivialTracingBenchmark {
+@OptIn(ExperimentalMetricApi::class)
+class TrivialTracingBenchmark(private val composableName: String) {
     @get:Rule
     val benchmarkRule = MacrobenchmarkRule()
 
     @RequiresApi(Build.VERSION_CODES.R) // TODO(234351579): Support API < 30
-    @OptIn(ExperimentalMetricApi::class)
     @Test
     fun test_composable_names_present_in_trace() {
-        val metrics = COMPOSABLE_NAMES.map { composableName ->
+        val metrics = listOf(
             TraceSectionMetric("%$PACKAGE_NAME.$composableName %$FILE_NAME:%")
-        }
+        )
         benchmarkRule.measureRepeated(
             packageName = PACKAGE_NAME,
             metrics = metrics,
@@ -74,5 +75,9 @@
             "Bar_4888EA32_ABC5_4550_BA78_1247FEC1AAC9",
             "Baz_609801AB_F5A9_47C3_9405_2E82542F21B8"
         )
+
+        @JvmStatic
+        @Parameters(name = "{0}")
+        fun parameters() = COMPOSABLE_NAMES
     }
 }
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
index 35f5b24..ac4def2 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/textfield/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
 package androidx.compose.material.textfield
 
 import android.os.Build
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredWidth
@@ -38,7 +37,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -82,15 +80,13 @@
     @Test
     fun outlinedTextField_withInput() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Text"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Text"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_withInput")
@@ -99,14 +95,12 @@
     @Test
     fun outlinedTextField_notFocused() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_not_focused")
@@ -115,14 +109,12 @@
     @Test
     fun outlinedTextField_focused() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -134,14 +126,12 @@
     fun outlinedTextField_focused_rtl() {
         rule.setMaterialContent {
             CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                    OutlinedTextField(
-                        value = "",
-                        onValueChange = {},
-                        label = { Text("Label") },
-                        modifier = Modifier.requiredWidth(280.dp)
-                    )
-                }
+                OutlinedTextField(
+                    value = "",
+                    onValueChange = {},
+                    label = { Text("Label") },
+                    modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+                )
             }
         }
 
@@ -153,16 +143,14 @@
     @Test
     fun outlinedTextField_error_focused() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Input"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    isError = true,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Input"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                isError = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -173,15 +161,13 @@
     @Test
     fun outlinedTextField_error_notFocused() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    isError = true,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                isError = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -374,15 +360,13 @@
     @Test
     fun outlinedTextField_disabled() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = TextFieldValue("Text"),
-                    onValueChange = {},
-                    singleLine = true,
-                    enabled = false,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = TextFieldValue("Text"),
+                onValueChange = {},
+                singleLine = true,
+                enabled = false,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlinedTextField_disabled")
@@ -391,15 +375,13 @@
     @Test
     fun outlinedTextField_disabled_notFocusable() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = TextFieldValue("Text"),
-                    onValueChange = {},
-                    singleLine = true,
-                    enabled = false,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = TextFieldValue("Text"),
+                onValueChange = {},
+                singleLine = true,
+                enabled = false,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -410,15 +392,13 @@
     @Test
     fun outlinedTextField_disabled_notScrolled() {
         rule.setMaterialContent {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = longText,
-                    onValueChange = { },
-                    singleLine = true,
-                    modifier = Modifier.requiredWidth(300.dp),
-                    enabled = false
-                )
-            }
+            OutlinedTextField(
+                value = longText,
+                onValueChange = { },
+                singleLine = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+                enabled = false
+            )
         }
 
         rule.mainClock.autoAdvance = false
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
index 197954f..0cbdd15 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/OutlinedTextField.kt
@@ -49,6 +49,7 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
@@ -160,7 +161,11 @@
     BasicTextField(
         value = value,
         modifier = if (label != null) {
-            modifier.padding(top = OutlinedTextFieldTopPadding)
+            modifier
+                // Merge semantics at the beginning of the modifier chain to ensure padding is
+                // considered part of the text field.
+                .semantics(mergeDescendants = true) {}
+                .padding(top = OutlinedTextFieldTopPadding)
         } else {
             modifier
         }
@@ -304,7 +309,11 @@
     BasicTextField(
         value = value,
         modifier = if (label != null) {
-            modifier.padding(top = OutlinedTextFieldTopPadding)
+            modifier
+                // Merge semantics at the beginning of the modifier chain to ensure padding is
+                // considered part of the text field.
+                .semantics(mergeDescendants = true) {}
+                .padding(top = OutlinedTextFieldTopPadding)
         } else {
             modifier
         }
diff --git a/compose/material3/material3/api/public_plus_experimental_current.txt b/compose/material3/material3/api/public_plus_experimental_current.txt
index fd3c767..e9d5bf1 100644
--- a/compose/material3/material3/api/public_plus_experimental_current.txt
+++ b/compose/material3/material3/api/public_plus_experimental_current.txt
@@ -210,9 +210,9 @@
   public final class ChipKt {
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void AssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedAssistChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedFilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void ElevatedSuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
-    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? selectedIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
+    method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void FilterChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void InputChip(boolean selected, kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? leadingIcon, optional kotlin.jvm.functions.Function0<kotlin.Unit>? avatar, optional kotlin.jvm.functions.Function0<kotlin.Unit>? trailingIcon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.SelectableChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.SelectableChipBorder? border, optional androidx.compose.material3.SelectableChipColors colors);
     method @androidx.compose.material3.ExperimentalMaterial3Api @androidx.compose.runtime.Composable public static void SuggestionChip(kotlin.jvm.functions.Function0<kotlin.Unit> onClick, kotlin.jvm.functions.Function0<kotlin.Unit> label, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional kotlin.jvm.functions.Function0<kotlin.Unit>? icon, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.material3.ChipElevation? elevation, optional androidx.compose.ui.graphics.Shape shape, optional androidx.compose.material3.ChipBorder? border, optional androidx.compose.material3.ChipColors colors);
   }
diff --git a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
index 22c5d76..2837fd9 100644
--- a/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
+++ b/compose/material3/material3/samples/src/main/java/androidx/compose/material3/samples/ChipSamples.kt
@@ -93,12 +93,16 @@
         selected = selected,
         onClick = { selected = !selected },
         label = { Text("Filter chip") },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            null
         }
     )
 }
@@ -112,12 +116,16 @@
         selected = selected,
         onClick = { selected = !selected },
         label = { Text("Filter chip") },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            null
         }
     )
 }
@@ -131,19 +139,22 @@
         selected = selected,
         onClick = { selected = !selected },
         label = { Text("Filter chip") },
-        leadingIcon = {
-            Icon(
-                imageVector = Icons.Filled.Home,
-                contentDescription = "Localized description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
-        },
-        selectedIcon = {
-            Icon(
-                imageVector = Icons.Filled.Done,
-                contentDescription = "Localized Description",
-                modifier = Modifier.size(FilterChipDefaults.IconSize)
-            )
+        leadingIcon = if (selected) {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Done,
+                    contentDescription = "Localized Description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
+        } else {
+            {
+                Icon(
+                    imageVector = Icons.Filled.Home,
+                    contentDescription = "Localized description",
+                    modifier = Modifier.size(FilterChipDefaults.IconSize)
+                )
+            }
         }
     )
 }
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
index e1d448a..2df1372 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipScreenshotTest.kt
@@ -20,7 +20,6 @@
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Done
-import androidx.compose.material.icons.filled.Home
 import androidx.compose.material.icons.filled.Person
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.testutils.assertAgainstGolden
@@ -329,7 +328,7 @@
                 onClick = {},
                 label = { Text("Filter Chip") },
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
@@ -342,58 +341,6 @@
     }
 
     @Test
-    fun filterChip_flat_withLeadingIcon_selected_lightTheme() {
-        rule.setMaterialContent(lightColorScheme()) {
-            FilterChip(
-                selected = true,
-                onClick = {},
-                label = { Text("Filter Chip") },
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_lightTheme")
-    }
-
-    @Test
-    fun filterChip_flat_withLeadingIcon_selected_darkTheme() {
-        rule.setMaterialContent(darkColorScheme()) {
-            FilterChip(
-                selected = true,
-                onClick = {},
-                label = { Text("Filter Chip") },
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_selected_darkTheme")
-    }
-
-    @Test
     fun filterChip_flat_notSelected() {
         rule.setMaterialContent(lightColorScheme()) {
             FilterChip(
@@ -415,7 +362,7 @@
                 label = { Text("Filter Chip") },
                 enabled = false,
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         tint = LocalContentColor.current,
@@ -429,33 +376,6 @@
     }
 
     @Test
-    fun filterChip_flat_withLeadingIcon_disabled_selected() {
-        rule.setMaterialContent(lightColorScheme()) {
-            FilterChip(
-                selected = true,
-                onClick = {},
-                label = { Text("Filter Chip") },
-                enabled = false,
-                modifier = Modifier.testTag(TestTag),
-                leadingIcon = {
-                    Icon(
-                        Icons.Filled.Home,
-                        contentDescription = "Localized Description"
-                    )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.requiredSize(FilterChipDefaults.IconSize)
-                    )
-                }
-            )
-        }
-        assertChipAgainstGolden("filterChip_flat_withLeadingIcon_disabled_selected")
-    }
-
-    @Test
     fun filterChip_flat_disabled_notSelected() {
         rule.setMaterialContent(lightColorScheme()) {
             FilterChip(
@@ -477,7 +397,7 @@
                 onClick = {},
                 label = { Text("Filter Chip") },
                 modifier = Modifier.testTag(TestTag),
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
index 1a173e3..2dd3963 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/ChipTest.kt
@@ -360,13 +360,6 @@
                         "Filter chip",
                         Modifier.testTag(TestChipTag)
                     )
-                },
-                selectedIcon = {
-                    Icon(
-                        imageVector = Icons.Filled.Done,
-                        contentDescription = "Localized Description",
-                        modifier = Modifier.size(FilterChipDefaults.IconSize)
-                    )
                 })
         }
 
@@ -395,7 +388,7 @@
                         Modifier.testTag(TestChipTag)
                     )
                 },
-                selectedIcon = {
+                leadingIcon = {
                     Icon(
                         imageVector = Icons.Filled.Done,
                         contentDescription = "Localized Description",
diff --git a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
index 8a49d2f..cccb678 100644
--- a/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
+++ b/compose/material3/material3/src/androidAndroidTest/kotlin/androidx/compose/material3/OutlinedTextFieldScreenshotTest.kt
@@ -17,7 +17,6 @@
 package androidx.compose.material3
 
 import android.os.Build
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.requiredHeight
 import androidx.compose.foundation.layout.requiredWidth
@@ -33,7 +32,6 @@
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.test.SemanticsNodeInteraction
 import androidx.compose.ui.test.captureToImage
 import androidx.compose.ui.test.junit4.createComposeRule
@@ -78,15 +76,13 @@
     @Test
     fun outlinedTextField_withInput() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Text"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Text"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_withInput")
@@ -95,14 +91,12 @@
     @Test
     fun outlinedTextField_notFocused() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_not_focused")
@@ -111,14 +105,12 @@
     @Test
     fun outlinedTextField_focused() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -130,14 +122,12 @@
     fun outlinedTextField_focused_rtl() {
         rule.setMaterialContent(lightColorScheme()) {
             CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                    OutlinedTextField(
-                        value = "",
-                        onValueChange = {},
-                        label = { Text("Label") },
-                        modifier = Modifier.requiredWidth(280.dp)
-                    )
-                }
+                OutlinedTextField(
+                    value = "",
+                    onValueChange = {},
+                    label = { Text("Label") },
+                    modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+                )
             }
         }
 
@@ -149,16 +139,14 @@
     @Test
     fun outlinedTextField_error_focused() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Input"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    isError = true,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Input"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                isError = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -169,15 +157,13 @@
     @Test
     fun outlinedTextField_error_notFocused() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    isError = true,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                isError = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_notFocused_errorState")
@@ -391,15 +377,13 @@
     @Test
     fun outlinedTextField_disabled() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = TextFieldValue("Text"),
-                    onValueChange = {},
-                    singleLine = true,
-                    enabled = false,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = TextFieldValue("Text"),
+                onValueChange = {},
+                singleLine = true,
+                enabled = false,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlinedTextField_disabled")
@@ -408,15 +392,13 @@
     @Test
     fun outlinedTextField_disabled_notFocusable() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = TextFieldValue("Text"),
-                    onValueChange = {},
-                    singleLine = true,
-                    enabled = false,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = TextFieldValue("Text"),
+                onValueChange = {},
+                singleLine = true,
+                enabled = false,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -427,15 +409,13 @@
     @Test
     fun outlinedTextField_disabled_notScrolled() {
         rule.setMaterialContent(lightColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = longText,
-                    onValueChange = { },
-                    singleLine = true,
-                    modifier = Modifier.requiredWidth(300.dp),
-                    enabled = false
-                )
-            }
+            OutlinedTextField(
+                value = longText,
+                onValueChange = { },
+                singleLine = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(300.dp),
+                enabled = false
+            )
         }
 
         rule.mainClock.autoAdvance = false
@@ -589,15 +569,13 @@
     @Test
     fun outlinedTextField_withInput_darkTheme() {
         rule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Text"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Text"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlined_textField_withInput_dark")
@@ -606,14 +584,12 @@
     @Test
     fun outlinedTextField_focused_darkTheme() {
         rule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = "",
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = "",
+                onValueChange = {},
+                label = { Text("Label") },
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -624,16 +600,14 @@
     @Test
     fun outlinedTextField_error_focused_darkTheme() {
         rule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                val text = "Input"
-                OutlinedTextField(
-                    value = TextFieldValue(text = text, selection = TextRange(text.length)),
-                    onValueChange = {},
-                    label = { Text("Label") },
-                    isError = true,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            val text = "Input"
+            OutlinedTextField(
+                value = TextFieldValue(text = text, selection = TextRange(text.length)),
+                onValueChange = {},
+                label = { Text("Label") },
+                isError = true,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         rule.onNodeWithTag(TextFieldTag).focus()
@@ -644,15 +618,13 @@
     @Test
     fun outlinedTextField_disabled_darkTheme() {
         rule.setMaterialContent(darkColorScheme()) {
-            Box(Modifier.semantics(mergeDescendants = true) {}.testTag(TextFieldTag)) {
-                OutlinedTextField(
-                    value = TextFieldValue("Text"),
-                    onValueChange = {},
-                    singleLine = true,
-                    enabled = false,
-                    modifier = Modifier.requiredWidth(280.dp)
-                )
-            }
+            OutlinedTextField(
+                value = TextFieldValue("Text"),
+                onValueChange = {},
+                singleLine = true,
+                enabled = false,
+                modifier = Modifier.testTag(TextFieldTag).requiredWidth(280.dp)
+            )
         }
 
         assertAgainstGolden("outlinedTextField_disabled_dark")
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
index 3ab2181..71ae1a7 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Chip.kt
@@ -220,8 +220,8 @@
  * This filter chip is applied with a flat style. If you want an elevated style, use the
  * [ElevatedFilterChip].
  *
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
  *
  * Example of a flat FilterChip with a trailing icon:
  * @sample androidx.compose.material3.samples.FilterChipSample
@@ -236,9 +236,9 @@
  * @param enabled controls the enabled state of this chip. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
  * @param trailingIcon optional icon at the end of the chip
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -263,7 +263,6 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     leadingIcon: @Composable (() -> Unit)? = null,
-    selectedIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: SelectableChipElevation? = FilterChipDefaults.filterChipElevation(),
@@ -277,7 +276,7 @@
     enabled = enabled,
     label = label,
     labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
-    leadingIcon = if (selected) selectedIcon else leadingIcon,
+    leadingIcon = leadingIcon,
     avatar = null,
     trailingIcon = trailingIcon,
     elevation = elevation,
@@ -304,8 +303,8 @@
  * This filter chip is applied with an elevated style. If you want a flat style, use the
  * [FilterChip].
  *
- * Tapping on a filter chip selects it, and in case a [selectedIcon] is provided (e.g. a checkmark),
- * it's appended to the starting edge of the chip's label, drawn instead of any given [leadingIcon].
+ * Tapping on a filter chip toggles its selection state. A selection state [leadingIcon] can be
+ * provided (e.g. a checkmark) to be appended at the starting edge of the chip's label.
  *
  * Example of an elevated FilterChip with a trailing icon:
  * @sample androidx.compose.material3.samples.ElevatedFilterChipSample
@@ -317,9 +316,9 @@
  * @param enabled controls the enabled state of this chip. When `false`, this component will not
  * respond to user input, and it will appear visually disabled and disabled to accessibility
  * services.
- * @param leadingIcon optional icon at the start of the chip, preceding the [label] text
- * @param selectedIcon optional icon at the start of the chip, preceding the [label] text, which is
- * displayed when the chip is selected, instead of any given [leadingIcon]
+ * @param leadingIcon optional icon at the start of the chip, preceding the [label] text. When
+ * [selected] is true, this icon may visually indicate that the chip is selected (for example, via a
+ * checkmark icon).
  * @param trailingIcon optional icon at the end of the chip
  * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s
  * for this chip. You can create and pass in your own `remember`ed instance to observe
@@ -344,7 +343,6 @@
     modifier: Modifier = Modifier,
     enabled: Boolean = true,
     leadingIcon: @Composable (() -> Unit)? = null,
-    selectedIcon: @Composable (() -> Unit)? = null,
     trailingIcon: @Composable (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
     elevation: SelectableChipElevation? = FilterChipDefaults.elevatedFilterChipElevation(),
@@ -358,7 +356,7 @@
     enabled = enabled,
     label = label,
     labelTextStyle = MaterialTheme.typography.fromToken(FilterChipTokens.LabelTextFont),
-    leadingIcon = if (selected) selectedIcon else leadingIcon,
+    leadingIcon = leadingIcon,
     avatar = null,
     trailingIcon = trailingIcon,
     elevation = elevation,
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
index 3e59495..66c86c1 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt
@@ -51,6 +51,7 @@
 import androidx.compose.ui.layout.Placeable
 import androidx.compose.ui.layout.layoutId
 import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
@@ -161,7 +162,11 @@
         BasicTextField(
             value = value,
             modifier = if (label != null) {
-                modifier.padding(top = OutlinedTextFieldTopPadding)
+                modifier
+                    // Merge semantics at the beginning of the modifier chain to ensure padding is
+                    // considered part of the text field.
+                    .semantics(mergeDescendants = true) {}
+                    .padding(top = OutlinedTextFieldTopPadding)
             } else {
                 modifier
             }
@@ -306,7 +311,11 @@
         BasicTextField(
             value = value,
             modifier = if (label != null) {
-                modifier.padding(top = OutlinedTextFieldTopPadding)
+                modifier
+                    // Merge semantics at the beginning of the modifier chain to ensure padding is
+                    // considered part of the text field.
+                    .semantics(mergeDescendants = true) {}
+                    .padding(top = OutlinedTextFieldTopPadding)
             } else {
                 modifier
             }
diff --git a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
index cbc5577..489920f 100644
--- a/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
+++ b/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt
@@ -452,7 +452,8 @@
                             previousSnapshot = currentSnapshot as? MutableSnapshot,
                             specifiedReadObserver = readObserver,
                             specifiedWriteObserver = writeObserver,
-                            mergeParentObservers = true
+                            mergeParentObservers = true,
+                            ownsPreviousSnapshot = false
                         )
                     else if (readObserver == null) return block()
                     else currentSnapshot.takeNestedSnapshot(readObserver)
@@ -1434,7 +1435,8 @@
     private val previousSnapshot: MutableSnapshot?,
     internal val specifiedReadObserver: ((Any) -> Unit)?,
     internal val specifiedWriteObserver: ((Any) -> Unit)?,
-    private val mergeParentObservers: Boolean
+    private val mergeParentObservers: Boolean,
+    private val ownsPreviousSnapshot: Boolean
 ) : MutableSnapshot(
     INVALID_SNAPSHOT,
     SnapshotIdSet.EMPTY,
@@ -1454,6 +1456,9 @@
     override fun dispose() {
         // Explicitly don't call super.dispose()
         disposed = true
+        if (ownsPreviousSnapshot) {
+            previousSnapshot?.dispose()
+        }
     }
 
     override var id: Int
@@ -1486,7 +1491,8 @@
         return if (!mergeParentObservers) {
             createTransparentSnapshotWithNoParentReadObserver(
                 previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
-                readObserver = readObserver
+                readObserver = mergedReadObserver,
+                ownsPreviousSnapshot = true
             )
         } else {
             currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1508,7 +1514,8 @@
                 previousSnapshot = nestedSnapshot,
                 specifiedReadObserver = mergedReadObserver,
                 specifiedWriteObserver = mergedWriteObserver,
-                mergeParentObservers = false
+                mergeParentObservers = false,
+                ownsPreviousSnapshot = true
             )
         } else {
             currentSnapshot.takeNestedMutableSnapshot(
@@ -1532,7 +1539,8 @@
 internal class TransparentObserverSnapshot(
     private val previousSnapshot: Snapshot?,
     specifiedReadObserver: ((Any) -> Unit)?,
-    private val mergeParentObservers: Boolean
+    private val mergeParentObservers: Boolean,
+    private val ownsPreviousSnapshot: Boolean
 ) : Snapshot(
     INVALID_SNAPSHOT,
     SnapshotIdSet.EMPTY,
@@ -1552,6 +1560,9 @@
     override fun dispose() {
         // Explicitly don't call super.dispose()
         disposed = true
+        if (ownsPreviousSnapshot) {
+            previousSnapshot?.dispose()
+        }
     }
 
     override var id: Int
@@ -1580,8 +1591,9 @@
         val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver)
         return if (!mergeParentObservers) {
             createTransparentSnapshotWithNoParentReadObserver(
-                previousSnapshot = currentSnapshot.takeNestedSnapshot(null),
-                readObserver = readObserver
+                currentSnapshot.takeNestedSnapshot(null),
+                mergedReadObserver,
+                ownsPreviousSnapshot = true
             )
         } else {
             currentSnapshot.takeNestedSnapshot(mergedReadObserver)
@@ -1599,18 +1611,21 @@
 private fun createTransparentSnapshotWithNoParentReadObserver(
     previousSnapshot: Snapshot?,
     readObserver: ((Any) -> Unit)? = null,
+    ownsPreviousSnapshot: Boolean = false
 ): Snapshot = if (previousSnapshot is MutableSnapshot || previousSnapshot == null) {
     TransparentObserverMutableSnapshot(
         previousSnapshot = previousSnapshot as? MutableSnapshot,
         specifiedReadObserver = readObserver,
         specifiedWriteObserver = null,
-        mergeParentObservers = false
+        mergeParentObservers = false,
+        ownsPreviousSnapshot = ownsPreviousSnapshot
     )
 } else {
     TransparentObserverSnapshot(
         previousSnapshot = previousSnapshot,
         specifiedReadObserver = readObserver,
-        mergeParentObservers = false
+        mergeParentObservers = false,
+        ownsPreviousSnapshot = ownsPreviousSnapshot
     )
 }
 
diff --git a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
index 8d279be..cdff066 100644
--- a/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
+++ b/compose/runtime/runtime/src/commonTest/kotlin/androidx/compose/runtime/snapshots/SnapshotTests.kt
@@ -936,6 +936,126 @@
         }
     }
 
+    @Test
+    fun testNestedWithinTransparentSnapshotDisposedCorrectly() {
+        val outerSnapshot = TransparentObserverSnapshot(
+            previousSnapshot = currentSnapshot(),
+            specifiedReadObserver = null,
+            mergeParentObservers = false,
+            ownsPreviousSnapshot = false
+        )
+
+        try {
+            outerSnapshot.enter {
+                val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+                try {
+                    innerSnapshot.enter { }
+                } finally {
+                    innerSnapshot.dispose()
+                }
+            }
+        } finally {
+            outerSnapshot.dispose()
+        }
+    }
+
+    @Test
+    fun testNestedWithinTransparentMutableSnapshotDisposedCorrectly() {
+        val outerSnapshot = TransparentObserverMutableSnapshot(
+            previousSnapshot = currentSnapshot() as? MutableSnapshot,
+            specifiedReadObserver = null,
+            specifiedWriteObserver = null,
+            mergeParentObservers = false,
+            ownsPreviousSnapshot = false
+        )
+
+        try {
+            outerSnapshot.enter {
+                val innerSnapshot = outerSnapshot.takeNestedSnapshot()
+
+                try {
+                    innerSnapshot.enter { }
+                } finally {
+                    innerSnapshot.dispose()
+                }
+            }
+        } finally {
+            outerSnapshot.dispose()
+        }
+    }
+
+    @Test
+    fun testTransparentSnapshotMergedWithNestedReadObserver() {
+        var outerChanges = 0
+        var innerChanges = 0
+        val state by mutableStateOf(0)
+
+        val outerSnapshot = TransparentObserverSnapshot(
+            previousSnapshot = currentSnapshot(),
+            specifiedReadObserver = { outerChanges++ },
+            mergeParentObservers = false,
+            ownsPreviousSnapshot = false
+        )
+
+        try {
+            outerSnapshot.enter {
+                val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+                    readObserver = { innerChanges++ }
+                )
+
+                try {
+                    innerSnapshot.enter {
+                        state // read
+                    }
+                } finally {
+                    innerSnapshot.dispose()
+                }
+            }
+        } finally {
+            outerSnapshot.dispose()
+        }
+
+        assertEquals(1, outerChanges)
+        assertEquals(1, innerChanges)
+    }
+
+    @Test
+    fun testTransparentMutableSnapshotMergedWithNestedReadObserver() {
+        var outerChanges = 0
+        var innerChanges = 0
+        val state by mutableStateOf(0)
+
+        val outerSnapshot = TransparentObserverMutableSnapshot(
+            previousSnapshot = currentSnapshot() as? MutableSnapshot,
+            specifiedReadObserver = { outerChanges++ },
+            specifiedWriteObserver = null,
+            mergeParentObservers = false,
+            ownsPreviousSnapshot = false
+        )
+
+        try {
+            outerSnapshot.enter {
+                val innerSnapshot = outerSnapshot.takeNestedSnapshot(
+                    readObserver = { innerChanges++ }
+                )
+
+                try {
+                    innerSnapshot.enter {
+                        state // read
+                    }
+                } finally {
+                    innerSnapshot.dispose()
+                }
+            }
+        } finally {
+            outerSnapshot.dispose()
+        }
+
+        assertEquals(1, outerChanges)
+        assertEquals(1, innerChanges)
+    }
+
     private var count = 0
 
     @BeforeTest
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
index f5dcd6c..b98fc6fdd 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
@@ -42,5 +42,7 @@
             .isEqualTo(LambdaLocation("TestLambdas.kt", 29, 30))
         assertThat(LambdaLocation.resolve(TestLambdas.inlinedParameter))
             .isEqualTo(LambdaLocation("TestLambdas.kt", 33, 33))
+        assertThat(LambdaLocation.resolve(TestLambdas.unnamed))
+            .isEqualTo(LambdaLocation("TestLambdas.kt", 35, 35))
     }
 }
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
index b127774..5ff2b27 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
@@ -32,6 +32,7 @@
     val inlinedParameter = { o: IntOffset ->
         o.x * 2
     }
+    val unnamed: (Int, Int) -> Float = { _, _ -> 0f }
 
     /**
      * This inline function will appear at a line numbers
diff --git a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
index 1a52bfb..11e4a1a 100644
--- a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
+++ b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
@@ -169,7 +169,8 @@
     InlineRange *ranges = nullptr;
     for (int i=0; i<variableCount; i++) {
         jvmtiLocalVariableEntry *variable = &variables[i];
-        if (strncmp("$i$f$", variable->name, 5) == 0) {
+        char* name = variable->name;
+        if (name != nullptr && strncmp("$i$f$", name, 5) == 0) {
             if (ranges == nullptr) {
                 jvmti->Allocate(sizeof(InlineRange) * (variableCount-i), (unsigned char **)&ranges);
             }
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 62dd84e..43fea44 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -553,13 +553,10 @@
                 .flatMap { config -> config.map { RawParameter(it.key.name, it.value) } }
         )
 
-        node.mergedSemantics.addAll(
-            modifierInfo.asSequence()
-                .map { it.modifier }
-                .filterIsInstance<SemanticsModifier>()
-                .map { it.id }
-                .flatMap { semanticsMap[it].orEmpty() }
-        )
+        val mergedSemantics = semanticsMap.get(layoutInfo.semanticsId)
+        if (mergedSemantics != null) {
+            node.mergedSemantics.addAll(mergedSemantics)
+        }
 
         node.id = modifierInfo.asSequence()
             .map { it.extra }
diff --git a/compose/ui/ui-tooling-data/build.gradle b/compose/ui/ui-tooling-data/build.gradle
index ab2b6fa..79fe224 100644
--- a/compose/ui/ui-tooling-data/build.gradle
+++ b/compose/ui/ui-tooling-data/build.gradle
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+
+import androidx.build.AndroidXComposePlugin
 import androidx.build.LibraryType
 import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
@@ -21,30 +23,99 @@
     id("AndroidXPlugin")
     id("com.android.library")
     id("AndroidXComposePlugin")
-    id("org.jetbrains.kotlin.android")
 }
 
-dependencies {
+AndroidXComposePlugin.applyAndConfigureKotlinPlugin(project)
 
-    implementation(libs.kotlinStdlib)
+if(!AndroidXComposePlugin.isMultiplatformEnabled(project)) {
 
-    api "androidx.annotation:annotation:1.1.0"
+    dependencies {
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block below
+         */
 
-    api("androidx.compose.runtime:runtime:1.2.0-rc02")
-    api(project(":compose:ui:ui"))
+        implementation(libs.kotlinStdlib)
 
-    androidTestImplementation project(":compose:ui:ui-test-junit4")
+        api "androidx.annotation:annotation:1.1.0"
 
-    androidTestImplementation(libs.junit)
-    androidTestImplementation(libs.testCore)
-    androidTestImplementation(libs.testRunner)
-    androidTestImplementation(libs.testRules)
+        api("androidx.compose.runtime:runtime:1.2.0-rc02")
+        api(project(":compose:ui:ui"))
 
-    androidTestImplementation(libs.truth)
-    androidTestImplementation(project(":compose:foundation:foundation-layout"))
-    androidTestImplementation(project(":compose:foundation:foundation"))
-    androidTestImplementation(project(":compose:material:material"))
-    androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+        androidTestImplementation project(":compose:ui:ui-test-junit4")
+
+        androidTestImplementation(libs.junit)
+        androidTestImplementation(libs.testCore)
+        androidTestImplementation(libs.testRunner)
+        androidTestImplementation(libs.testRules)
+
+        androidTestImplementation(libs.truth)
+        androidTestImplementation(project(":compose:foundation:foundation-layout"))
+        androidTestImplementation(project(":compose:foundation:foundation"))
+        androidTestImplementation(project(":compose:material:material"))
+        androidTestImplementation("androidx.activity:activity-compose:1.3.1")
+    }
+}
+
+if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
+    androidXComposeMultiplatform {
+        android()
+        desktop()
+    }
+
+    kotlin {
+
+        /*
+         * When updating dependencies, make sure to make the an an analogous update in the
+         * corresponding block above
+         */
+        sourceSets {
+            commonMain.dependencies {
+
+                implementation(libs.kotlinStdlib)
+
+                api "androidx.annotation:annotation:1.1.0"
+
+                api("androidx.compose.runtime:runtime:1.2.0-rc02")
+                api(project(":compose:ui:ui"))
+            }
+            jvmMain.dependencies {
+                implementation(libs.kotlinStdlib)
+            }
+            androidMain.dependencies {
+                api("androidx.annotation:annotation:1.1.0")
+            }
+
+            commonTest.dependencies {
+                implementation(kotlin("test-junit"))
+            }
+
+            // TODO(b/214407011): These dependencies leak into instrumented tests as well. If you
+            //  need to add Robolectric (which must be kept out of androidAndroidTest), use a top
+            //  level dependencies block instead:
+            //  `dependencies { testImplementation(libs.robolectric) }`
+            androidTest.dependencies {
+                implementation(libs.truth)
+            }
+            androidAndroidTest.dependencies {
+                implementation(project(":compose:ui:ui-test-junit4"))
+
+                implementation(libs.junit)
+                implementation(libs.testCore)
+                implementation(libs.testRunner)
+                implementation(libs.testRules)
+
+                implementation(libs.truth)
+                implementation(project(":compose:foundation:foundation-layout"))
+                implementation(project(":compose:foundation:foundation"))
+                implementation(project(":compose:material:material"))
+                implementation("androidx.activity:activity-compose:1.3.1")
+            }
+        }
+    }
+    dependencies {
+        samples(projectOrArtifact(":compose:ui:ui-unit:ui-unit-samples"))
+    }
 }
 
 androidx {
diff --git a/compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/BoundsTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/BoundsTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/Inspectable.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/Inspectable.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/InspectableTests.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/InspectableTests.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ModifierInfoTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetData.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetData.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/OffsetInformationTest.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/TestActivity.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/TestActivity.kt
diff --git a/compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt b/compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/androidTest/java/androidx/compose/ui/tooling/data/ToolingTest.kt
rename to compose/ui/ui-tooling-data/src/androidAndroidTest/kotlin/androidx/compose/ui/tooling/data/ToolingTest.kt
diff --git a/compose/ui/ui-tooling-data/src/main/AndroidManifest.xml b/compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/AndroidManifest.xml
rename to compose/ui/ui-tooling-data/src/androidMain/AndroidManifest.xml
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/SlotTree.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/SlotTree.kt
diff --git a/compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt b/compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
similarity index 100%
rename from compose/ui/ui-tooling-data/src/main/java/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
rename to compose/ui/ui-tooling-data/src/jvmMain/kotlin/androidx/compose/ui/tooling/data/UiToolingDataApi.kt
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -1,4 +1,8 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+    Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
 RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
     Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
 RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index bf169d7..6d14189 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1871,6 +1871,7 @@
     method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
     method public long localToRoot(long relativeToLocal);
     method public long localToWindow(long relativeToLocal);
+    method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
     method public long windowToLocal(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getSemanticsId();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public int getWidth();
     method public boolean isAttached();
@@ -1916,6 +1918,7 @@
     property public abstract boolean isPlaced;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int semanticsId;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     property public abstract int width;
   }
@@ -2769,9 +2772,9 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
-    method public int getId();
+    method @Deprecated public default int getId();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract int id;
+    property @Deprecated public default int id;
     property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
   }
 
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index f6fb19e..32bc836 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2017,6 +2017,7 @@
     method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
     method public long localToRoot(long relativeToLocal);
     method public long localToWindow(long relativeToLocal);
+    method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
     method public long windowToLocal(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -2051,6 +2052,7 @@
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getSemanticsId();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public int getWidth();
     method public boolean isAttached();
@@ -2062,6 +2064,7 @@
     property public abstract boolean isPlaced;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int semanticsId;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     property public abstract int width;
   }
@@ -2982,9 +2985,9 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
-    method public int getId();
+    method @Deprecated public default int getId();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract int id;
+    property @Deprecated public default int id;
     property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
   }
 
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -1,4 +1,8 @@
 // Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+    Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
 RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
     Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
 RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index f6326ba..2b300fe 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1871,6 +1871,7 @@
     method public long localPositionOf(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, long relativeToSource);
     method public long localToRoot(long relativeToLocal);
     method public long localToWindow(long relativeToLocal);
+    method public default void transformFrom(androidx.compose.ui.layout.LayoutCoordinates sourceCoordinates, float[] matrix);
     method public long windowToLocal(long relativeToWindow);
     property public abstract boolean isAttached;
     property public abstract androidx.compose.ui.layout.LayoutCoordinates? parentCoordinates;
@@ -1905,6 +1906,7 @@
     method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
     method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
     method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+    method public int getSemanticsId();
     method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
     method public int getWidth();
     method public boolean isAttached();
@@ -1916,6 +1918,7 @@
     property public abstract boolean isPlaced;
     property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
     property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+    property public abstract int semanticsId;
     property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
     property public abstract int width;
   }
@@ -2805,9 +2808,9 @@
   }
 
   @kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
-    method public int getId();
+    method @Deprecated public default int getId();
     method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
-    property public abstract int id;
+    property @Deprecated public default int id;
     property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
   }
 
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 5adc1c1..c77098c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -904,7 +904,6 @@
         val nodes = SemanticsOwner(
             LayoutNode().also {
                 it.modifier = SemanticsModifierCore(
-                    id = SemanticsModifierCore.generateSemanticsId(),
                     mergeDescendants = false,
                     clearAndSetSemantics = false,
                     properties = {}
@@ -1264,9 +1263,9 @@
         mergeDescendants: Boolean,
         properties: (SemanticsPropertyReceiver.() -> Unit)
     ): SemanticsNode {
-        val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, false, properties)
+        val semanticsModifier = SemanticsModifierCore(mergeDescendants, false, properties)
         return SemanticsNode(
-            SemanticsEntity(InnerPlaceable(LayoutNode()), semanticsModifier),
+            SemanticsEntity(InnerPlaceable(LayoutNode(semanticsId = id)), semanticsModifier),
             true
         )
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
index a7ea12c..0fe6b5d 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/input/pointer/HitPathTrackerTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.geometry.Rect
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
@@ -3755,6 +3756,12 @@
             ) {
             }
 
+            override fun transform(matrix: Matrix) {
+            }
+
+            override fun inverseTransform(matrix: Matrix) {
+            }
+
             override fun mapOffset(point: Offset, inverse: Boolean) = point
         }
     }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
index b75e054..4deac3c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/LookaheadLayoutTest.kt
@@ -61,6 +61,7 @@
 import androidx.compose.ui.composed
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.platform.AndroidOwnerExtraAssertionsRule
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.test.junit4.createAndroidComposeRule
@@ -976,6 +977,38 @@
         }
     }
 
+    @Test
+    fun lookaheadLayoutTransformFrom() {
+        val matrix = Matrix()
+        rule.setContent {
+            CompositionLocalProvider(LocalDensity provides Density(1f)) {
+                LookaheadLayout(
+                    measurePolicy = { measurables, constraints ->
+                        val placeable = measurables[0].measure(constraints)
+                        // Position the children.
+                        layout(placeable.width + 10, placeable.height + 10) {
+                            placeable.place(10, 10)
+                        }
+                    },
+                    content = {
+                        Box(
+                            Modifier
+                                .onPlaced { lookaheadScopeCoordinates, layoutCoordinates ->
+                                    layoutCoordinates.transformFrom(
+                                        lookaheadScopeCoordinates,
+                                        matrix
+                                    )
+                                }
+                                .size(10.dp))
+                    }
+                )
+            }
+        }
+        rule.waitForIdle()
+        val posInChild = matrix.map(Offset(10f, 10f))
+        assertEquals(Offset.Zero, posInChild)
+    }
+
     private fun assertSameLayoutWithAndWithoutLookahead(
         content: @Composable (
             modifier: Modifier
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
index 241e5cd..86a6f47 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/layout/RemeasureWithIntrinsicsTest.kt
@@ -406,6 +406,33 @@
                 shapeColor = Color.Red
             )
     }
+
+    @Test
+    fun introducingChildIntrinsicsViaModifierWhenParentUsedIntrinsicSizes() {
+        var childModifier by mutableStateOf(Modifier as Modifier)
+
+        rule.setContent {
+            LayoutUsingIntrinsics() {
+                Box(
+                    Modifier
+                        .testTag("child")
+                        .then(childModifier)
+                )
+            }
+        }
+
+        rule.onNodeWithTag("child")
+            .assertWidthIsEqualTo(0.dp)
+            .assertHeightIsEqualTo(0.dp)
+
+        rule.runOnIdle {
+            childModifier = Modifier.withIntrinsics(30.dp, 20.dp)
+        }
+
+        rule.onNodeWithTag("child")
+            .assertWidthIsEqualTo(30.dp)
+            .assertHeightIsEqualTo(20.dp)
+    }
 }
 
 @Composable
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 085a1f7..55e8f7e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -70,7 +70,6 @@
 import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
 import kotlin.math.max
 
 @MediumTest
@@ -91,16 +90,6 @@
         isDebugInspectorInfoEnabled = false
     }
 
-    private fun executeUpdateBlocking(updateFunction: () -> Unit) {
-        val latch = CountDownLatch(1)
-        rule.runOnUiThread {
-            updateFunction()
-            latch.countDown()
-        }
-
-        latch.await()
-    }
-
     @Test
     fun unchangedSemanticsDoesNotCauseRelayout() {
         val layoutCounter = Counter(0)
@@ -118,6 +107,22 @@
     }
 
     @Test
+    fun valueSemanticsAreEqual() {
+        assertEquals(
+            Modifier.semantics {
+                text = AnnotatedString("text")
+                contentDescription = "foo"
+                popup()
+            },
+            Modifier.semantics {
+                text = AnnotatedString("text")
+                contentDescription = "foo"
+                popup()
+            }
+        )
+    }
+
+    @Test
     fun depthFirstPropertyConcat() {
         val root = "root"
         val child1 = "child1"
@@ -533,6 +538,12 @@
 
         val isAfter = mutableStateOf(false)
 
+        val content: @Composable () -> Unit = {
+            SimpleTestLayout {
+                nodeCount++
+            }
+        }
+
         rule.setContent {
             SimpleTestLayout(
                 Modifier.testTag(TestTag).semantics {
@@ -543,12 +554,9 @@
                             return@onClick true
                         }
                     )
-                }
-            ) {
-                SimpleTestLayout {
-                    nodeCount++
-                }
-            }
+                },
+                content = content
+            )
         }
 
         // This isn't the important part, just makes sure everything is behaving as expected
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
index 9344d54..0885058 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/viewinterop/AndroidViewTest.kt
@@ -17,6 +17,8 @@
 package androidx.compose.ui.viewinterop
 
 import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
 import android.os.Build
 import android.os.Bundle
 import android.os.Parcelable
@@ -33,6 +35,7 @@
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
@@ -45,8 +48,10 @@
 import androidx.compose.runtime.saveable.rememberSaveableStateHolder
 import androidx.compose.runtime.setValue
 import androidx.compose.testutils.assertPixels
+import androidx.compose.ui.AbsoluteAlignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.layout.onGloballyPositioned
 import androidx.compose.ui.platform.ComposeView
 import androidx.compose.ui.platform.LocalDensity
@@ -272,7 +277,10 @@
             }
         }
         rule.setContent {
-            AndroidView({ frameLayout }, Modifier.testTag("view").background(color = Color.Blue))
+            AndroidView({ frameLayout },
+                Modifier
+                    .testTag("view")
+                    .background(color = Color.Blue))
         }
 
         rule.onNodeWithTag("view").captureToImage().assertPixels(IntSize(size, size)) {
@@ -356,9 +364,11 @@
             CompositionLocalProvider(LocalDensity provides density) {
                 AndroidView(
                     { FrameLayout(it) },
-                    Modifier.requiredSize(size).onGloballyPositioned {
-                        assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
-                    }
+                    Modifier
+                        .requiredSize(size)
+                        .onGloballyPositioned {
+                            assertThat(it.size).isEqualTo(IntSize(sizeIpx, sizeIpx))
+                        }
                 )
             }
         }
@@ -566,7 +576,11 @@
         val sizeDp = with(rule.density) { size.toDp() }
         rule.setContent {
             Column {
-                Box(Modifier.size(sizeDp).background(Color.Blue).testTag("box"))
+                Box(
+                    Modifier
+                        .size(sizeDp)
+                        .background(Color.Blue)
+                        .testTag("box"))
                 AndroidView(factory = { SurfaceView(it) })
             }
         }
@@ -619,6 +633,40 @@
         }
     }
 
+    @Test
+    @SdkSuppress(minSdkVersion = Build.VERSION_CODES.O)
+    fun androidView_noClip() {
+        rule.setContent {
+            Box(Modifier.fillMaxSize().background(Color.White)) {
+                with(LocalDensity.current) {
+                    Box(Modifier.requiredSize(150.toDp()).testTag("box")) {
+                        Box(
+                            Modifier.size(100.toDp(), 100.toDp()).align(AbsoluteAlignment.TopLeft)
+                        ) {
+                            AndroidView(factory = { context ->
+                                object : View(context) {
+                                    init {
+                                        clipToOutline = false
+                                    }
+
+                                    override fun onDraw(canvas: Canvas) {
+                                        val paint = Paint()
+                                        paint.color = Color.Blue.toArgb()
+                                        paint.style = Paint.Style.FILL
+                                        canvas.drawRect(0f, 0f, 150f, 150f, paint)
+                                    }
+                                }
+                            })
+                        }
+                    }
+                }
+            }
+        }
+        rule.onNodeWithTag("box").captureToImage().assertPixels(IntSize(150, 150)) {
+            Color.Blue
+        }
+    }
+
     private class StateSavingView(
         private val key: String,
         private val value: String,
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a60a1e2..532f0d5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -189,7 +189,6 @@
         private set
 
     private val semanticsModifier = SemanticsModifierCore(
-        id = SemanticsModifierCore.generateSemanticsId(),
         mergeDescendants = false,
         clearAndSetSemantics = false,
         properties = {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 130f682..b7ff6d8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -1558,7 +1558,8 @@
             ) {
                 val androidView = view.androidViewsHandler.layoutNodeToHolder[wrapper.layoutNode]
                 if (androidView == null) {
-                    virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.modifier.id)
+                    virtualViewId =
+                        semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.layoutNode.semanticsId)
                 }
             }
         }
@@ -1719,7 +1720,7 @@
                     ?.isMergingSemanticsOfDescendants == true
             }?.outerSemantics?.let { semanticsWrapper = it }
         }
-        val id = semanticsWrapper.modifier.id
+        val id = semanticsWrapper.layoutNode.semanticsId
         if (!subtreeChangedSemanticsNodesIds.add(id)) {
             return
         }
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
index a62b572..07f4b2e 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/RenderNodeLayer.android.kt
@@ -25,6 +25,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.RenderEffect
@@ -341,6 +342,17 @@
         this.invalidateParentLayer = invalidateParentLayer
     }
 
+    override fun transform(matrix: Matrix) {
+        matrix.timesAssign(matrixCache.calculateMatrix(renderNode))
+    }
+
+    override fun inverseTransform(matrix: Matrix) {
+        val inverse = matrixCache.calculateInverseMatrix(renderNode)
+        if (inverse != null) {
+            matrix.timesAssign(inverse)
+        }
+    }
+
     companion object {
         private val getMatrix: (DeviceRenderNode, android.graphics.Matrix) -> Unit = { rn, matrix ->
             rn.getMatrix(matrix)
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
index 912ff1f..55d1dcd 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ViewLayer.android.kt
@@ -27,6 +27,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.CanvasHolder
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Path
 import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.graphics.RenderEffect
@@ -356,6 +357,17 @@
         this.invalidateParentLayer = invalidateParentLayer
     }
 
+    override fun transform(matrix: Matrix) {
+        matrix.timesAssign(matrixCache.calculateMatrix(this))
+    }
+
+    override fun inverseTransform(matrix: Matrix) {
+        val inverse = matrixCache.calculateInverseMatrix(this)
+        if (inverse != null) {
+            matrix.timesAssign(inverse)
+        }
+    }
+
     companion object {
         private val getMatrix: (View, android.graphics.Matrix) -> Unit = { view, matrix ->
             val newMatrix = view.matrix
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
index fe922bc..9b725ee 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/viewinterop/AndroidView.android.kt
@@ -167,6 +167,10 @@
 
     override val viewRoot: View get() = this
 
+    init {
+        clipChildren = false
+    }
+
     var factory: ((Context) -> T)? = null
         set(value) {
             field = value
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
index 6f7cfe9..2a8772f 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutCoordinates.kt
@@ -18,6 +18,7 @@
 
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.node.LayoutNodeWrapper
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -93,6 +94,12 @@
     fun localBoundingBoxOf(sourceCoordinates: LayoutCoordinates, clipBounds: Boolean = true): Rect
 
     /**
+     * Modifies [matrix] to be a transform to convert a coordinate in [sourceCoordinates]
+     * to a coordinate in `this` [LayoutCoordinates].
+     */
+    fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {}
+
+    /**
      * Returns the position in pixels of an [alignment line][AlignmentLine],
      * or [AlignmentLine.Unspecified] if the line is not provided.
      */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
index 571029f..cb2fd62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -77,6 +77,11 @@
      * Returns true if this layout is currently a part of the layout tree.
      */
     val isAttached: Boolean
+
+    /**
+     * Unique and stable id representing this node to the semantics system.
+     */
+    val semanticsId: Int
 }
 
 /**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
index 828213c..dc47e9e 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LookaheadLayoutCoordinates.kt
@@ -21,6 +21,7 @@
 import androidx.compose.ui.ExperimentalComposeUiApi
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.node.LayoutNodeWrapper
 import androidx.compose.ui.node.LookaheadDelegate
 import androidx.compose.ui.unit.IntSize
@@ -107,6 +108,10 @@
         clipBounds: Boolean
     ): Rect = wrapper.localBoundingBoxOf(sourceCoordinates, clipBounds)
 
+    override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+        wrapper.transformFrom(sourceCoordinates, matrix)
+    }
+
     override fun get(alignmentLine: AlignmentLine): Int = wrapper.get(alignmentLine)
 }
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c973b88..a5b4070 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.platform.simpleIdentityToString
 import androidx.compose.ui.semantics.SemanticsEntity
+import androidx.compose.ui.semantics.SemanticsModifierCore.Companion.generateSemanticsId
 import androidx.compose.ui.semantics.outerSemantics
 import androidx.compose.ui.unit.Constraints
 import androidx.compose.ui.unit.Density
@@ -74,7 +75,9 @@
     // virtual nodes will be treated as the direct children of the virtual node parent.
     // This whole concept will be replaced with a proper subcomposition logic which allows to
     // subcompose multiple times into the same LayoutNode and define offsets.
-    private val isVirtual: Boolean = false
+    private val isVirtual: Boolean = false,
+    // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
+    override val semanticsId: Int = generateSemanticsId()
 ) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
     Owner.OnLayoutCompletedListener {
 
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
index 6191cb2..34fef50 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNodeWrapper.kt
@@ -31,6 +31,7 @@
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.DefaultCameraDistance
 import androidx.compose.ui.graphics.GraphicsLayerScope
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.Paint
 import androidx.compose.ui.graphics.ReusableGraphicsLayerScope
 import androidx.compose.ui.graphics.TransformOrigin
@@ -717,6 +718,43 @@
         return ancestorToLocal(commonAncestor, position)
     }
 
+    override fun transformFrom(sourceCoordinates: LayoutCoordinates, matrix: Matrix) {
+        val layoutNodeWrapper = sourceCoordinates.toWrapper()
+        val commonAncestor = findCommonAncestor(layoutNodeWrapper)
+
+        matrix.reset()
+        // Transform from the source to the common ancestor
+        layoutNodeWrapper.transformToAncestor(commonAncestor, matrix)
+        // Transform from the common ancestor to this
+        transformFromAncestor(commonAncestor, matrix)
+    }
+
+    private fun transformToAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+        var wrapper = this
+        while (wrapper != ancestor) {
+            wrapper.layer?.transform(matrix)
+            val position = wrapper.position
+            if (position != IntOffset.Zero) {
+                tmpMatrix.reset()
+                tmpMatrix.translate(position.x.toFloat(), position.y.toFloat())
+                matrix.timesAssign(tmpMatrix)
+            }
+            wrapper = wrapper.wrappedBy!!
+        }
+    }
+
+    private fun transformFromAncestor(ancestor: LayoutNodeWrapper, matrix: Matrix) {
+        if (ancestor != this) {
+            wrappedBy!!.transformFromAncestor(ancestor, matrix)
+            if (position != IntOffset.Zero) {
+                tmpMatrix.reset()
+                tmpMatrix.translate(-position.x.toFloat(), -position.y.toFloat())
+                matrix.timesAssign(tmpMatrix)
+            }
+            layer?.inverseTransform(matrix)
+        }
+    }
+
     override fun localBoundingBoxOf(
         sourceCoordinates: LayoutCoordinates,
         clipBounds: Boolean
@@ -1139,6 +1177,10 @@
         private val graphicsLayerScope = ReusableGraphicsLayerScope()
         private val tmpLayerPositionalProperties = LayerPositionalProperties()
 
+        // Used for matrix calculations. It should not be used for anything that could lead to
+        // reentrancy.
+        private val tmpMatrix = Matrix()
+
         /**
          * Hit testing specifics for pointer input.
          */
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
index 73d3ce0..a12e478 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/OwnedLayer.kt
@@ -121,4 +121,16 @@
      * as new after this call.
      */
     fun reuseLayer(drawBlock: (Canvas) -> Unit, invalidateParentLayer: () -> Unit)
+
+    /**
+     * Calculates the transform from the parent to the local coordinates and multiplies
+     * [matrix] by the transform.
+     */
+    fun transform(matrix: Matrix)
+
+    /**
+     * Calculates the transform from the layer to the parent and multiplies [matrix] by
+     * the transform.
+     */
+    fun inverseTransform(matrix: Matrix)
 }
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
index 164d52e..54e3dfa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
@@ -26,6 +26,9 @@
     wrapped: LayoutNodeWrapper,
     modifier: SemanticsModifier
 ) : LayoutNodeEntity<SemanticsEntity, SemanticsModifier>(wrapped, modifier) {
+    val id: Int
+        get() = layoutNode.semanticsId
+
     private val useMinimumTouchTarget: Boolean
         get() = modifier.semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
 
@@ -56,7 +59,7 @@
     }
 
     override fun toString(): String {
-        return "${super.toString()} id: ${modifier.id} config: ${modifier.semanticsConfiguration}"
+        return "${super.toString()} semanticsId: $id config: ${modifier.semanticsConfiguration}"
     }
 
     fun touchBoundsInRoot(): Rect {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 142f6fd..de40d11 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -16,10 +16,11 @@
 
 package androidx.compose.ui.semantics
 
-import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
 import androidx.compose.ui.platform.AtomicInt
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.NoInspectorInfo
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.internal.JvmDefaultWithCompatibility
 
@@ -29,12 +30,12 @@
  */
 @JvmDefaultWithCompatibility
 interface SemanticsModifier : Modifier.Element {
-    /**
-     * The unique id of this semantics.
-     *
-     * Should be generated from SemanticsModifierCore.generateSemanticsId().
-     */
-    val id: Int
+    @Deprecated(
+        message = "SemanticsModifier.id is now unused and has been set to a fixed value. " +
+            "Retrieve the id from LayoutInfo instead.",
+        replaceWith = ReplaceWith("")
+    )
+    val id: Int get() = -1
 
     /**
      * The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
@@ -44,18 +45,18 @@
 }
 
 internal class SemanticsModifierCore(
-    override val id: Int,
     mergeDescendants: Boolean,
     clearAndSetSemantics: Boolean,
-    properties: (SemanticsPropertyReceiver.() -> Unit)
-) : SemanticsModifier {
+    properties: (SemanticsPropertyReceiver.() -> Unit),
+    inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
+) : SemanticsModifier, InspectorValueInfo(inspectorInfo) {
     override val semanticsConfiguration: SemanticsConfiguration =
         SemanticsConfiguration().also {
             it.isMergingSemanticsOfDescendants = mergeDescendants
             it.isClearingSemantics = clearAndSetSemantics
-
             it.properties()
         }
+
     companion object {
         private var lastIdentifier = AtomicInt(0)
         fun generateSemanticsId() = lastIdentifier.addAndGet(1)
@@ -64,15 +65,12 @@
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (other !is SemanticsModifierCore) return false
-
-        if (id != other.id) return false
         if (semanticsConfiguration != other.semanticsConfiguration) return false
-
         return true
     }
 
     override fun hashCode(): Int {
-        return 31 * semanticsConfiguration.hashCode() + id.hashCode()
+        return semanticsConfiguration.hashCode()
     }
 }
 
@@ -109,16 +107,16 @@
 fun Modifier.semantics(
     mergeDescendants: Boolean = false,
     properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+    mergeDescendants = mergeDescendants,
+    clearAndSetSemantics = false,
+    properties = properties,
     inspectorInfo = debugInspectorInfo {
         name = "semantics"
         this.properties["mergeDescendants"] = mergeDescendants
         this.properties["properties"] = properties
     }
-) {
-    val id = remember { SemanticsModifierCore.generateSemanticsId() }
-    SemanticsModifierCore(id, mergeDescendants, clearAndSetSemantics = false, properties)
-}
+)
 
 /**
  * Clears the semantics of all the descendant nodes and sets new semantics.
@@ -137,12 +135,12 @@
  */
 fun Modifier.clearAndSetSemantics(
     properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+    mergeDescendants = false,
+    clearAndSetSemantics = true,
+    properties = properties,
     inspectorInfo = debugInspectorInfo {
         name = "clearAndSetSemantics"
         this.properties["properties"] = properties
     }
-) {
-    val id = remember { SemanticsModifierCore.generateSemanticsId() }
-    SemanticsModifierCore(id, mergeDescendants = false, clearAndSetSemantics = true, properties)
-}
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 1701b58..1a45eb5d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -64,7 +64,6 @@
     private var fakeNodeParent: SemanticsNode? = null
 
     internal val unmergedConfig = outerSemanticsEntity.collapsedSemanticsConfiguration()
-    val id: Int = outerSemanticsEntity.modifier.id
 
     /**
      * The [LayoutInfo] that this is associated with.
@@ -81,6 +80,8 @@
      */
     internal val layoutNode: LayoutNode = outerSemanticsEntity.layoutNode
 
+    val id: Int = layoutNode.semanticsId
+
     // GEOMETRY
 
     /**
@@ -379,9 +380,12 @@
     ): SemanticsNode {
         val fakeNode = SemanticsNode(
             outerSemanticsEntity = SemanticsEntity(
-                wrapped = LayoutNode(isVirtual = true).innerLayoutNodeWrapper,
+                wrapped = LayoutNode(
+                    isVirtual = true,
+                    semanticsId =
+                        if (role != null) roleFakeNodeId() else contentDescriptionFakeNodeId()
+                ).innerLayoutNodeWrapper,
                 modifier = SemanticsModifierCore(
-                    if (role != null) this.roleFakeNodeId() else contentDescriptionFakeNodeId(),
                     mergeDescendants = false,
                     clearAndSetSemantics = false,
                     properties = properties
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index c672f75..b420d49 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -115,7 +115,6 @@
     override val sharedDrawScope = LayoutNodeDrawScope()
 
     private val semanticsModifier = SemanticsModifierCore(
-        id = SemanticsModifierCore.generateSemanticsId(),
         mergeDescendants = false,
         clearAndSetSemantics = false,
         properties = {}
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
index b219330..49c7b5d 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaLayer.skiko.kt
@@ -232,6 +232,14 @@
         canvas.restore()
     }
 
+    override fun transform(matrix: Matrix) {
+        matrix.timesAssign(getMatrix(inverse = false))
+    }
+
+    override fun inverseTransform(matrix: Matrix) {
+        matrix.timesAssign(getMatrix(inverse = true))
+    }
+
     private fun performDrawLayer(canvas: Canvas, bounds: Rect) {
         if (alpha > 0) {
             if (shadowElevation > 0) {
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index b561f1d..cd84292 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -28,6 +28,7 @@
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Canvas
 import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Matrix
 import androidx.compose.ui.graphics.RenderEffect
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.graphics.TransformOrigin
@@ -46,6 +47,7 @@
 import androidx.compose.ui.layout.Measurable
 import androidx.compose.ui.layout.MeasureResult
 import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.layout.RootMeasurePolicy.measure
 import androidx.compose.ui.layout.layout
 import androidx.compose.ui.layout.positionInRoot
 import androidx.compose.ui.platform.AccessibilityManager
@@ -53,6 +55,7 @@
 import androidx.compose.ui.platform.TextToolbar
 import androidx.compose.ui.platform.ViewConfiguration
 import androidx.compose.ui.platform.WindowInfo
+import androidx.compose.ui.platform.invertTo
 import androidx.compose.ui.semantics.SemanticsConfiguration
 import androidx.compose.ui.semantics.SemanticsEntity
 import androidx.compose.ui.semantics.SemanticsModifier
@@ -824,6 +827,204 @@
     }
 
     @Test
+    fun layoutNodeWrapper_transformFrom_offsets() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child = ZeroSizedLayoutNode()
+        parent.insertAt(0, child)
+        parent.place(-100, 10)
+        child.place(50, 80)
+
+        val matrix = Matrix()
+        child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(Offset(-50f, -80f), matrix.map(Offset.Zero))
+
+        parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(Offset(50f, 80f), matrix.map(Offset.Zero))
+    }
+
+    @Test
+    fun layoutNodeWrapper_transformFrom_translation() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child = ZeroSizedLayoutNode()
+        parent.insertAt(0, child)
+        child.modifier = Modifier.graphicsLayer {
+            translationX = 5f
+            translationY = 2f
+        }
+        parent.outerLayoutNodeWrapper.measureScope
+            .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+        child.outerLayoutNodeWrapper
+            .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+        parent.place(0, 0)
+        child.place(0, 0)
+
+        val matrix = Matrix()
+        child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(-5f, matrix.map(Offset.Zero).x, 0.001f)
+        assertEquals(-2f, matrix.map(Offset.Zero).y, 0.001f)
+
+        parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(5f, matrix.map(Offset.Zero).x, 0.001f)
+        assertEquals(2f, matrix.map(Offset.Zero).y, 0.001f)
+    }
+
+    @Test
+    fun layoutNodeWrapper_transformFrom_rotation() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child = ZeroSizedLayoutNode()
+        parent.insertAt(0, child)
+        child.modifier = Modifier.graphicsLayer {
+            rotationZ = 90f
+        }
+        parent.outerLayoutNodeWrapper.measureScope
+            .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+        child.outerLayoutNodeWrapper
+            .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+        parent.place(0, 0)
+        child.place(0, 0)
+
+        val matrix = Matrix()
+        child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+        assertEquals(-1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+
+        parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(0f, matrix.map(Offset(1f, 0f)).x, 0.001f)
+        assertEquals(1f, matrix.map(Offset(1f, 0f)).y, 0.001f)
+    }
+
+    @Test
+    fun layoutNodeWrapper_transformFrom_scale() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child = ZeroSizedLayoutNode()
+        parent.insertAt(0, child)
+        child.modifier = Modifier.graphicsLayer {
+            scaleX = 0f
+        }
+        parent.outerLayoutNodeWrapper.measureScope
+            .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+        child.outerLayoutNodeWrapper
+            .measureScope.measure(listOf(child.outerLayoutNodeWrapper), Constraints())
+        parent.place(0, 0)
+        child.place(0, 0)
+
+        val matrix = Matrix()
+        child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+        // The X coordinate is somewhat nonsensical since it is scaled to 0
+        // We've chosen to make it not transform when there's a nonsensical inverse.
+        assertEquals(1f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+        assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+        parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+        // This direction works, so we can expect the normal scaling
+        assertEquals(0f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+        assertEquals(1f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+        child.innerLayoutNodeWrapper.onLayerBlockUpdated {
+            scaleX = 0.5f
+            scaleY = 0.25f
+        }
+
+        child.innerLayoutNodeWrapper.transformFrom(parent.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(2f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+        assertEquals(4f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+
+        parent.innerLayoutNodeWrapper.transformFrom(child.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(0.5f, matrix.map(Offset(1f, 1f)).x, 0.001f)
+        assertEquals(0.25f, matrix.map(Offset(1f, 1f)).y, 0.001f)
+    }
+
+    @Test
+    fun layoutNodeWrapper_transformFrom_siblings() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child1 = ZeroSizedLayoutNode()
+        parent.insertAt(0, child1)
+        child1.modifier = Modifier.graphicsLayer {
+            scaleX = 0.5f
+            scaleY = 0.25f
+            transformOrigin = TransformOrigin(0f, 0f)
+        }
+        val child2 = ZeroSizedLayoutNode()
+        parent.insertAt(0, child2)
+        child2.modifier = Modifier.graphicsLayer {
+            scaleX = 5f
+            scaleY = 2f
+            transformOrigin = TransformOrigin(0f, 0f)
+        }
+        parent.outerLayoutNodeWrapper.measureScope
+            .measure(listOf(parent.outerLayoutNodeWrapper), Constraints())
+        child1.outerLayoutNodeWrapper
+            .measureScope.measure(listOf(child1.outerLayoutNodeWrapper), Constraints())
+        child2.outerLayoutNodeWrapper
+            .measureScope.measure(listOf(child2.outerLayoutNodeWrapper), Constraints())
+        parent.place(0, 0)
+        child1.place(100, 200)
+        child2.place(5, 11)
+
+        val matrix = Matrix()
+        child2.innerLayoutNodeWrapper.transformFrom(child1.innerLayoutNodeWrapper, matrix)
+
+        // (20, 36) should be (10, 9) in real coordinates due to scaling
+        // Translate to (110, 209) in the parent
+        // Translate to (105, 198) in child2's coordinates, discounting scale
+        // Scaled to (21, 99)
+        val offset = matrix.map(Offset(20f, 36f))
+        assertEquals(21f, offset.x, 0.001f)
+        assertEquals(99f, offset.y, 0.001f)
+
+        child1.innerLayoutNodeWrapper.transformFrom(child2.innerLayoutNodeWrapper, matrix)
+        val offset2 = matrix.map(Offset(21f, 99f))
+        assertEquals(20f, offset2.x, 0.001f)
+        assertEquals(36f, offset2.y, 0.001f)
+    }
+
+    @Test
+    fun layoutNodeWrapper_transformFrom_cousins() {
+        val parent = ZeroSizedLayoutNode()
+        parent.attach(MockOwner())
+        val child1 = ZeroSizedLayoutNode()
+        parent.insertAt(0, child1)
+        val child2 = ZeroSizedLayoutNode()
+        parent.insertAt(1, child2)
+
+        val grandChild1 = ZeroSizedLayoutNode()
+        child1.insertAt(0, grandChild1)
+        val grandChild2 = ZeroSizedLayoutNode()
+        child2.insertAt(0, grandChild2)
+
+        parent.place(-100, 10)
+        child1.place(10, 11)
+        child2.place(22, 33)
+        grandChild1.place(45, 27)
+        grandChild2.place(17, 59)
+
+        val matrix = Matrix()
+        grandChild1.innerLayoutNodeWrapper.transformFrom(grandChild2.innerLayoutNodeWrapper, matrix)
+
+        // (17, 59) + (22, 33) - (10, 11) - (45, 27) = (-16, 54)
+        assertEquals(Offset(-16f, 54f), matrix.map(Offset.Zero))
+
+        grandChild2.innerLayoutNodeWrapper.transformFrom(grandChild1.innerLayoutNodeWrapper, matrix)
+
+        assertEquals(Offset(16f, -54f), matrix.map(Offset.Zero))
+    }
+
+    @Test
     fun hitTest_pointerInBounds_pointerInputFilterHit() {
         val pointerInputFilter: PointerInputFilter = mockPointerInputFilter()
         val layoutNode =
@@ -1134,7 +1335,6 @@
     fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
         val semanticsConfiguration = SemanticsConfiguration()
         val semanticsModifier = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val layoutNode =
@@ -1157,7 +1357,6 @@
     fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
         val semanticsConfiguration = SemanticsConfiguration()
         val semanticsModifier = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
@@ -1176,11 +1375,9 @@
     fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
         val semanticsConfiguration = SemanticsConfiguration()
         val semanticsModifier1 = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val semanticsModifier2 = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -1238,11 +1435,9 @@
     fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
         val semanticsConfiguration = SemanticsConfiguration()
         val semanticsModifier1 = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val semanticsModifier2 = object : SemanticsModifier {
-            override val id: Int = 1
             override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
         }
         val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -2384,6 +2579,8 @@
         drawBlock: (Canvas) -> Unit,
         invalidateParentLayer: () -> Unit
     ): OwnedLayer {
+        val transform = Matrix()
+        val inverseTransform = Matrix()
         return object : OwnedLayer {
             override fun updateLayerProperties(
                 scaleX: Float,
@@ -2405,6 +2602,12 @@
                 layoutDirection: LayoutDirection,
                 density: Density
             ) {
+                transform.reset()
+                // This is not expected to be 100% accurate
+                transform.scale(scaleX, scaleY)
+                transform.rotateZ(rotationZ)
+                transform.translate(translationX, translationY)
+                transform.invertTo(inverseTransform)
             }
 
             override fun isInLayer(position: Offset) = true
@@ -2437,6 +2640,14 @@
             ) {
             }
 
+            override fun transform(matrix: Matrix) {
+                matrix.timesAssign(transform)
+            }
+
+            override fun inverseTransform(matrix: Matrix) {
+                matrix.timesAssign(inverseTransform)
+            }
+
             override fun mapOffset(point: Offset, inverse: Boolean) = point
         }
     }
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
index e3d6b20..707ae1b 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
@@ -54,12 +54,18 @@
     }
 
     @Test
-    fun testCreate_success() {
+    fun testCreate_success_enableMlock() {
         val counter: SharedCounter = SharedCounter.create { testFile }
         assertThat(counter).isNotNull()
     }
 
     @Test
+    fun testCreate_success_disableMlock() {
+        val counter: SharedCounter = SharedCounter.create(false) { testFile }
+        assertThat(counter).isNotNull()
+    }
+
+    @Test
     fun testCreate_failure() {
         val tempFile = tempFolder.newFile()
         tempFile.setReadable(false)
@@ -72,13 +78,13 @@
 
     @Test
     fun testGetValue() {
-        val counter: SharedCounter = SharedCounter.create { testFile }
+        val counter: SharedCounter = SharedCounter.create(false) { testFile }
         assertThat(counter.getValue()).isEqualTo(0)
     }
 
     @Test
     fun testIncrementAndGet() {
-        val counter: SharedCounter = SharedCounter.create { testFile }
+        val counter: SharedCounter = SharedCounter.create(false) { testFile }
         for (count in 1..100) {
             assertThat(counter.incrementAndGetValue()).isEqualTo(count)
         }
@@ -86,7 +92,7 @@
 
     @Test
     fun testIncrementInParallel() = runTest {
-        val counter: SharedCounter = SharedCounter.create { testFile }
+        val counter: SharedCounter = SharedCounter.create(false) { testFile }
         val valueToAdd = 100
         val numCoroutines = 10
         val numbers: MutableSet<Int> = mutableSetOf()
@@ -105,4 +111,20 @@
             assertThat(numbers).contains(num)
         }
     }
+
+    @Test
+    fun testManyInstancesWithMlockDisabled() = runTest {
+        // More than 16
+        val numCoroutines = 5000
+        val counters = mutableListOf<SharedCounter>()
+        val deferred = async {
+            repeat(numCoroutines) {
+                val tempFile = tempFolder.newFile()
+                val counter = SharedCounter.create(false) { tempFile }
+                assertThat(counter.getValue()).isEqualTo(0)
+                counters.add(counter)
+            }
+        }
+        deferred.await()
+    }
 }
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
index 892157e..61cddca 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
@@ -35,10 +35,19 @@
 extern "C" {
 
 JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeTruncateFile(
         JNIEnv *env, jclass clazz, jint fd) {
+    if (int errNum = datastore::TruncateFile(fd)) {
+        return ThrowIoException(env, strerror(errNum));
+    }
+    return 0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+        JNIEnv *env, jclass clazz, jint fd, jboolean enable_mlock) {
     void* address = nullptr;
-    if (int errNum = datastore::CreateSharedCounter(fd, &address)) {
+    if (int errNum = datastore::CreateSharedCounter(fd, &address, enable_mlock)) {
         return ThrowIoException(env, strerror(errNum));
     }
     return reinterpret_cast<jlong>(address);
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
index 9dcf9da..68f5ce1 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
@@ -39,16 +39,24 @@
 
 namespace datastore {
 
+int TruncateFile(int fd) {
+    return (ftruncate(fd, NUM_BYTES) == 0) ? 0 : errno;
+}
+
 /*
- * This function returns non-zero errno if fails to create the counter. Caller should use
- * "strerror(errno)" to get error message.
+ * This function returns non-zero errno if fails to create the counter. Caller should have called
+ * "TruncateFile" before calling this method. Caller should use "strerror(errno)" to get error
+ * message.
  */
-int CreateSharedCounter(int fd, void** counter_address) {
-    if (ftruncate(fd, NUM_BYTES) != 0) {
-      return errno;
-    }
-    void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE,
-                           MAP_SHARED | MAP_LOCKED, fd, 0);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock) {
+    // Map with MAP_SHARED so the memory region is shared with other processes.
+    // MAP_LOCKED may cause memory starvation (b/233902124) so is configurable.
+    int map_flags = MAP_SHARED;
+    // TODO(b/233902124): the impact of MAP_POPULATE is still unclear, experiment
+    // with it when possible.
+    map_flags |= enable_mlock ? MAP_LOCKED : MAP_POPULATE;
+
+    void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE, map_flags, fd, 0);
 
     if (mmap_result == MAP_FAILED) {
         return errno;
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
index 756e2fe..cf73095 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
@@ -21,7 +21,8 @@
 #define DATASTORE_SHARED_COUNTER_H
 
 namespace datastore {
-int CreateSharedCounter(int fd, void** counter_address);
+int TruncateFile(int fd);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock);
 uint32_t GetCounterValue(std::atomic<uint32_t>* counter);
 uint32_t IncrementAndGetCounterValue(std::atomic<uint32_t>* counter);
 } // namespace datastore
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index eb64a3c..e01662d 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -25,7 +25,8 @@
  * Put the JNI methods in a separate class to make them internal to the package.
  */
 internal class NativeSharedCounter {
-    external fun nativeCreateSharedCounter(fd: Int): Long
+    external fun nativeTruncateFile(fd: Int): Int
+    external fun nativeCreateSharedCounter(fd: Int, enableMlock: Boolean): Long
     external fun nativeGetCounterValue(address: Long): Int
     external fun nativeIncrementAndGetCounterValue(address: Long): Int
 }
@@ -57,22 +58,28 @@
         fun loadLib() = System.loadLibrary("datastore_shared_counter")
 
         @SuppressLint("SyntheticAccessor")
-        private fun createCounterFromFd(pfd: ParcelFileDescriptor): SharedCounter {
+        private fun createCounterFromFd(
+            pfd: ParcelFileDescriptor,
+            enableMlock: Boolean
+        ): SharedCounter {
             val nativeFd = pfd.getFd()
-            val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd)
+            if (nativeSharedCounter.nativeTruncateFile(nativeFd) != 0) {
+                throw IOException("Failed to truncate counter file")
+            }
+            val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd, enableMlock)
             if (address < 0) {
-                throw IOException("Failed to mmap or truncate counter file")
+                throw IOException("Failed to mmap counter file")
             }
             return SharedCounter(address)
         }
 
-        internal fun create(produceFile: () -> File): SharedCounter {
+        internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
             val file = produceFile()
             return ParcelFileDescriptor.open(
                 file,
                 ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
             ).use {
-                createCounterFromFd(it)
+                createCounterFromFd(it, enableMlock)
             }
         }
     }
diff --git a/development/JetpadClient.py b/development/JetpadClient.py
index 1e15f07..19b74bc 100644
--- a/development/JetpadClient.py
+++ b/development/JetpadClient.py
@@ -33,7 +33,7 @@
         return None
     rawJetpadReleaseOutputLines = rawJetpadReleaseOutput.splitlines()
     if len(rawJetpadReleaseOutputLines) <= 2:
-        print_e("Error: Date %s returned zero results from Jetpad.  Please check your date" % args.date)
+        print_e("Error: Date %s returned zero results from Jetpad.  Please check your date" % date)
         return None
     jetpadReleaseOutput = iter(rawJetpadReleaseOutputLines)
     return jetpadReleaseOutput
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
index 013c163..8ce954a 100755
--- a/development/auto-version-updater/update_versions_for_release.py
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -19,10 +19,6 @@
 import argparse
 from datetime import date
 import subprocess
-from shutil import rmtree
-from shutil import copyfile
-from distutils.dir_util import copy_tree
-from distutils.dir_util import DistutilsFileError
 import toml
 
 # Import the JetpadClient from the parent directory
diff --git a/development/referenceDocs/stageReferenceDocsWithDackka.sh b/development/referenceDocs/stageReferenceDocsWithDackka.sh
index 202d561..f12d43e 100755
--- a/development/referenceDocs/stageReferenceDocsWithDackka.sh
+++ b/development/referenceDocs/stageReferenceDocsWithDackka.sh
@@ -71,7 +71,6 @@
   "androidx/heifwriter"
   "androidx/hilt"
   "androidx/leanback"
-  "androidx/loader"
   "androidx/media"
   "androidx/media2"
   "androidx/mediarouter"
@@ -123,7 +122,6 @@
   "androidx/heifwriter"
   "androidx/hilt"
   "androidx/leanback"
-  "androidx/loader"
   "androidx/media"
   "androidx/media2"
   "androidx/mediarouter"
diff --git a/docs-public/build.gradle b/docs-public/build.gradle
index a2b8326..b4d24d9 100644
--- a/docs-public/build.gradle
+++ b/docs-public/build.gradle
@@ -140,9 +140,9 @@
     docs("androidx.enterprise:enterprise-feedback:1.1.0")
     docs("androidx.enterprise:enterprise-feedback-testing:1.1.0")
     docs("androidx.exifinterface:exifinterface:1.3.3")
-    docs("androidx.fragment:fragment:1.5.0")
-    docs("androidx.fragment:fragment-ktx:1.5.0")
-    docs("androidx.fragment:fragment-testing:1.5.0")
+    docs("androidx.fragment:fragment:1.5.1")
+    docs("androidx.fragment:fragment-ktx:1.5.1")
+    docs("androidx.fragment:fragment-testing:1.5.1")
     docs("androidx.glance:glance:1.0.0-alpha03")
     docs("androidx.glance:glance-appwidget:1.0.0-alpha03")
     docs("androidx.glance:glance-appwidget-proto:1.0.0-alpha03")
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index 348abec..ffc1efc 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -169,6 +169,7 @@
     docs(project(":hilt:hilt-navigation-fragment"))
     docs(project(":hilt:hilt-work"))
     docs(project(":interpolator:interpolator"))
+    docs(project(":javascriptengine:javascriptengine"))
     docs(project(":metrics:metrics-performance"))
     docs(project(":leanback:leanback"))
     docs(project(":leanback:leanback-paging"))
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 982978f..4c8fc2f 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -25,7 +25,7 @@
 
 dependencies {
     api(project(":fragment:fragment"))
-    api("androidx.activity:activity-ktx:1.5.0") {
+    api("androidx.activity:activity-ktx:1.5.1") {
         because "Mirror fragment dependency graph for -ktx artifacts"
     }
     api("androidx.core:core-ktx:1.2.0") {
@@ -34,10 +34,10 @@
     api("androidx.collection:collection-ktx:1.1.0") {
         because "Mirror fragment dependency graph for -ktx artifacts"
     }
-    api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.0") {
+    api("androidx.lifecycle:lifecycle-livedata-core-ktx:2.5.1") {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.savedstate:savedstate-ktx:1.2.0") {
         because 'Mirror fragment dependency graph for -ktx artifacts'
     }
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index 309118a..02e8a8a 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,10 +29,10 @@
     api("androidx.collection:collection:1.1.0")
     api("androidx.viewpager:viewpager:1.0.0")
     api("androidx.loader:loader:1.0.0")
-    api("androidx.activity:activity:1.5.0")
-    api("androidx.lifecycle:lifecycle-livedata-core:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+    api("androidx.activity:activity:1.5.1")
+    api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     api("androidx.savedstate:savedstate:1.2.0")
     api("androidx.annotation:annotation-experimental:1.0.0")
     api(libs.kotlinStdlib)
diff --git a/gradle.properties b/gradle.properties
index 18c3692..c17e4a1 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -24,7 +24,7 @@
 android.experimental.lint.missingBaselineIsEmptyBaseline=true
 
 # Don't generate versioned API files
-androidx.writeVersionedApiFiles=false
+androidx.writeVersionedApiFiles=true
 
 # Don't warn about needing to update AGP
 android.suppressUnsupportedCompileSdk=Tiramisu,33
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 97b429e..dc99e9d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -150,7 +150,7 @@
 multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
 nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
 okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
-okio = { module = "com.squareup.okio:okio", version = "3.0.0" }
+okio = { module = "com.squareup.okio:okio", version = "3.1.0" }
 playCore = { module = "com.google.android.play:core", version = "1.10.3" }
 playServicesBase = { module = "com.google.android.gms:play-services-base", version = "17.0.0" }
 playServicesBasement = { module = "com.google.android.gms:play-services-basement", version = "17.0.0" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 5d3ed73..05c098b 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -323,6 +323,43 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    07D3516820BCF6B1
+uid    Ben Manes <ben.manes@gmail.com>
+
+sub    11F4CE313A637CC1
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBF3HgdMBCAC3ET5ipFXdZ9GGMbtsCQ3HGT40saajsNDOdov2nMJxzKkVe3wk
+sN3bpgbsqBU9ykVkIhX8zV5+v8DOBzkV0pJ2eLjFa9jBPvNjV+KoK2BAI5pzNzYg
+sHPwo1aRXdI0MvCy+7iaIiiGF4/O16AhU4LmALHnaRQZCyuN6VOQ8rlqNvcczwUf
+J2DQeLHqR/tsch7S01hGpPAptBeu19PyAlQsntYN0yLCLKoe9dFXWCDkvd1So5LF
+6So+ryPqupumBbh4WxCmTp9qwDJYJItjAE0zyPe890FurOtxrFTwtRtX6d6qGKkY
+/B4T3r0tTE1EiOUpmSnxmGNItMh7/l5UtnHjABEBAAG0H0JlbiBNYW5lcyA8YmVu
+Lm1hbmVzQGdtYWlsLmNvbT6JAU4EEwEIADgWIQRjXuYnNF88HdQisuIH01FoILz2
+sQUCXceB0wIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAH01FoILz2sdoo
+B/0YUh73jUMl14MjWvp9zrFHN8h+LqB4NMQcP93RdPTtDKi0a+0h8gQtm0D+K49Q
+BQbFztOObfZS3kdJ3VOqmodScWrGtMU3HsYT2ioQalqbYvl9FIPDrlOjHaZgwgyJ
+We0DVKHRApbtIh+NxTpQUJtanxgF60ZtOoToZe8XMGc9LaCZcrFxK/AlMdDMgUCx
+qzBbXhAcvut2bJVL5B4kLNMABrbUuFMjTNI4JxvgTXKL/jNk6XPtCjdmgIh7mT/G
+Mpu9t3i1zegAPdM5N/MAgiGHqm+blANLniSAbZja8Ny7211fwOYoJ546VPwDjL7B
+rBlymB3COoYZhql2DcBBg39cuQENBF3HgdMBCACu3VQKKmagcPbcMZOqbDXE5iK3
+0G742rCpf/j3ywnwTZJQ/58HtAi8+/fXxUhTHswoON2TwiiHrHAkObe+K9A+jv0E
+xjKVMmQ/sOCYWZDEGMth4yJnzDbT1Tlm/l2i5Lv0ZaD7fTEhtprQNuU06dveTeJs
+zDyqtK9T80mvI4+GH59wM80l1y6uj8KA4pY0PdSFgbyS9iAFADGsUsc6t1KiZ5W1
+9odMjDPlQtJ20pm5CvJlDZbYNRJ54CSldZikRvmNRg5mWdRLNfbRMFDLFfcdYLdO
+WJXnAt9cKFJC9P//ItZFrlhu3akTH//HF2kxQNW61Sd92/xtFUD/2tN1GlXfABEB
+AAGJATYEGAEIACAWIQRjXuYnNF88HdQisuIH01FoILz2sQUCXceB0wIbDAAKCRAH
+01FoILz2saySCACibIpnls5wJkfX1B/7tDjWk2hEGZYcASr0xp/DDwSgJ5edByuQ
+NQF7RHuCk0ke6IQGfytMLJlXeEIu79DvgPakxBP5iG+c095FbhRu+9nCEkRqQvop
+4fA7ZdhuerOyuObWz8+o3Z2RywWPXlK+F/9iJiO/qtvmdORuikJtN9VxgvAUvANZ
+RtlzjL296p0TJzGqXhyer46CHl/Yj7TtX6EpnZDgiaQbOWRFOZ5x81xI79bQD7Ew
+DzfrwQHbjQDkqhkwOoV6Wq239ZaHh6p7GXHnQkDMQ0H/7Y2tw6PH5VM8fDJkJKF2
+PIukJrUXa06KqrdZ9YxqvSmu5UY6tMSRwGWp
+=/wFN
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    083891AD4774845A
 uid    Eclipse Project for JAXB <jaxb-dev@eclipse.org>
 
@@ -3715,6 +3752,46 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    5B05CCDE140C2876
+sub    9D29AE4A6B50E01F
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQMuBEwVZOURCADNnKQzSjFuI9/IGj3WTJcPU2B/H8NbZaTsz5WE91WumgZulK2q
+YeD4u6zdOyFK7DEScgxk7dicox9cNEgYKQnQXctDhfqER9bnvA2iJ+AFxjRAWyvs
+en3ClYLXT5UVx0H1ZfDVKCvmaZVirZInfkqbi3OiPQoWrUfu02c3DiHQJ+Y34kdB
+egH2sIShNH8WLfEZ3YDQ4XaWHVuN1C7VwCBM8R3OeTTfyDrTsuyqJ0SeZXRR/6df
+2pEckjF9DNSXyjzFg24MrZhuhgbnj0oR1zmRh1EF+KlBfF4DF4acfxWqqcJVJx/3
+FTtOkLe3Xjj+inyJgxOW52gD4DsJpyf1tIbjAQDZvOdlRRCqZB4FnzzIb/1GmkGD
+JpDLC4MQmqgxkm0n8wgAmmHLpqDTdmuyJXvdX9RdGycpW64sljd1mpzTWJ8UKDhj
+uFQVHSSEdLoHoVj8ItnBV2kXd2uoQ/tWzbxFBST7wE/tX0e9G5XWaPKogvOKeDus
+u9XTIds2krYp80UTYWFZ88oNwGikdIrEYURSYDsYt15miROtKHWbSOHeLVbZqgVx
+dtWPqQVfH4gJGEH97/OSmozqDVog1aZDKBLGZQosng5h4j2RAQpjkaIdxKl8m7CQ
+x0Yi9tA8yD1QhRGggANQIb4n00G/vm7RMU/7NBvvjAQ/nAFjbsyO5oX1rBY1M6Xo
+NFqIBrHSBzV9MmhS3nXU+ZjAktCRhyJ7TsoHM0OYEAf8CduM5Zzp5w02iVYkFQBB
+wAoKHMpycW5LhMMMS4w7gmOfP7y04rtg6+zVe41y6bOqn/SxHCcCgnE/nhdexlzH
+ElYE1H7+HpphoI5vEwS6uElF67CoO5r74Zrb6nshGEj2AoOqjbrsdQm0noBBNYAu
+f9RsjU0sQQFzLW8+2xahqK3oZkLWOkSxzLtVwJbm7EGaGIYxEBjg87OnGQkAi9vv
+tVPwdO3VWyvgKLuPHudLDhTpeH3AMbzKgnru1Pnh/ZpiRhPzsbuFtFPEX8PMuCyE
+n4OLzUALl98kXuPjG5ww+24UsNgKMbKbu8qq/zRu7IHlpZvd730RoCWU2/i18tnY
+zLkCDQRMFWTlEAgA+MQFGIhyA4Ww9g7J8ZiEltwSzRblrjM1q9anexsBIGsWH37A
+92rlVK1RzMVfhj5yl+BzIBGO+zHbgycX7iB5/Fwsm+6R/2Uich6NDm1Qai9rc/jg
+3MS0phOAQzgxlGKOTS2GzdbDJCBQMijDObNe+Cs5DNB/E29/nzzCTQvtRzSeplZN
+r+8Q8lWz6efXmm5EeeZxN4x1YXjjzMJCHbc3yGxOjTgYQOs962yUYsg9UDRJm1OH
+9NKZe1m3dTRIMUcZvL12dq/kyiHHR9V/6CkdiNw1AFMi3tvEdvX4D1k1/Qr/2ORZ
+E4lRzgug4sKkpgaclLnkJZ9EMczmUFTGbbkx3wADBQf/Y+2nZCJSuHiDv/+SdhQh
+OBapZ2hYPDvg29mpPqin/LwH7eFTNv/oos1wzuzGtTHHGEP5mUQLOxjwdAXsWMMj
+scSbCs66ytTN7X4O8qh+1yN7vrM6ZBL12Ix7Ku40cgkWyvTVLBXKaEGm4ElhAmSL
+Fpu+/fJw0riR6rIuwHcGB4R1IJtMWcj+b1odgw9QmJ8AGpHh2WVdXspoCGnTUN4m
+DEswZjplkKXCgLypU13SrHVOqhjd4caK5GNZUfWtCKtwNcJMnvgp2truMvh9BBn6
+widfK48hEknQtXzGjui+bZz2/AD7/OT/T1CqDspB8IQlBCMBn8J4U1grSrZ1wTJf
+HIhnBBgRCAAPBQJMFWTlAhsMBQkLRzUAAAoJEFsFzN4UDCh23wsBANDSDn2KWz7H
+b5geDwUTX4T8Uqn21eFbp54tFTfopCd/AP4nTdX1iahsClr9q6G+CWQBuQWHVmq3
+FlPU/jTn6vXQwA==
+=dKtU
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    5D67BFFCBA1F9A39
 sub    DBE749136BF76809
 -----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -3833,6 +3910,39 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    DEE12B9896F97E34
+sub    9A716F957BC42546
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQENBFAxQKwBCADJGPv6pmFEq0SDwAKESEgCdnXycbR0bNXpNa/3VGboNto1xKgd
+AQ/sI5x+CmN0hpUjklEwff6QIt3MlofEMkAzSfRmTobhJTK9W7r4+p5DuhJpi5Wz
+ITdbNCMT3Cvp13rRE+dx9qY+WFQmTYPf3gq+C6T8Q1i35ePNlCTN2RayaFxxR77D
+W93zKZDdd7I1qH0Vx7GGcSwBgBlEB8jmhNAkz/zAhv53S6px3ZttqYYmuwRtg6Fi
+i/u9VoDR/c9tyUq8L6oAUtg0mo4CP/tfUF/uZnibshEsLzbRP961VQXduhn8HcRp
+k6QPTj37B1vsNWJ9U7XXJ6pYnkizQo7sl5XxABEBAAG5AQ0EUDFArAEIALyNR+z1
+eBBF4S+dOEWKXz2ANmsp6RRhvR09QeQwNycVdbdEXpOiSZUCAkw/EhuJWmHBngat
+0KBO+7CIHyQqwHnqyatizzKXi1OuaEhMzPsQMwPRfYyWHgN0aklc5oOzB2RbSJN4
+et/oVvfAplVSjgR0v+56+qXw9TFlp4kxqFeJLycZ+5ImKQ+XclsBokKuE7cjeF+g
+O5oY/CFHdkxD8d+cLF8FSNUFMypuDQ4IH9zPYGkUJqsb2t67iMyxi14RqyN2YNqK
+JcwxTL42VBlUFlTBoF2Y3w0LNll6pR2WSNvpcj+5/uBjtY1qAj5e7yVts+d1YZsX
+7D76AV742RQ31kkAEQEAAYkCRAQYAQIADwUCUDFArAIbLgUJB4YfgAEpCRDe4SuY
+lvl+NMBdIAQZAQIABgUCUDFArAAKCRCacW+Ve8QlRhFDB/9xE/cXf5fVaLa598xL
+muXiD9U1B04dPdz445/chdDS9iGWBB+5QVvAqv2Jt0hyPN0+n9Mk/4lLStEEL8TP
+NLdTBP1JRvVWC1c+G3kTJq05Abj8CGFFm1UZhFRwCTJ+vrv8fSb15s+YYxBLIUdl
+tKld6OupTHm8A4XJQOhYxd5PHs72bJ3bXs4GmPLKD/RpYmXYJ9EZHQHKnrhZKJ8R
+JKTM6sxBrgdVeI1K0ekA0o5HAVpNEXgY1gG8Pa14jqK0iwlcI02ntqeJkobvv1wN
+vh+nJT2wM5QyLH737kdPrUdi63PfCYLOEHYhI6sFkzI/DAtI/C3wmHtTuRam3aLs
+Rnb7GNQH/i07ndoI4trmUor3X1JBbcjw2BVS+idCtML3jhKtziwK2/kz0rJqBQKa
+Z/zxgEfwkRPqhXLaBW8a1G/d1mGphazHqSaDqylz07XqR31ZtGCc6256anaVbWaW
+9HXUsU5ADNrAK9PdD0EibGB8YumuSTtApICUqN5SVz+h3Mi1MXVsmbiVSAZPzLTD
+0YRwzPJ3jiXIrKDUmZMM7oWwGx6nzW++tW8aKyLKm7x1/y8g+XHvySQiVOKAvvxj
+yPStkEW38Rls5nucpyLzLjoA5vlyIcOkeKCy2jlUmM56YrAIWNn/eCRFPHMOY1DO
+B1nUXMr+2W21xZO+/sWrEEysY0mdGU0=
+=uzFx
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    5F69AD087600B22C
 uid    Eric Bruneton <ebruneton@free.fr>
 
@@ -9496,6 +9606,49 @@
 -----END PGP PUBLIC KEY BLOCK-----
 
 
+pub    D364ABAA39A47320
+sub    3F606403DCA455C8
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+mQINBGH0NlsBEACnLJ3vl/aV+4ytkJ6QSfDFHrwzSo1eEXyuFZ85mLijvgGuaKRr
+c9/lKed0MuyhLJ7YD752kcFCEIyPbjeqEFsBcgU/RWa1AEfaay4eMLBzLSOwCvhD
+m+1zSFswH2bOqeLSbFZPQ9sVIOzO6AInaOTOoecHChHnUztAhRIOIUYmhABJGiu5
+jCP5SStoXm8YtRWT1unJcduHQ51EztQe02k+RTratQ31OSkeJORle7k7cudCS+yp
+z5gTaS1Bx02v0Y8Qaw17vY9Pn8DmsECRvXL6K7ItX6zKkSdJYVGMtiF/kp4rg94I
+XodrlzrMGPGPga9fTcqMPvx/3ffwgIsgtgaKg7te++L3db/xx48XgZ2qYAU8GssE
+N14xRFQmr8sg+QiCIHL0Az88v9mILYOqgxa3RvQ79tTqAKwPg0o2w/wF/WU0Rw53
+mdNy9JTUjetWKuoTmDaXVZO4LQ2g4W2dQTbgHyomiIgV7BnLFUiqOLPo+imruSCs
+W31Arjpb8q6XGTwjySa8waJxHhyV2AvEdAHUIdNuhD4dmPKXszlfFZwXbo1OOuIF
+tUZ9lsOQiCpuO7IpIprLc8L9d1TRnCrfM8kxMbX4KVGajWL+c8FlLnUwR4gSxT1G
+qIgZZ09wL5QiTeGF3biS5mxvn+gF9ns2Ahr2QmMqA2k5AMBTJimmY/OSWwARAQAB
+uQINBGH0NlsBEAC9o6m+D2LubGjOJxLQB1BnfBOkFHadsbkb82QFdrCNsd44fJie
+aqZVP+6XHKVRHSPktwpE1FnjThBJJsLwwcvwWXwDwvED57n4bATPlrPGuG7x+LRV
+bxFBTd+LQUCcHd3puruvbEjQdV54mbgdMqAp5dSA4Fc6h2hMWVBX4EdLiH/0ui3l
+UoqYTJcB73U1/jbKcbs0+cVuXIpmAPQpIs30p0wWLOKiJqn9tTZpwfntnrdfLvKL
+3FZcRQeWZjqH1Ywt4zWlCRqGEp7yVqhK5gn4nfEdSX2koxr53OOsGk2Pjhzs/5XJ
+Li1FTOcnja5kkqOPiPGB/BxAnjPCEsSiOFmF3Af4WdYa3+TK8+ggBSEeLjjLa5zy
+qexfhADwgb5ASZitUErJZDhAvqHGwfz3VPENy3K2kJLH+maWwOT1ZRoJnz3fxwIu
+gKhPx1MzlwhTclIknK7q2CNcB61pC9lg70ICW090NgknE2DtmjrRMONhcSkuWGLZ
+BKBgRqNwITJFcAdg6+ffZzGLsnEd+6A29PdsXfLS9KJqiabvpiBg8RaAAWiv5Tqs
+Nu9YSWUQUzBZO43u8AxTtThuHYZrxasoC3sCGIcRy2V9eaq480DRJ9uotONMutIH
+UDVSdqViPmmit0+PyRiCX/DOeBHumaEOm+RqIxPE8h6W8sHrYAQ7J1a3AQARAQAB
+iQI2BBgBCgAgFiEE7gyocwdAkvgG9Ztl02SrqjmkcyAFAmH0NlsCGwwACgkQ02Sr
+qjmkcyAsehAAps6j+qpjyNGUet/B6Z7nJcobSxnCIP/c+uUPD1oB6Uuht6NTYWQd
+wmEqL5BGz8WNTsBd0cQYvSztrMiz5tCDoiGGrWcgWxrrNxc1EVydhBbT4PpiG6CB
+WFCoEXN76/f0ndxZbjjobElTXbQ6oaLh2812OavgMdiJUVBgXrtfgi5/h49Wpc5o
+/IDM3bfujfrn5nvPIkd7Ee+GaK2YSCT7pfK4N/eW1g1SusqRQxBKCU3C5MVgVjkp
+Ba82U0kTxUGDFYUUcS+Yjhi/w4uynwIXW0pSl5wvxVVxNBfGFH5fkprkpcuVXp9B
+6SRVM85uUoZJFaIFyoAhU9uQQfVe6ugwP9BbhzRzDpJe9tiOcaazwzNnP5Zj31nI
+V6UltZu7mVSl1JwIcWxW3b36p4Ht9G5jIPQc8xS+oMd//p8r4sYFB4KOYas1ukRN
+iCshn9tJfeohkKj9ewxyUNf1rS8uOUJvZC3c3XRF8CJXRpxmHu2pPNf0QxFVhghL
+Y2cJU1OWGi6NyZN65EdfmkTbeDxdlSNv89STD4Vp6MmFtrA4JZDSR0Bp1zEPKiSx
+jpG5FpfVv6lXmFboa5qkXAHG9+bcaRYoXun+wJ3ioWo+cQEdy/bsX03+MHMsms8l
+ikmfPIGVw73RF3HXjJ8GVqTkqbo4ZpgTw/7Z3+fAYE/vxquhnpl2HvE=
+=5tlI
+-----END PGP PUBLIC KEY BLOCK-----
+
+
 pub    D57506CD188FD842
 sub    63F72A7A8658D653
 -----BEGIN PGP PUBLIC KEY BLOCK-----
@@ -10915,3 +11068,50 @@
 Pt2uco8an9pO9/oqU6vlZUr38w==
 =alQS
 -----END PGP PUBLIC KEY BLOCK-----
+
+
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Comment: Hostname:
+Version: Hockeypuck 2.1.0-152-ga266fd3
+
+xsDNBFJQhigBDADpuhND/VUQwJT0nnJxfjAIur59hyaZZ3Ph/KIgmCneyq7lzYO6
+xa1ucH8mqNBVNLLBhs4CjihBddU/ZKTX3WnZyhQKQMZr3Tg+TCNFmAR4/hnZ3NjZ
+N5N5gUj/dqVI2rIvypIuxUApl88BYMsxYpn2+8FKeMd8oBJLqFRJ3WNjB4Op2tRO
+XRWoxs1ypubS/IV1zkphHHpi6VSABlTyTWu4kXEj/1/GpsdtHRa9kvdWw7yKQbnM
+XuwOxtzZFJcyu0P2jYVfHHvxcjxuklc9edmCGdNxgKIoo0LXZOeFIi6OWtwzD0pn
+O6ovJ+PL9QscMdnQlPwsiCwjNUNue20GBv3aUIYc+Z8Gq0SqSan5V0IiKRHMJkzd
+FAhnpkSFBvHhPJn07BCcb1kctqL+xnLxIdi7arq3WNA/6bJjsojc/x3FdIvORIeP
+sqejhtL8mCBvbMAMHSBrFxclMp+HSz2ouHEEPIQam0KeN8t1yEqIy3/aYKMzHj9c
+C3s8XOaBCbJbKpMAEQEAAc09VG9iaWFzIFdhcm5la2UgKGZvciBkZXZlbG9wbWVu
+dCBwdXJwb3NlcykgPHQud2FybmVrZUBnbXgubmV0PsLBFgQTAQgAQAIbDwcLCQgH
+AwIBBhUIAgkKCwQWAgMBAh4BAheAFiEE1HfVGBLmkgEdsR5mpuouK/IuBUMFAl+f
+HewFCREQdggACgkQpuouK/IuBUPAjgv+IvGD8arZP2epxB10nNxehgdB3vVGRvCz
+AWyw/d56KBwGN1czmlHINP/Ejfh4bRZgFXILISqcf+8rATvISsCgKzzfluOfDuFR
+puqZisrlaqEpDqUGK2R8x7kxARaB2G3g4dy6xyJZwv/5dfFPQJ/aQjeNkRSoXI4W
+WLNexZB3E0Gx9a3F32Xvr87vu9GchsoftxQft9joFupRg+kCipQ+w36D9gWmFXtj
+pYT3Wdrm0AcP6lezq+SpcwVn3+DW79p0/WOLhRr6NNQsRBIuM5nNIbCt8hnj9ule
+PZGctzwCTY8suID4Ru18NOiU8NKztoXII7XRloB9v5ezwktKoDzwTBgwm2+XM/vv
+GFlB09LaICdiuPQaiqSZbeLKKmBT1hTEtEHiPdMld2Hlji/rVYS3Ceiv0YUoOnmo
+AAEmtAG7ghpIJxyVtWZchZ55Hrb4oU5AntshrwYMWNRe0toxjQds5Ds2I2lqkjeU
+paUjQXEmPDS1hnckKAxI2PiOeifiLljxwsEWBBMBAgBAAhsPBwsJCAcDAgEGFQgC
+CQoLBBYCAwECHgECF4AWIQTUd9UYEuaSAR2xHmam6i4r8i4FQwUCW5n2GQUJDSpo
+eAAKCRCm6i4r8i4FQ9byDAC6yPry/EBRyJgpWXgLca8Dy56Oe9XtRA+kuAxq+c3q
+GmLy8JdBYxWeBI/dnjwzU6jCLLnY6eTigjSemHZRMPOoyxXYF47LpaoWL52JDi4R
+7xft+GD5Hy+tbDlYW5RVeMzR2Okg3XpvTmsYlcgSr6HCL0L7D25tpcFZMZrls9LN
+z80HetFk4LrR1LvVL8GpFv74xyWullpQU2QwnwXCzUpsXa9qOzwZltNIUfs4gVNG
+KhzfabYmMtlBAXzpi20bRWmJY4W+vGJKC9yWL1L4iu7LrIgMedqsKoMrl4Bg8xKE
+JGU0JEHWgfRopSr0FccP1bxWOaoJ2iN/v3Lifrk0T24vBA9cbTrnQmwrbNftJBLb
+7ccgkvkaFk+8qBe5t/OFgoV5zvmJ6xNEojpFnOtLfrPVpu8b7t3mcGVq1jQJ8afa
+8yIlQrLsA+ubA71pqgdv2ZhoWvL3R2wyxZGMX3xefqavJNxaziHGQorddrg9dyEO
+0xqXKDzjN5vuDTgSJimmZiHCwP8EEwECACkFAlJQhigCGw8FCQlmr/gHCwkIBwMC
+AQYVCAIJCgsEFgIDAQIeAQIXgAAKCRCm6i4r8i4FQ//CC/oD2LxmXHedlqlKl5WU
+EEFoXjDRpcSnfOTFdCn9U5bpBxM2gtlxNB4890TVga6C9kGfgkf9e11/ftdFQgHQ
+2LQKwpRaPOQdfk8Ek/oONmO6x6oIYXrVvY57xsW5AiFHUtPd84NJBoAyTePxstrJ
+TrFo0KQ8wX84rsU2XF/5CRCUuvx+Xomv1ALEed8Ajf9dhY85UTwIWXFINKwMTbNC
+neoBeUy3xugYEYWZCkrIk/iUvwA2pwqCwzHeDRomf1OTwW3VZ0U9/cfFyt3RgkU5
+goF55YOIpnKAjSkyygESaAs4kPrMtAJ6gy8lKsBEpxQfJWH6c5Q6MZn3RVb2S5Dx
+vlpCeiKIqnKtX1DnZrCZntt4Dwrrt4aFemLJ7+iaYndbMun3mAxG6Nqm+CfEOicG
+uTmFS6yakutYNOxJrxtz7yEIIt6yr5T3fQk6LhczhjXpVlvExPutlIsbtVZSsSlE
+lFV5uuVOVYcfjnQJtuUj5JtwP6mhn0Njj/YiJPzG2ugpM0M=
+=BDYe
+-----END PGP PUBLIC KEY BLOCK-----
\ No newline at end of file
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index c8f18a2..f923e74 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -218,6 +218,7 @@
             <trusting group="com.squareup.leakcanary"/>
          </trusted-key>
          <trusted-key id="62c82e50836eb3ee" group="com.github.gundy"/>
+         <trusted-key id="635ee627345f3c1dd422b2e207d3516820bcf6b1" group="com.github.ben-manes.caffeine"/>
          <trusted-key id="6525fd70cc303655" group="org.codehaus.mojo"/>
          <trusted-key id="666a4692ce11b7b3f4eb7b3410066a9707090cf9" group="org.javassist" name="javassist"/>
          <trusted-key id="694621a7227d8d5289699830abe9f3126bb741c1">
@@ -257,6 +258,7 @@
          <trusted-key id="79156e0351af8604de9b186b09a79e1e15a04694" group="org.vafer" name="jdependency"/>
          <trusted-key id="7999befba1039e8b" group="net.bytebuddy"/>
          <trusted-key id="7a8860944fad5f62" group="org.apache.commons"/>
+         <trusted-key id="7c669810892cbd3148fa92995b05ccde140c2876" group="org.eclipse.jgit"/>
          <trusted-key id="7c7d8456294423ba" group="org.objenesis"/>
          <trusted-key id="7cb548acfe3d47e92afa566dc29b11246382a4d7" group="com.charleskorn.kaml"/>
          <trusted-key id="7cd52b5a8295137c88fb5748dddafa7674e54418" group="org.testng" name="testng"/>
@@ -373,6 +375,8 @@
          <trusted-key id="c6f7d1c804c821f49af3bfc13ad93c3c677a106e" group="io.perfmark" name="perfmark-api"/>
          <trusted-key id="c70b844f002f21f6d2b9c87522e44ac0622b91c3" group="com.beust" name="jcommander"/>
          <trusted-key id="c7be5bcc9fec15518cfda882b0f3710fa64900e7">
+            <trusting group="com.google.auto"/>
+            <trusting group="com.google.auto.service"/>
             <trusting group="com.google.auto.value"/>
             <trusting group="com.google.code.gson"/>
          </trusted-key>
@@ -398,6 +402,7 @@
          <trusted-key id="cfae163b64ac9189" group="org.jetbrains.skiko"/>
          <trusted-key id="d041cad2e452550f" group="com.google.protobuf"/>
          <trusted-key id="d196a5e3e70732eeb2e5007f1861c322c56014b2" group="commons-lang"/>
+         <trusted-key id="d477d51812e692011db11e66a6ea2e2bf22e0543" group="io.github.java-diff-utils"/>
          <trusted-key id="d4c89ea4aaf455fd88b22087efe8086f9e93774e" group="junit"/>
          <trusted-key id="d4da5eab3cd7e958" group="com.google.devtools.ksp"/>
          <trusted-key id="d4fb0b7b5e8c18c993a8a386eb9d04a9a679fe18" group="com.uber.nullaway" name="nullaway"/>
@@ -413,6 +418,7 @@
          <trusted-key id="dddafa7674e54418" group="org.testng"/>
          <trusted-key id="e0130a3ed5a2079e" group="org.webjars"/>
          <trusted-key id="e0cb7823cfd00fbf" group="com.jakewharton.android.repackaged"/>
+         <trusted-key id="e0d98c5fd55a8af232290e58dee12b9896f97e34" group="org.pcollections"/>
          <trusted-key id="e16ab52d79fd224f" group="com.google.api.grpc"/>
          <trusted-key id="e62231331bca7e1f292c9b88c1b12a5d99c0729d" group="org.jetbrains"/>
          <trusted-key id="e77417ac194160a3fabd04969a259c7ee636c5ed">
@@ -427,6 +433,7 @@
          <trusted-key id="eb380dc13c39f675" group="com.intellij"/>
          <trusted-key id="eb9d04a9a679fe18" group="com.uber.nullaway"/>
          <trusted-key id="ecdfea3cb4493b94" group="jline"/>
+         <trusted-key id="ee0ca873074092f806f59b65d364abaa39a47320" group="com.google.errorprone"/>
          <trusted-key id="ee9e7dc9d92fc896" group="com.google.errorprone"/>
          <trusted-key id="eef9ecc7d5d90518" group="com.google.dagger"/>
          <trusted-key id="efe8086f9e93774e" group="junit"/>
@@ -461,8 +468,7 @@
       </trusted-keys>
    </configuration>
    <components>
-      <!-- Unsigned -->
-      <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1">
+      <component group="backport-util-concurrent" name="backport-util-concurrent" version="3.1" androidx:reason="Unsigned">
          <artifact name="backport-util-concurrent-3.1.jar">
             <sha256 value="f5759b7fcdfc83a525a036deedcbd32e5b536b625ebc282426f16ca137eb5902" origin="Generated by Gradle"/>
          </artifact>
@@ -470,8 +476,7 @@
             <sha256 value="770471090ca40a17b9e436ee2ec00819be42042da6f4085ece1d37916dc08ff9" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="classworlds" name="classworlds" version="1.1-alpha-2">
+      <component group="classworlds" name="classworlds" version="1.1-alpha-2" androidx:reason="Unsigned">
          <artifact name="classworlds-1.1-alpha-2.jar">
             <sha256 value="2bf4e59f3acd106fea6145a9a88fe8956509f8b9c0fdd11eb96fee757269e3f3" origin="Generated by Gradle"/>
          </artifact>
@@ -479,8 +484,7 @@
             <sha256 value="0cc647963b74ad1d7a37c9868e9e5a8f474e49297e1863582253a08a4c719cb1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gundy/semver4j/issues/6 -->
-      <component group="com.github.gundy" name="semver4j" version="0.16.4">
+      <component group="com.github.gundy" name="semver4j" version="0.16.4" androidx:reason="Unsigned https://github.com/gundy/semver4j/issues/6">
          <artifact name="semver4j-0.16.4-nodeps.jar">
             <sha256 value="3f59eca516374ccd4fd3551625bf50f8a4b191f700508f7ce4866460a6128af0" origin="Generated by Gradle"/>
          </artifact>
@@ -489,8 +493,7 @@
             <sha256 value="32001db2443b339dd21f5b79ff29d1ade722d1ba080c214bde819f0f72d1604d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google" name="google" version="1">
+      <component group="com.google" name="google" version="1" androidx:reason="Unsigned">
          <artifact name="google-1.pom">
             <sha256 value="cd6db17a11a31ede794ccbd1df0e4d9750f640234731f21cff885a9997277e81" origin="Generated by Gradle"/>
          </artifact>
@@ -543,8 +546,7 @@
             <sha256 value="c6898b1f71e69b15bf90c31fc3ef2de1cffbf454a770700f755b5a47ea48b540" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.code.findbugs" name="jsr305" version="1.3.9">
+      <component group="com.google.code.findbugs" name="jsr305" version="1.3.9" androidx:reason="Unsigned">
          <artifact name="jsr305-1.3.9.jar">
             <sha256 value="905721a0eea90a81534abb7ee6ef4ea2e5e645fa1def0a5cd88402df1b46c9ed" origin="Generated by Gradle"/>
          </artifact>
@@ -552,8 +554,7 @@
             <sha256 value="feab9191311c3d7aeef2b66d6064afc80d3d1d52d980fb07ae43c78c987ba93a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.code.findbugs" name="jsr305" version="2.0.1">
+      <component group="com.google.code.findbugs" name="jsr305" version="2.0.1" androidx:reason="Unsigned">
          <artifact name="jsr305-2.0.1.jar">
             <sha256 value="1e7f53fa5b8b5c807e986ba335665da03f18d660802d8bf061823089d1bee468" origin="Generated by Gradle"/>
          </artifact>
@@ -561,8 +562,7 @@
             <sha256 value="02c12c3c2ae12dd475219ff691c82a4d9ea21f44bc594a181295bf6d43dcfbb0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.google.prefab" name="cli" version="2.0.0">
+      <component group="com.google.prefab" name="cli" version="2.0.0" androidx:reason="Unsigned">
          <artifact name="cli-2.0.0-all.jar">
             <sha256 value="d9bd89f68446b82be038aae774771ad85922d0b375209b17625a2734b5317e29" origin="Generated by Gradle"/>
          </artifact>
@@ -570,8 +570,7 @@
             <sha256 value="4856401a263b39c5394b36a16e0d99628cf05c68008a0cda9691c72bb101e1df" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.googlecode.json-simple" name="json-simple" version="1.1">
+      <component group="com.googlecode.json-simple" name="json-simple" version="1.1" androidx:reason="Unsigned">
          <artifact name="json-simple-1.1.jar">
             <sha256 value="2d9484f4c649f708f47f9a479465fc729770ee65617dca3011836602264f6439" origin="Generated by Gradle"/>
          </artifact>
@@ -579,20 +578,17 @@
             <sha256 value="47a89be0fa0fedd476db5fd2c83487654d2a119c391f83a142be876667cf7dab" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
-      <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2">
+      <component group="com.gradle" name="common-custom-user-data-gradle-plugin" version="1.7.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
          <artifact name="common-custom-user-data-gradle-plugin-1.7.2.pom">
             <sha256 value="c70db912c8b127b1b9a6c0cccac1a9353e9fc3b063a3be0114a5208f43c09c31" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/gradle/gradle/issues/20349 -->
-      <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2">
+      <component group="com.gradle" name="gradle-enterprise-gradle-plugin" version="3.10.2" androidx:reason="Unsigned https://github.com/gradle/gradle/issues/20349">
          <artifact name="gradle-enterprise-gradle-plugin-3.10.2.pom">
             <sha256 value="57603c9a75a9ef86ce30b1cb2db728d3cd9caf1be967343f1fc2316c85df5653" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.okio" name="okio" version="2.8.0">
+      <component group="com.squareup.okio" name="okio" version="2.8.0" androidx:reason="Unsigned">
          <artifact name="okio-2.8.0.module">
             <sha256 value="17baab7270389a5fa63ab12811864d0a00f381611bc4eb042fa1bd5918ed0965" origin="Generated by Gradle"/>
          </artifact>
@@ -600,20 +596,17 @@
             <sha256 value="4496b06e73982fcdd8a5393f46e5df2ce2fa4465df5895454cac68a32f09bbc8" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.okio" name="okio" version="2.10.0">
+      <component group="com.squareup.okio" name="okio" version="2.10.0" androidx:reason="Unsigned">
          <artifact name="okio-jvm-2.10.0.jar">
             <sha256 value="a27f091d34aa452e37227e2cfa85809f29012a8ef2501a9b5a125a978e4fcbc1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0">
+      <component group="com.squareup.sqldelight" name="coroutines-extensions-jvm" version="1.3.0" androidx:reason="Unsigned">
          <artifact name="sqldelight-coroutines-extensions-jvm-1.3.0.jar">
             <sha256 value="47305eab44f8b2aef533d8ce76cec9eb5175715cac26b538b6bff5b106ed0ba1" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-grpc-client" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-grpc-client-3.6.0.module">
             <sha256 value="f4d91b43e5ce4603d63842652f063f16c0827abda1922dfb9551a4ac23ba4462" origin="Generated by Gradle"/>
          </artifact>
@@ -621,8 +614,7 @@
             <sha256 value="96904172b35af353e4459786a7d02f1550698cd03b249799ecb563cea3b4c277" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-runtime" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-runtime" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-runtime-3.6.0.module">
             <sha256 value="3b99891842fdec80e7b24ae7f7c485ae41ca35b47c902ca2043cc948aaf58010" origin="Generated by Gradle"/>
          </artifact>
@@ -630,8 +622,7 @@
             <sha256 value="ac41d3f9b8a88046788c6827b0519bf0c53dcc271f598f48aa666c6f5a9523d0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="com.squareup.wire" name="wire-schema" version="3.6.0">
+      <component group="com.squareup.wire" name="wire-schema" version="3.6.0" androidx:reason="Unsigned">
          <artifact name="wire-schema-3.6.0.module">
             <sha256 value="85abd765f2efca0545889c935d8c240e31736a22221231a59bcc4510358b6aaa" origin="Generated by Gradle"/>
          </artifact>
@@ -639,8 +630,7 @@
             <sha256 value="108bc4bafe7024a41460a1a60e72b6a95b69e5afd29c9f11ba7d8e0de2207976" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187 -->
-      <component group="de.undercouch" name="gradle-download-task" version="4.1.1">
+      <component group="de.undercouch" name="gradle-download-task" version="4.1.1" androidx:reason="Invalid signature https://github.com/michel-kraemer/gradle-download-task/issues/187">
          <artifact name="gradle-download-task-4.1.1.jar">
             <ignored-keys>
                <ignored-key id="1fa37fbe4453c1073e7ef61d6449005f96bc97a3" reason="PGP verification failed"/>
@@ -658,8 +648,7 @@
             </sha256>
          </artifact>
       </component>
-      <!-- Unsigned https://github.com/johnrengelman/shadow/issues/760 -->
-      <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1">
+      <component group="gradle.plugin.com.github.johnrengelman" name="shadow" version="7.1.1" androidx:reason="Unsigned https://github.com/johnrengelman/shadow/issues/760">
          <artifact name="shadow-7.1.1.jar">
             <sha256 value="a870861a7a3d54ffd97822051a27b2f1b86dd5c480317f0b97f3b27581b742af" origin="Generated by Gradle"/>
          </artifact>
@@ -667,8 +656,7 @@
             <sha256 value="683be0cd32af9c80a6d4a143b9a6ac2eb45ebc3ccd16db4ca11b94e55fc5e52f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13">
+      <component group="gradle.plugin.com.google.protobuf" name="protobuf-gradle-plugin" version="0.8.13" androidx:reason="Unsigned">
          <artifact name="protobuf-gradle-plugin-0.8.13.jar">
             <sha256 value="8a04b6eee4eab68c73b6e61cc8e00206753691b781d042afbae746f97e8c6f2d" origin="Generated by Gradle"/>
          </artifact>
@@ -676,8 +664,7 @@
             <sha256 value="d8c46016037cda6360561b9c6a21a6c2a4847cad15c3c63903e15328fbcccc45" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.activation" name="activation" version="1.1">
+      <component group="javax.activation" name="activation" version="1.1" androidx:reason="Unsigned">
          <artifact name="activation-1.1.jar">
             <sha256 value="2881c79c9d6ef01c58e62beea13e9d1ac8b8baa16f2fc198ad6e6776defdcdd3" origin="Generated by Gradle"/>
          </artifact>
@@ -685,8 +672,7 @@
             <sha256 value="d490e540a11504b9d71718b1c85fef7b3de6802361290824539b076d58faa8a0" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.annotation" name="jsr250-api" version="1.0">
+      <component group="javax.annotation" name="jsr250-api" version="1.0" androidx:reason="Unsigned">
          <artifact name="jsr250-api-1.0.jar">
             <sha256 value="a1a922d0d9b6d183ed3800dfac01d1e1eb159f0e8c6f94736931c1def54a941f" origin="Generated by Gradle"/>
          </artifact>
@@ -694,8 +680,7 @@
             <sha256 value="548b0ef6f04356ef2283af5140d9404f38fd3891a509d468537abf2f9462944d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.inject" name="javax.inject" version="1">
+      <component group="javax.inject" name="javax.inject" version="1" androidx:reason="Unsigned">
          <artifact name="javax.inject-1.jar">
             <sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/>
          </artifact>
@@ -703,8 +688,7 @@
             <sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="javax.xml.stream" name="stax-api" version="1.0-2">
+      <component group="javax.xml.stream" name="stax-api" version="1.0-2" androidx:reason="Unsigned">
          <artifact name="stax-api-1.0-2.jar">
             <sha256 value="e8c70ebd76f982c9582a82ef82cf6ce14a7d58a4a4dca5cb7b7fc988c80089b7" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
@@ -712,8 +696,7 @@
             <sha256 value="2864f19da84fd52763d75a197a71779b2decbccaac3eb4e4760ffc884c5af4a2" origin="Generated by Gradle because artifact wasn't signed"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9">
+      <component group="me.champeau.gradle" name="japicmp-gradle-plugin" version="0.2.9" androidx:reason="Unsigned">
          <artifact name="japicmp-gradle-plugin-0.2.9.jar">
             <sha256 value="320944e8f3a42a38a5e0f08c6e1e8ae11a63fc82e1f7bf0429a6b7d89d26fac3" origin="Generated by Gradle"/>
          </artifact>
@@ -721,8 +704,7 @@
             <sha256 value="41fc0c243907c241cffa24a06a8cb542747c848ebad5feb6b0413d61b4a0ebc2" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="nekohtml" name="nekohtml" version="1.9.6.2">
+      <component group="nekohtml" name="nekohtml" version="1.9.6.2" androidx:reason="Unsigned">
          <artifact name="nekohtml-1.9.6.2.jar">
             <sha256 value="fdff6cfa9ed9cc911c842a5d2395f209ec621ef1239d46810e9e495809d3ae09" origin="Generated by Gradle"/>
          </artifact>
@@ -730,8 +712,7 @@
             <sha256 value="f5655d331af6afcd4dbaedaa739b889380c771a7e83f7aea5c8544a05074cf0b" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="nekohtml" name="xercesMinimal" version="1.9.6.2">
+      <component group="nekohtml" name="xercesMinimal" version="1.9.6.2" androidx:reason="Unsigned">
          <artifact name="xercesMinimal-1.9.6.2.jar">
             <sha256 value="95b8b357d19f63797dd7d67622fd3f18374d64acbc6584faba1c7759a31e8438" origin="Generated by Gradle"/>
          </artifact>
@@ -739,20 +720,17 @@
             <sha256 value="c219d697fa9c8f243d8f6e347499b6d4e8af1d0cac4bbc7b3907d338a2024c13" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="1">
+      <component group="net.java" name="jvnet-parent" version="1" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-1.pom">
             <sha256 value="281440811268e65d9e266b3cc898297e214e04f09740d0386ceeb4a8923d63bf" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="3">
+      <component group="net.java" name="jvnet-parent" version="3" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-3.pom">
             <sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="4">
+      <component group="net.java" name="jvnet-parent" version="4" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-4.pom">
             <sha256 value="471395735549495297c8ff939b9a32e08b91302020ff773586d27e497abb8fbb" origin="Generated by Gradle"/>
             <!-- Gradle doesn't add keyring files for parent poms so we need to explicitly specify it here to trust -->
@@ -760,14 +738,12 @@
             <pgp value="44fbdbbc1a00fe414f1c1873586654072ead6677"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.java" name="jvnet-parent" version="5">
+      <component group="net.java" name="jvnet-parent" version="5" androidx:reason="Unsigned">
          <artifact name="jvnet-parent-5.pom">
             <sha256 value="1af699f8d9ddab67f9a0d202fbd7915eb0362a5a6dfd5ffc54cafa3465c9cb0a" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="net.sf.kxml" name="kxml2" version="2.3.0">
+      <component group="net.sf.kxml" name="kxml2" version="2.3.0" androidx:reason="Unsigned">
          <artifact name="kxml2-2.3.0.jar">
             <sha256 value="f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2" origin="Generated by Gradle"/>
          </artifact>
@@ -775,8 +751,7 @@
             <sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2">
+      <component group="org.ccil.cowan.tagsoup" name="tagsoup" version="1.2" androidx:reason="Unsigned">
          <artifact name="tagsoup-1.2.jar">
             <sha256 value="10d12b82c9a58a7842765a1152a56fbbd11eac9122a621f5a86a087503297266" origin="Generated by Gradle"/>
          </artifact>
@@ -784,8 +759,7 @@
             <sha256 value="186fd460ee13150e31188703a2c871bf86e20332636f3ede4ab959cd5568da78" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15">
+      <component group="org.codehaus.plexus" name="plexus-utils" version="1.5.15" androidx:reason="Unsigned">
          <artifact name="plexus-utils-1.5.15.jar">
             <sha256 value="2ca121831e597b4d8f2cb22d17c5c041fc23a7777ceb6bfbdd4dfb34bbe7d997" origin="Generated by Gradle"/>
          </artifact>
@@ -793,26 +767,22 @@
             <sha256 value="12a3c9a32b82fdc95223cab1f9d344e14ef3e396da14c4d0013451646f3280e7" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus" version="1.0.4">
+      <component group="org.codehaus.plexus" name="plexus" version="1.0.4" androidx:reason="Unsigned">
          <artifact name="plexus-1.0.4.pom">
             <sha256 value="2242fd02d12b1ca73267fb3d89863025517200e7a4ee426cba4667d0172c74c3" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus" version="2.0.2">
+      <component group="org.codehaus.plexus" name="plexus" version="2.0.2" androidx:reason="Unsigned">
          <artifact name="plexus-2.0.2.pom">
             <sha256 value="e246e2a062b5d989fdefc521c9c56431ba5554ff8d2344edee9218a34a546a33" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14">
+      <component group="org.codehaus.plexus" name="plexus-components" version="1.1.14" androidx:reason="Unsigned">
          <artifact name="plexus-components-1.1.14.pom">
             <sha256 value="381d72c526be217b770f9f8c3f749a86d3b1548ac5c1fcb48d267530ec60d43f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1">
+      <component group="org.codehaus.plexus" name="plexus-container-default" version="1.0-alpha-9-stable-1" androidx:reason="Unsigned">
          <artifact name="plexus-container-default-1.0-alpha-9-stable-1.jar">
             <sha256 value="7c758612888782ccfe376823aee7cdcc7e0cdafb097f7ef50295a0b0c3a16edf" origin="Generated by Gradle"/>
          </artifact>
@@ -820,14 +790,12 @@
             <sha256 value="ef71d45a49edfe76be0f520312a76bc2aae73ec0743a5ffffe10d30122c6e2b2" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3">
+      <component group="org.codehaus.plexus" name="plexus-containers" version="1.0.3" androidx:reason="Unsigned">
          <artifact name="plexus-containers-1.0.3.pom">
             <sha256 value="7c75075badcb014443ee94c8c4cad2f4a9905be3ce9430fe7b220afc7fa3a80f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11">
+      <component group="org.codehaus.plexus" name="plexus-interpolation" version="1.11" androidx:reason="Unsigned">
          <artifact name="plexus-interpolation-1.11.jar">
             <sha256 value="fd9507feb858fa620d1b4aa4b7039fdea1a77e09d3fd28cfbddfff468d9d8c28" origin="Generated by Gradle"/>
          </artifact>
@@ -835,8 +803,7 @@
             <sha256 value="b84d281f59b9da528139e0752a0e1cab0bd98d52c58442b00e45c9748e1d9eee" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-android-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-android-gradle-plugin-0.9.17-g014.jar">
             <sha256 value="64b2e96fd20762351c74f08d598d49c25a490a3b685b8a09446e81d6db36fe81" origin="Generated by Gradle"/>
          </artifact>
@@ -844,8 +811,7 @@
             <sha256 value="956ff381c6c775161a82823bb52d0aa40a8f6a37ab85059f149531f5e5efb7da" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-fatjar" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-fatjar-0.9.17-g014.jar">
             <sha256 value="47cf09501402a101e555588cf5fa9ed83f8572bce9fd60db29e74b5d079628e3" origin="Generated by Gradle"/>
          </artifact>
@@ -853,8 +819,7 @@
             <sha256 value="ceb601f55f14337261fea474bb061407dc0e52146f80d74cd0b43d66febd401f" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014">
+      <component group="org.jetbrains.dokka" name="dokka-gradle-plugin" version="0.9.17-g014" androidx:reason="Unsigned">
          <artifact name="dokka-gradle-plugin-0.9.17-g014.jar">
             <sha256 value="643a7eddeb521832c6021508b7477b603517438481bc06633dca12eb1f339422" origin="Generated by Gradle"/>
          </artifact>
@@ -867,8 +832,7 @@
             <sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm" version="7.0">
+      <component group="org.ow2.asm" name="asm" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-7.0.jar">
             <sha256 value="b88ef66468b3c978ad0c97fd6e90979e56155b4ac69089ba7a44e9aa7ffe9acf" origin="Generated by Gradle"/>
          </artifact>
@@ -876,8 +840,7 @@
             <sha256 value="83f65b1083d5ce4f8ba7f9545cfe9ff17824589c9a7cc82c3a4695801e4f5f68" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-analysis" version="7.0">
+      <component group="org.ow2.asm" name="asm-analysis" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-analysis-7.0.jar">
             <sha256 value="e981f8f650c4d900bb033650b18e122fa6b161eadd5f88978d08751f72ee8474" origin="Generated by Gradle"/>
          </artifact>
@@ -885,8 +848,7 @@
             <sha256 value="c6b54477e9d5bae1e7addff2e24cbf92aaff2ff08fd6bc0596c3933c3fadc2cb" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-commons" version="7.0">
+      <component group="org.ow2.asm" name="asm-commons" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-commons-7.0.jar">
             <sha256 value="fed348ef05958e3e846a3ac074a12af5f7936ef3d21ce44a62c4fa08a771927d" origin="Generated by Gradle"/>
          </artifact>
@@ -894,8 +856,7 @@
             <sha256 value="f4c697886cdb4a5b2472054a0b5e34371e9b48e620be40c3ed48e1f4b6d51eb4" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-tree" version="7.0">
+      <component group="org.ow2.asm" name="asm-tree" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-tree-7.0.jar">
             <sha256 value="cfd7a0874f9de36a999c127feeadfbfe6e04d4a71ee954d7af3d853f0be48a6c" origin="Generated by Gradle"/>
          </artifact>
@@ -903,8 +864,7 @@
             <sha256 value="d39e7dd12f4ff535a0839d1949c39c7644355a4470220c94b76a5c168c57a068" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.ow2.asm" name="asm-util" version="7.0">
+      <component group="org.ow2.asm" name="asm-util" version="7.0" androidx:reason="Unsigned">
          <artifact name="asm-util-7.0.jar">
             <sha256 value="75fbbca440ef463f41c2b0ab1a80abe67e910ac486da60a7863cbcb5bae7e145" origin="Generated by Gradle"/>
          </artifact>
@@ -912,14 +872,12 @@
             <sha256 value="e07bce4bb55d5a06f4c10d912fc9dee8a9b9c04ec549bbb8db4f20db34706f75" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="org.sonatype.oss" name="oss-parent" version="7">
+      <component group="org.sonatype.oss" name="oss-parent" version="7" androidx:reason="Unsigned">
          <artifact name="oss-parent-7.pom">
             <sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Invalid signature -->
-      <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2">
+      <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2" androidx:reason="Invalid signature">
          <artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar">
             <pgp value="db0597e3144342256bc81e3ec727d053c4481cf5"/>
             <sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/>
@@ -933,8 +891,7 @@
             </sha256>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="pull-parser" name="pull-parser" version="2">
+      <component group="pull-parser" name="pull-parser" version="2" androidx:reason="Unsigned">
          <artifact name="pull-parser-2.jar">
             <sha256 value="b20c1e56513faeffb9b01d9d03ba1a36128ac3f9be39b2d0edbe2e240b029d3f" origin="Generated by Gradle"/>
          </artifact>
@@ -942,8 +899,7 @@
             <sha256 value="4823677670797c2b71e8ebbe5437c41151f4e8edb7c6c0d473279715070f36d3" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="xmlpull" name="xmlpull" version="1.1.3.1">
+      <component group="xmlpull" name="xmlpull" version="1.1.3.1" androidx:reason="Unsigned">
          <artifact name="xmlpull-1.1.3.1.jar">
             <sha256 value="34e08ee62116071cbb69c0ed70d15a7a5b208d62798c59f2120bb8929324cb63" origin="Generated by Gradle"/>
          </artifact>
@@ -951,8 +907,7 @@
             <sha256 value="8f10ffd8df0d3e9819c8cc8402709c6b248bc53a954ef6e45470d9ae3a5735fb" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned -->
-      <component group="xpp3" name="xpp3" version="1.1.4c">
+      <component group="xpp3" name="xpp3" version="1.1.4c" androidx:reason="Unsigned">
          <artifact name="xpp3-1.1.4c.jar">
             <sha256 value="0341395a481bb887803957145a6a37879853dd625e9244c2ea2509d9bb7531b9" origin="Generated by Gradle"/>
          </artifact>
@@ -960,8 +915,7 @@
             <sha256 value="4e54622f5dc0f8b6c51e28650268f001e3b55d076c8e3a9d9731c050820c0a3d" origin="Generated by Gradle"/>
          </artifact>
       </component>
-      <!-- Unsigned, b/227204920 -->
-      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10">
+      <component group="" name="kotlin-native-prebuilt-linux-x86_64" version="1.7.10" androidx:reason="Unsigned, b/227204920">
          <artifact name="kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz">
             <sha256 value="f3bd13bc0089fe95609109604d5993a49838828787f15e0e79eef6612b587dc1" origin="Hand-built using sha256sum kotlin-native-prebuilt-linux-x86_64-1.7.10.tar.gz"/>
          </artifact>
diff --git a/graphics/OWNERS b/graphics/OWNERS
index db046a2..9ba9c32 100644
--- a/graphics/OWNERS
+++ b/graphics/OWNERS
@@ -1,5 +1,4 @@
 # Bug component: 1137062
 sumir@google.com
 jreck@google.com
-xxayedawgxx@google.com
-njawad@google.com
\ No newline at end of file
+njawad@google.com
diff --git a/javascriptengine/OWNERS b/javascriptengine/OWNERS
new file mode 100644
index 0000000..e3fd7e0
--- /dev/null
+++ b/javascriptengine/OWNERS
@@ -0,0 +1,2 @@
+abhijithnair@google.com
+torne@google.com
diff --git a/javascriptengine/javascriptengine/api/current.txt b/javascriptengine/javascriptengine/api/current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/public_plus_experimental_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/api/res-current.txt b/javascriptengine/javascriptengine/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/res-current.txt
diff --git a/javascriptengine/javascriptengine/api/restricted_current.txt b/javascriptengine/javascriptengine/api/restricted_current.txt
new file mode 100644
index 0000000..e6f50d0
--- /dev/null
+++ b/javascriptengine/javascriptengine/api/restricted_current.txt
@@ -0,0 +1 @@
+// Signature format: 4.0
diff --git a/javascriptengine/javascriptengine/build.gradle b/javascriptengine/javascriptengine/build.gradle
new file mode 100644
index 0000000..69ad2ab
--- /dev/null
+++ b/javascriptengine/javascriptengine/build.gradle
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+}
+
+dependencies {
+    annotationProcessor(libs.nullaway)
+    // Add dependencies here
+}
+
+android {
+    namespace "androidx.javascriptengine"
+}
+
+androidx {
+    name = "JavaScript Engine"
+    type = LibraryType.PUBLISHED_LIBRARY
+    mavenGroup = LibraryGroups.JAVASCRIPTENGINE
+    inceptionYear = "2022"
+    description = "Javascript Engine is a static library you can add to your Android application in order to evaluate JavaScript."
+}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
similarity index 67%
rename from camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
rename to javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
index fdc9e2d..4082445 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ b/javascriptengine/javascriptengine/src/main/androidx/javascriptengine/package-info.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -14,12 +14,8 @@
  * limitations under the License.
  */
 
-package androidx.camera.integration.uiwidgets.compose.ui.screen.gallery
-
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Composable
-fun GalleryScreen() {
-    Text("Gallery Screen")
-}
\ No newline at end of file
+/**
+ * The androidx.javascriptengine library is a static library you can add to your Android application
+ * in order to evaluate JavaScript.
+ */
+package androidx.javascriptengine;
diff --git a/libraryversions.toml b/libraryversions.toml
index 4895850..5c0c365 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -19,7 +19,7 @@
 CAR_APP = "1.3.0-alpha01"
 COLLECTION = "1.3.0-alpha02"
 COMPOSE = "1.3.0-alpha02"
-COMPOSE_COMPILER = "1.3.0-beta01"
+COMPOSE_COMPILER = "1.3.0-rc01"
 COMPOSE_MATERIAL3 = "1.0.0-alpha15"
 COMPOSE_RUNTIME_TRACING = "1.0.0-alpha01"
 CONTENTPAGER = "1.1.0-alpha01"
@@ -61,6 +61,7 @@
 HILT_NAVIGATION_COMPOSE = "1.1.0-alpha01"
 INSPECTION = "1.0.0"
 INTERPOLATOR = "1.1.0-alpha01"
+JAVASCRIPTENGINE = "1.0.0-alpha01"
 LEANBACK = "1.2.0-alpha03"
 LEANBACK_GRID = "1.0.0-alpha02"
 LEANBACK_PAGING = "1.1.0-alpha10"
@@ -74,7 +75,7 @@
 MEDIA = "1.7.0-alpha01"
 MEDIA2 = "1.3.0-alpha01"
 MEDIAROUTER = "1.4.0-alpha01"
-METRICS = "1.0.0-alpha03"
+METRICS = "1.0.0-alpha04"
 NAVIGATION = "2.6.0-alpha01"
 PAGING = "3.2.0-alpha02"
 PAGING_COMPOSE = "1.0.0-alpha16"
@@ -188,6 +189,7 @@
 INSPECTION = { group = "androidx.inspection", atomicGroupVersion = "versions.INSPECTION" }
 INSPECTION_EXTENSIONS = { group = "androidx.inspection.extensions", atomicGroupVersion = "versions.SQLITE_INSPECTOR" }
 INTERPOLATOR = { group = "androidx.interpolator", atomicGroupVersion = "versions.INTERPOLATOR" }
+JAVASCRIPTENGINE = { group = "androidx.javascriptengine", atomicGroupVersion = "versions.JAVASCRIPTENGINE" }
 LEANBACK = { group = "androidx.leanback" }
 LEGACY = { group = "androidx.legacy" }
 LIBYUV = { group = "libyuv", atomicGroupVersion = "versions.LIBYUV" }
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 1ec1fe6b..8bfb087 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -48,7 +48,7 @@
      * Temporary per-frame to track UI and user state.
      * Unlike the states tracked in `states`, any state in this structure is only valid until
      * the next frame, at which point it is cleared. Any state data added here is automatically
-     * removed; there is no matching "remove" method for [.addSingleFrameState]
+     * removed; there is no matching "remove" method for [.putSingleFrameState]
      *
      * @see putSingleFrameState
      */
@@ -164,7 +164,13 @@
      * State information can be about UI elements that are currently active (such as the current
      * [Activity] or layout) or a user interaction like flinging a list.
      * If the PerformanceMetricsState object already contains an entry with the same key,
-     * the old value is replaced by the new one.
+     * the old value is replaced by the new one. Note that this means apps with several
+     * instances of similar objects (such as multipe `RecyclerView`s) should
+     * therefore use unique keys for these instances to avoid clobbering state values
+     * for other instances and to provide enough information for later analysis which
+     * allows for disambiguation between these objects. For example, using "RVHeaders" and
+     * "RVContent" might be more helpful than just "RecyclerView" for a messaging app using
+     * `RecyclerView` objects for both a headers list and a list of message contents.
      *
      * Some state may be provided automatically by other AndroidX libraries.
      * But applications are encouraged to add user state specific to those applications
diff --git a/navigation/navigation-common/api/current.txt b/navigation/navigation-common/api/current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/current.txt
+++ b/navigation/navigation-common/api/current.txt
@@ -200,6 +200,7 @@
     method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
     method public final void addDeepLink(String uriPattern);
     method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+    method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
     method public final androidx.navigation.NavAction? getAction(@IdRes int id);
     method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
     method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/public_plus_experimental_current.txt b/navigation/navigation-common/api/public_plus_experimental_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/public_plus_experimental_current.txt
+++ b/navigation/navigation-common/api/public_plus_experimental_current.txt
@@ -200,6 +200,7 @@
     method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
     method public final void addDeepLink(String uriPattern);
     method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+    method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
     method public final androidx.navigation.NavAction? getAction(@IdRes int id);
     method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
     method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/api/restricted_current.txt b/navigation/navigation-common/api/restricted_current.txt
index abab5da..3d8788e 100644
--- a/navigation/navigation-common/api/restricted_current.txt
+++ b/navigation/navigation-common/api/restricted_current.txt
@@ -200,6 +200,7 @@
     method public final void addArgument(String argumentName, androidx.navigation.NavArgument argument);
     method public final void addDeepLink(String uriPattern);
     method public final void addDeepLink(androidx.navigation.NavDeepLink navDeepLink);
+    method public final String? fillInLabel(android.content.Context context, android.os.Bundle? bundle);
     method public final androidx.navigation.NavAction? getAction(@IdRes int id);
     method public final java.util.Map<java.lang.String,androidx.navigation.NavArgument> getArguments();
     method public static final kotlin.sequences.Sequence<androidx.navigation.NavDestination> getHierarchy(androidx.navigation.NavDestination);
diff --git a/navigation/navigation-common/build.gradle b/navigation/navigation-common/build.gradle
index c872393..8bc8969 100644
--- a/navigation/navigation-common/build.gradle
+++ b/navigation/navigation-common/build.gradle
@@ -32,10 +32,10 @@
 
 dependencies {
     api("androidx.annotation:annotation:1.1.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.savedstate:savedstate-ktx:1.2.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
     implementation("androidx.core:core-ktx:1.1.0")
     implementation("androidx.collection:collection-ktx:1.1.0")
 
diff --git a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
index 10d30a9..77dcb58 100644
--- a/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
+++ b/navigation/navigation-common/src/main/java/androidx/navigation/NavDestination.kt
@@ -28,6 +28,7 @@
 import androidx.collection.valueIterator
 import androidx.core.content.res.use
 import androidx.navigation.common.R
+import java.util.regex.Pattern
 import kotlin.reflect.KClass
 
 /**
@@ -508,6 +509,50 @@
         return defaultArgs
     }
 
+    /**
+     * Parses a dynamic label containing arguments into a String.
+     *
+     * Supports String Resource arguments by parsing `R.string` values of `ReferenceType`
+     * arguments found in `android:label` into their String values.
+     *
+     * Returns `null` if label is null.
+     *
+     * Returns the original label if the label was a static string.
+     *
+     * @param context Context used to resolve a resource's name
+     * @param bundle Bundle containing the arguments used in the label
+     * @return The parsed string or null if the label is null
+     * @throws IllegalArgumentException if an argument provided in the label cannot be found in
+     * the bundle, or if the label contains a string template but the bundle is null
+     */
+    public fun fillInLabel(context: Context, bundle: Bundle?): String? {
+        val label = label ?: return null
+
+        val fillInPattern = Pattern.compile("\\{(.+?)\\}")
+        val matcher = fillInPattern.matcher(label)
+        val builder = StringBuffer()
+
+        while (matcher.find()) {
+            val argName = matcher.group(1)
+            if (bundle != null && bundle.containsKey(argName)) {
+                matcher.appendReplacement(builder, "")
+                val argType = argName?.let { arguments[argName]?.type }
+                if (argType == NavType.ReferenceType) {
+                    val value = context.getString(bundle.getInt(argName))
+                    builder.append(value)
+                } else {
+                    builder.append(bundle.getString(argName))
+                }
+            } else {
+                throw IllegalArgumentException(
+                    "Could not find \"$argName\" in $bundle to fill label \"$label\""
+                )
+            }
+        }
+        matcher.appendTail(builder)
+        return builder.toString()
+    }
+
     override fun toString(): String {
         val sb = StringBuilder()
         sb.append(javaClass.simpleName)
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index f26a72d..3d7416c 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,18 +28,18 @@
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.compose.foundation:foundation-layout:1.0.1")
-    api("androidx.activity:activity-compose:1.5.0")
+    api("androidx.activity:activity-compose:1.5.1")
     api("androidx.compose.animation:animation:1.0.1")
     api("androidx.compose.runtime:runtime:1.0.1")
     api("androidx.compose.runtime:runtime-saveable:1.0.1")
     api("androidx.compose.ui:ui:1.0.1")
-    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0")
+    api("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
     // old version of common-java8 conflicts with newer version, because both have
     // DefaultLifecycleEventObserver.
     // Outside of androidx this is resolved via constraint added to lifecycle-common,
     // but it doesn't work in androidx.
     // See aosp/1804059
-    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.0"
+    implementation "androidx.lifecycle:lifecycle-common-java8:2.5.1"
     api(projectOrArtifact(":navigation:navigation-runtime-ktx"))
 
     androidTestImplementation(projectOrArtifact(":compose:material:material"))
diff --git a/navigation/navigation-fragment/build.gradle b/navigation/navigation-fragment/build.gradle
index 9a64a90..12ced4a 100644
--- a/navigation/navigation-fragment/build.gradle
+++ b/navigation/navigation-fragment/build.gradle
@@ -23,7 +23,7 @@
 }
 
 dependencies {
-    api("androidx.fragment:fragment-ktx:1.5.0")
+    api("androidx.fragment:fragment-ktx:1.5.1")
     api(project(":navigation:navigation-runtime"))
     api("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
     api(libs.kotlinStdlib)
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 6478105..94c45c4 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -25,9 +25,9 @@
 
 dependencies {
     api(project(":navigation:navigation-common"))
-    api("androidx.activity:activity-ktx:1.5.0")
-    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.0")
-    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0")
+    api("androidx.activity:activity-ktx:1.5.1")
+    api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
+    api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
     api("androidx.annotation:annotation-experimental:1.1.0")
     implementation('androidx.collection:collection:1.0.0')
 
diff --git a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
index 7c4c68e..356161d 100644
--- a/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
+++ b/navigation/navigation-ui/src/androidTest/java/androidx/navigation/ui/NavigationUITest.kt
@@ -17,8 +17,12 @@
 package androidx.navigation.ui
 
 import android.content.Context
+import android.graphics.drawable.Drawable
+import android.os.Bundle
 import androidx.appcompat.widget.Toolbar
 import androidx.core.content.res.TypedArrayUtils.getString
+import androidx.navigation.NavController
+import androidx.navigation.NavDestination
 import androidx.navigation.NavGraph
 import androidx.navigation.NavGraphNavigator
 import androidx.navigation.NavHostController
@@ -51,7 +55,7 @@
 
     @UiThreadTest
     @Test
-    fun navigateWithStringReferenceArgs() {
+    fun navigateWithSingleStringReferenceArg() {
         val context = ApplicationProvider.getApplicationContext<Context>()
         val navController = NavHostController(context)
         navController.navigatorProvider.addNavigator(TestNavigator())
@@ -74,7 +78,155 @@
             endDestination + "/${R.string.dest_title}"
         )
 
-        val expected = context.resources.getString(R.string.dest_title)
+        val expected = "${context.resources.getString(R.string.dest_title)}"
         assertThat(toolbar.title.toString()).isEqualTo(expected)
     }
+
+    @UiThreadTest
+    @Test
+    fun navigateWithMultiStringReferenceArgs() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val navController = NavHostController(context)
+        navController.navigatorProvider.addNavigator(TestNavigator())
+
+        val startDestination = "start_destination"
+        val endDestination = "end_destination"
+
+        navController.graph = navController.createGraph(startDestination = startDestination) {
+            test(startDestination)
+            test("$endDestination/{test}") {
+                label = "start/{test}/end/{test}"
+                argument(name = "test") {
+                    type = NavType.ReferenceType
+                }
+            }
+        }
+
+        val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+        navController.navigate(
+            endDestination + "/${R.string.dest_title}"
+        )
+
+        val argString = context.resources.getString(R.string.dest_title)
+        val expected = "start/$argString/end/$argString"
+        assertThat(toolbar.title.toString()).isEqualTo(expected)
+    }
+
+    @UiThreadTest
+    @Test(expected = IllegalArgumentException::class)
+    fun navigateWithArg_NotFoundInBundleThrows() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val navController = NavHostController(context)
+        navController.navigatorProvider.addNavigator(TestNavigator())
+
+        val startDestination = "start_destination"
+        val endDestination = "end_destination"
+        val labelString = "end/{test}"
+
+        navController.graph = navController.createGraph(startDestination = startDestination) {
+            test(startDestination)
+            test(endDestination) {
+                label = labelString
+            }
+        }
+
+        val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+        // empty bundle
+        val testListener = createToolbarOnDestinationChangedListener(
+            toolbar = toolbar, bundle = Bundle(), context = context, navController = navController
+        )
+
+        // navigate to destination. Since the argument {test} is not present in the bundle,
+        // this should throw an IllegalArgumentException
+        navController.apply {
+            addOnDestinationChangedListener(testListener)
+            navigate(route = endDestination)
+        }
+    }
+
+    @UiThreadTest
+    @Test(expected = IllegalArgumentException::class)
+    fun navigateWithArg_NullBundleThrows() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val navController = NavHostController(context)
+        navController.navigatorProvider.addNavigator(TestNavigator())
+
+        val startDestination = "start_destination"
+        val endDestination = "end_destination"
+        val labelString = "end/{test}"
+
+        navController.graph = navController.createGraph(startDestination = startDestination) {
+            test(startDestination)
+            test("$endDestination/{test}") {
+                label = labelString
+                argument(name = "test") {
+                    type = NavType.ReferenceType
+                }
+            }
+        }
+
+        val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+        // null Bundle
+        val testListener = createToolbarOnDestinationChangedListener(
+            toolbar = toolbar, bundle = null, context = context, navController = navController
+        )
+
+        // navigate to destination, should throw due to template found but null bundle
+        navController.apply {
+            addOnDestinationChangedListener(testListener)
+            navigate(route = endDestination + "/${R.string.dest_title}")
+        }
+    }
+
+    @UiThreadTest
+    @Test
+    fun navigateWithStaticLabel() {
+        val context = ApplicationProvider.getApplicationContext<Context>()
+        val navController = NavHostController(context)
+        navController.navigatorProvider.addNavigator(TestNavigator())
+
+        val startDestination = "start_destination"
+        val endDestination = "end_destination"
+        val labelString = "end/test"
+
+        navController.graph = navController.createGraph(startDestination = startDestination) {
+            test(startDestination)
+            test(endDestination) {
+                label = labelString
+            }
+        }
+
+        val toolbar = Toolbar(context).apply { setupWithNavController(navController) }
+
+        // navigate to destination, static label should be returned directly
+        navController.navigate(route = endDestination)
+        assertThat(toolbar.title.toString()).isEqualTo(labelString)
+    }
+
+    private fun createToolbarOnDestinationChangedListener(
+        toolbar: Toolbar,
+        bundle: Bundle?,
+        context: Context,
+        navController: NavController
+    ): NavController.OnDestinationChangedListener {
+       return object : AbstractAppBarOnDestinationChangedListener(
+            context, AppBarConfiguration.Builder(navController.graph).build()
+        ) {
+            override fun setTitle(title: CharSequence?) {
+                toolbar.title = title
+            }
+
+            override fun onDestinationChanged(
+                controller: NavController,
+                destination: NavDestination,
+                arguments: Bundle?
+            ) {
+                super.onDestinationChanged(controller, destination, bundle)
+            }
+
+            override fun setNavigationIcon(icon: Drawable?, contentDescription: Int) {}
+        }
+    }
 }
diff --git a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
index 2c73ae9..9962d7b 100644
--- a/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
+++ b/navigation/navigation-ui/src/main/java/androidx/navigation/ui/AbstractAppBarOnDestinationChangedListener.kt
@@ -26,10 +26,8 @@
 import androidx.navigation.FloatingWindow
 import androidx.navigation.NavController
 import androidx.navigation.NavDestination
-import androidx.navigation.NavType
 import androidx.navigation.ui.NavigationUI.matchDestinations
 import java.lang.ref.WeakReference
-import java.util.regex.Pattern
 
 /**
  * The abstract OnDestinationChangedListener for keeping any type of app bar updated.
@@ -51,7 +49,6 @@
 
     protected abstract fun setNavigationIcon(icon: Drawable?, @StringRes contentDescription: Int)
 
-    @Suppress("DEPRECATION")
     override fun onDestinationChanged(
         controller: NavController,
         destination: NavDestination,
@@ -65,33 +62,12 @@
             controller.removeOnDestinationChangedListener(this)
             return
         }
-        val label = destination.label
-        if (label != null) {
-            // Fill in the data pattern with the args to build a valid URI
-            val title = StringBuffer()
-            val fillInPattern = Pattern.compile("\\{(.+?)\\}")
-            val matcher = fillInPattern.matcher(label)
-            while (matcher.find()) {
-                val argName = matcher.group(1)
-                if (arguments != null && arguments.containsKey(argName)) {
-                    matcher.appendReplacement(title, "")
 
-                    val argType = argName?.let { destination.arguments[argName]?.type }
-                    if (argType == NavType.ReferenceType) {
-                        val value = context.resources.getString(arguments[argName] as Int)
-                        title.append(value)
-                    } else {
-                        title.append(arguments[argName].toString())
-                    }
-                } else {
-                    throw IllegalArgumentException(
-                        "Could not find \"$argName\" in $arguments to fill label \"$label\""
-                    )
-                }
-            }
-            matcher.appendTail(title)
-            setTitle(title)
+        val label = destination.fillInLabel(context, arguments)
+        if (label != null) {
+            setTitle(label)
         }
+
         val isTopLevelDestination = destination.matchDestinations(topLevelDestinations)
         if (openableLayout == null && isTopLevelDestination) {
             setNavigationIcon(null, 0)
diff --git a/paging/paging-common/build.gradle b/paging/paging-common/build.gradle
index fdc7e4b..8f4aacb5 100644
--- a/paging/paging-common/build.gradle
+++ b/paging/paging-common/build.gradle
@@ -23,6 +23,11 @@
 }
 
 dependencies {
+    // Atomic Group
+    constraints {
+        implementation(project(":paging:paging-runtime"))
+    }
+
     api("androidx.annotation:annotation:1.3.0")
     api("androidx.arch.core:core-common:2.1.0")
     api(libs.kotlinStdlib)
diff --git a/paging/paging-runtime/build.gradle b/paging/paging-runtime/build.gradle
index e7cc847..eb2de3b 100644
--- a/paging/paging-runtime/build.gradle
+++ b/paging/paging-runtime/build.gradle
@@ -31,6 +31,11 @@
 }
 
 dependencies {
+    //Atomic Group
+    constraints {
+        implementation(project(":paging:paging-common"))
+    }
+
     api(project(":paging:paging-common"))
     // Ensure that the -ktx dependency graph mirrors the Java dependency graph
     api(project(":paging:paging-common-ktx"))
diff --git a/recyclerview/recyclerview/api/1.3.0-beta02.txt b/recyclerview/recyclerview/api/1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/1.3.0-beta02.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/current.ignore b/recyclerview/recyclerview/api/current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/current.ignore
+++ b/recyclerview/recyclerview/api/current.ignore
@@ -1,3 +1,5 @@
 // Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
-    Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+    Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+    Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/current.txt b/recyclerview/recyclerview/api/current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/current.txt
+++ b/recyclerview/recyclerview/api/current.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/public_plus_experimental_current.txt b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
index b4c70ae..ba238e0 100644
--- a/recyclerview/recyclerview/api/public_plus_experimental_current.txt
+++ b/recyclerview/recyclerview/api/public_plus_experimental_current.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
+++ b/recyclerview/recyclerview/api/restricted_1.3.0-beta02.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/api/restricted_current.ignore b/recyclerview/recyclerview/api/restricted_current.ignore
index 453fe1d..de0ee4c 100644
--- a/recyclerview/recyclerview/api/restricted_current.ignore
+++ b/recyclerview/recyclerview/api/restricted_current.ignore
@@ -1,3 +1,5 @@
 // Baseline format: 1.0
-RemovedMethod: androidx.recyclerview.widget.SortedListAdapterCallback#SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter):
-    Removed constructor androidx.recyclerview.widget.SortedListAdapterCallback(androidx.recyclerview.widget.RecyclerView.Adapter)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context) parameter #0:
+    Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1)
+InvalidNullConversion: androidx.recyclerview.widget.LinearLayoutManager#LinearLayoutManager(android.content.Context, int, boolean) parameter #0:
+    Attempted to remove @NonNull annotation from parameter arg1 in androidx.recyclerview.widget.LinearLayoutManager(android.content.Context arg1, int arg2, boolean arg3)
diff --git a/recyclerview/recyclerview/api/restricted_current.txt b/recyclerview/recyclerview/api/restricted_current.txt
index a3a2ebf..4086071 100644
--- a/recyclerview/recyclerview/api/restricted_current.txt
+++ b/recyclerview/recyclerview/api/restricted_current.txt
@@ -272,8 +272,8 @@
   }
 
   public class LinearLayoutManager extends androidx.recyclerview.widget.RecyclerView.LayoutManager implements androidx.recyclerview.widget.ItemTouchHelper.ViewDropHandler androidx.recyclerview.widget.RecyclerView.SmoothScroller.ScrollVectorProvider {
-    ctor public LinearLayoutManager(android.content.Context);
-    ctor public LinearLayoutManager(android.content.Context, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
+    ctor public LinearLayoutManager(android.content.Context!);
+    ctor public LinearLayoutManager(android.content.Context!, @androidx.recyclerview.widget.RecyclerView.Orientation int, boolean);
     ctor public LinearLayoutManager(android.content.Context, android.util.AttributeSet?, int, int);
     method protected void calculateExtraLayoutSpace(androidx.recyclerview.widget.RecyclerView.State, int[]);
     method public android.graphics.PointF? computeScrollVectorForPosition(int);
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 8386fcf..771487a 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -156,7 +156,11 @@
      *
      * @param context Current context, will be used to access resources.
      */
-    public LinearLayoutManager(@NonNull Context context) {
+    public LinearLayoutManager(
+            // Suppressed because fixing it requires a source-incompatible change to a very
+            // commonly used constructor, for no benefit: the context parameter is unused
+            @SuppressLint("UnknownNullness") Context context
+    ) {
         this(context, RecyclerView.DEFAULT_ORIENTATION, false);
     }
 
@@ -166,8 +170,13 @@
      *                      #VERTICAL}.
      * @param reverseLayout When set to true, layouts from end to start.
      */
-    public LinearLayoutManager(@NonNull Context context, @RecyclerView.Orientation int orientation,
-            boolean reverseLayout) {
+    public LinearLayoutManager(
+            // Suppressed because fixing it requires a source-incompatible change to a very
+            // commonly used constructor, for no benefit: the context parameter is unused
+            @SuppressLint("UnknownNullness") Context context,
+            @RecyclerView.Orientation int orientation,
+            boolean reverseLayout
+    ) {
         setOrientation(orientation);
         setReverseLayout(reverseLayout);
     }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index e43619f..c500f7e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -36,8 +36,10 @@
 import androidx.room.solver.query.result.QueryResultBinder
 import androidx.room.solver.shortcut.binder.CallableDeleteOrUpdateMethodBinder.Companion.createDeleteOrUpdateBinder
 import androidx.room.solver.shortcut.binder.CallableInsertMethodBinder.Companion.createInsertBinder
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
 import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
 import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
 import androidx.room.solver.transaction.binder.CoroutineTransactionMethodBinder
 import androidx.room.solver.transaction.binder.InstantTransactionMethodBinder
 import androidx.room.solver.transaction.binder.TransactionMethodBinder
@@ -91,6 +93,11 @@
 
     abstract fun findDeleteOrUpdateMethodBinder(returnType: XType): DeleteOrUpdateMethodBinder
 
+    abstract fun findUpsertMethodBinder(
+        returnType: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodBinder
+
     abstract fun findTransactionMethodBinder(
         callType: TransactionMethod.CallType
     ): TransactionMethodBinder
@@ -176,6 +183,11 @@
     override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
         context.typeAdapterStore.findDeleteOrUpdateMethodBinder(returnType)
 
+    override fun findUpsertMethodBinder(
+        returnType: XType,
+        params: List<ShortcutQueryParameter>
+    ) = context.typeAdapterStore.findUpsertMethodBinder(returnType, params)
+
     override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
         InstantTransactionMethodBinder(
             TransactionMethodAdapter(executableElement.jvmName, callType)
@@ -255,6 +267,23 @@
         )
     }
 
+    override fun findUpsertMethodBinder(
+        returnType: XType,
+        params: List<ShortcutQueryParameter>
+    ) = createUpsertBinder(
+        typeArg = returnType,
+        adapter = context.typeAdapterStore.findUpsertAdapter(returnType, params)
+    ) { callableImpl, dbField ->
+        addStatement(
+            "return $T.execute($N, $L, $L, $N)",
+            RoomCoroutinesTypeNames.COROUTINES_ROOM,
+            dbField,
+            "true", // inTransaction
+            callableImpl,
+            continuationParam.name
+        )
+    }
+
     override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
         createDeleteOrUpdateBinder(
             typeArg = returnType,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 2f1aff8..879a7b3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -208,6 +208,11 @@
         params: List<ShortcutQueryParameter>
     ) = delegate.findInsertMethodBinder(returnType, params)
 
+    fun findUpsertMethodBinder(
+        returnType: XType,
+        params: List<ShortcutQueryParameter>
+    ) = delegate.findUpsertMethodBinder(returnType, params)
+
     fun findDeleteOrUpdateMethodBinder(returnType: XType) =
         delegate.findDeleteOrUpdateMethodBinder(returnType)
 }
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 1e1ac0d..cc06a34 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -81,16 +81,22 @@
 import androidx.room.solver.query.result.SingleNamedColumnRowAdapter
 import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
 import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
 import androidx.room.solver.shortcut.binderprovider.DeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.InsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.UpsertMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.InstantDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.InstantInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.InstantUpsertMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
 import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
 import androidx.room.solver.shortcut.result.InsertMethodAdapter
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
 import androidx.room.solver.types.BoxedBooleanToBoxedIntConverter
 import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
 import androidx.room.solver.types.ByteArrayColumnTypeAdapter
@@ -233,6 +239,13 @@
             add(InstantDeleteOrUpdateMethodBinderProvider(context))
         }
 
+    val upsertBinderProviders: List<UpsertMethodBinderProvider> =
+        mutableListOf<UpsertMethodBinderProvider>().apply {
+            addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
+            add(GuavaListenableFutureUpsertMethodBinderProvider(context))
+            add(InstantUpsertMethodBinderProvider(context))
+        }
+
     /**
      * Searches 1 way to bind a value into a statement.
      */
@@ -391,6 +404,15 @@
         }.provide(typeMirror, params)
     }
 
+    fun findUpsertMethodBinder(
+        typeMirror: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodBinder {
+        return upsertBinderProviders.first {
+            it.matches(typeMirror)
+        }.provide(typeMirror, params)
+    }
+
     fun findQueryResultBinder(
         typeMirror: XType,
         query: ParsedQuery,
@@ -432,6 +454,15 @@
         return InsertMethodAdapter.create(typeMirror, params)
     }
 
+    @Suppress("UNUSED_PARAMETER") // param will be used in a future change
+    fun findUpsertAdapter(
+        typeMirror: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodAdapter? {
+        // TODO: change for UpsertMethodAdapter when bind has been created
+        return null
+    }
+
     fun findQueryResultAdapter(
         typeMirror: XType,
         query: ParsedQuery,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
new file mode 100644
index 0000000..37aa163
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.room.solver.shortcut.binder
+
+import androidx.room.ext.CallableTypeSpecBuilder
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder for deferred upsert methods.
+ *
+ * This binder will create a Callable implementation that delegates to the
+ * [UpsertMethodAdapter]. Usage of the Callable impl is then delegate to the [addStmntBlock]
+ * function.
+ */
+class CallableUpsertMethodBinder(
+    val typeArg: XType,
+    val addStmntBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit,
+    adapter: UpsertMethodAdapter?
+) : UpsertMethodBinder(adapter) {
+
+    companion object {
+        fun createUpsertBinder(
+            typeArg: XType,
+            adapter: UpsertMethodAdapter?,
+            addCodeBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit
+        ) = CallableUpsertMethodBinder(typeArg, addCodeBlock, adapter)
+    }
+
+    override fun convertAndReturn(
+        parameters: List<ShortcutQueryParameter>,
+        upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+        dbField: FieldSpec,
+        scope: CodeGenScope
+    ) {
+        val adapterScope = scope.fork()
+        val callableImpl = CallableTypeSpecBuilder(typeArg.typeName) {
+            // TODO add the createMethodBody in UpsertMethodAdapter
+            addCode(adapterScope.generate())
+        }.build()
+
+        scope.builder().apply {
+            addStmntBlock(callableImpl, dbField)
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
new file mode 100644
index 0000000..06860f7
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binder
+
+import androidx.room.ext.N
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder that knows how to write instant (blocking) upsert methods.
+ */
+class InstantUpsertMethodBinder(adapter: UpsertMethodAdapter?) : UpsertMethodBinder(adapter) {
+
+    override fun convertAndReturn(
+        parameters: List<ShortcutQueryParameter>,
+        upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+        dbField: FieldSpec,
+        scope: CodeGenScope
+    ) {
+        scope.builder().apply {
+            addStatement("$N.assertNotSuspendingTransaction()", DaoWriter.dbField)
+        }
+        // TODO: createUpsertionMethodBody
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
index ff8c34f..9ec0168 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
@@ -21,8 +21,33 @@
 import androidx.room.vo.ShortcutQueryParameter
 import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.TypeSpec
-
+/**
+ * Connects the upsert method, the database and the [UpsertMethodAdapter].
+ */
 abstract class UpsertMethodBinder(val adapter: UpsertMethodAdapter?) {
+
+    /**
+     * Received the upsert method parameters, the upsertion adapters and generations the code that
+     * runs the upsert and returns the result.
+     *
+     * For example, for the DAO method:
+     * ```
+     * @Upsert
+     * fun addPublishers(vararg publishers: Publisher): List<Long>
+     * ```
+     * The following code will be generated:
+     *
+     * ```
+     * __db.beginTransaction();
+     * try {
+     *  List<Long> _result = __upsertionAdapterOfPublisher.upsertAndReturnIdsList(publishers);
+     *  __db.setTransactionSuccessful();
+     *  return _result;
+     * } finally {
+     *  __db.endTransaction();
+     * }
+     * ```
+     */
     abstract fun convertAndReturn(
         parameters: List<ShortcutQueryParameter>,
         upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..8f8cee0
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Guava ListenableFuture binders.
+ */
+class GuavaListenableFutureUpsertMethodBinderProvider(
+    private val context: Context
+) : UpsertMethodBinderProvider {
+
+    private val hasGuavaRoom by lazy {
+        context.processingEnv.findTypeElement(RoomGuavaTypeNames.GUAVA_ROOM) != null
+    }
+
+    override fun matches(declared: XType): Boolean =
+        declared.typeArguments.size == 1 &&
+            declared.rawType.typeName == GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
+
+    override fun provide(
+        declared: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodBinder {
+        if (!hasGuavaRoom) {
+            context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
+        }
+
+        val typeArg = declared.typeArguments.first()
+        val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+        return createUpsertBinder(typeArg, adapter) { callableImpl, dbField ->
+            addStatement(
+                "return $T.createListenableFuture($N, $L, $L)",
+                RoomGuavaTypeNames.GUAVA_ROOM,
+                dbField,
+                "true", // inTransaction
+                callableImpl
+            )
+        }
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..4a3ef91
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.solver.shortcut.binder.InstantUpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for instant (blocking) upsert method binder.
+ */
+class InstantUpsertMethodBinderProvider(private val context: Context) : UpsertMethodBinderProvider {
+
+    override fun matches(declared: XType) = true
+
+    override fun provide(
+        declared: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodBinder {
+        return InstantUpsertMethodBinder(
+            context.typeAdapterStore.findUpsertAdapter(declared, params)
+        )
+    }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..df1911e
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XRawType
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.solver.RxType
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Rx Callable binders.
+ */
+open class RxCallableUpsertMethodBinderProvider internal constructor(
+    val context: Context,
+    private val rxType: RxType
+) : UpsertMethodBinderProvider {
+
+    /**
+     * [Single] and [Maybe] are generics but [Completable] is not so each implementation of this
+     * class needs to define how to extract the type argument.
+     */
+    open fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+
+    override fun matches(declared: XType): Boolean =
+        declared.typeArguments.size == 1 && matchesRxType(declared)
+
+    private fun matchesRxType(declared: XType): Boolean {
+        return declared.rawType.typeName == rxType.className
+    }
+
+    override fun provide(
+        declared: XType,
+        params: List<ShortcutQueryParameter>
+    ): UpsertMethodBinder {
+        val typeArg = extractTypeArg(declared)
+        val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+        return CallableUpsertMethodBinder.createUpsertBinder(typeArg, adapter) { callableImpl, _ ->
+            addStatement("return $T.fromCallable($L)", rxType.className, callableImpl)
+        }
+    }
+
+    companion object {
+        fun getAll(context: Context) = listOf(
+            RxCallableUpsertMethodBinderProvider(context, RxType.RX2_SINGLE),
+            RxCallableUpsertMethodBinderProvider(context, RxType.RX2_MAYBE),
+            RxCompletableUpsertMethodBinderProvider(context, RxType.RX2_COMPLETABLE),
+            RxCallableUpsertMethodBinderProvider(context, RxType.RX3_SINGLE),
+            RxCallableUpsertMethodBinderProvider(context, RxType.RX3_MAYBE),
+            RxCompletableUpsertMethodBinderProvider(context, RxType.RX3_COMPLETABLE)
+        )
+    }
+}
+
+private class RxCompletableUpsertMethodBinderProvider(
+    context: Context,
+    rxType: RxType
+) : RxCallableUpsertMethodBinderProvider(context, rxType) {
+
+    private val completableType: XRawType? by lazy {
+        context.processingEnv.findType(rxType.className)?.rawType
+    }
+
+    /**
+     * Since Completable is not a generic, the supported return type should be Void.
+     * Like this, the generated Callable.call method will return Void.
+     */
+    override fun extractTypeArg(declared: XType): XType =
+        context.COMMON_TYPES.VOID
+
+    override fun matches(declared: XType): Boolean = isCompletable(declared)
+
+    private fun isCompletable(declared: XType): Boolean {
+        if (completableType == null) {
+            return false
+        }
+        return declared.rawType.isAssignableFrom(completableType!!)
+    }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..0858f92
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for upsert method binders.
+ */
+interface UpsertMethodBinderProvider {
+
+    /**
+     * Check whether the [XType] can be handled by the [UpsertMethodBinder]
+     */
+    fun matches(declared: XType): Boolean
+
+    /**
+     * Provider of [UpsertMethodBinder], based on the [XType] and the list of parameters
+     */
+    fun provide(declared: XType, params: List<ShortcutQueryParameter>): UpsertMethodBinder
+}
\ No newline at end of file
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 993cdd2..2fa5dbb 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
@@ -56,8 +56,10 @@
 import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
 import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
 import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
 import androidx.room.solver.types.CompositeAdapter
 import androidx.room.solver.types.CustomTypeConverterWrapper
@@ -793,6 +795,69 @@
     }
 
     @Test
+    fun testFindUpsertSingle() {
+        listOf(
+            Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
+            Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
+        ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+            @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
+            runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+                val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+                assertThat(single).isNotNull()
+                assertThat(
+                    RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+                        it.matches(single.type)
+                    }).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun testFindUpsertMaybe() {
+        listOf(
+            Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
+            Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
+        ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+            runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+                val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+                assertThat(
+                    RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+                        it.matches(maybe.type)
+                    }).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun testFindUpsertCompletable() {
+        listOf(
+            Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
+            Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
+        ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+            runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+                val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+                assertThat(
+                    RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+                        it.matches(completable.type)
+                    }).isTrue()
+            }
+        }
+    }
+
+    @Test
+    fun testFindUpsertListenableFuture() {
+        runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE)) {
+                invocation ->
+            val future = invocation.processingEnv
+                .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
+            assertThat(
+                GuavaListenableFutureUpsertMethodBinderProvider(invocation.context).matches(
+                    future.type
+                )).isTrue()
+        }
+    }
+
+    @Test
     fun testFindLiveData() {
         runProcessorTest(
             sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)
diff --git a/settings.gradle b/settings.gradle
index e5d3d62..c5e912a 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -92,9 +92,9 @@
         value("androidx.projects", getRequestedProjectSubsetName() ?: "Unset")
         value("androidx.useMaxDepVersions", providers.gradleProperty("androidx.useMaxDepVersions").isPresent().toString())
 
-        // Do not publish scan for androidx-platform-dev
-        // publishAlways()
-        // publishIfAuthenticated()
+        // Publish scan for androidx-main
+        publishAlways()
+        publishIfAuthenticated()
     }
 }
 
@@ -673,6 +673,7 @@
 includeProject(":inspection:inspection-gradle-plugin", [BuildType.MAIN])
 includeProject(":inspection:inspection-testing", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":interpolator:interpolator", [BuildType.MAIN])
+includeProject(":javascriptengine:javascriptengine", [BuildType.MAIN])
 includeProject(":leanback:leanback", [BuildType.MAIN])
 includeProject(":leanback:leanback-grid", [BuildType.MAIN])
 includeProject(":leanback:leanback-paging", [BuildType.MAIN])
diff --git a/tracing/tracing-perfetto-common/api/current.txt b/tracing/tracing-perfetto-common/api/current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/current.txt
+++ b/tracing/tracing-perfetto-common/api/current.txt
@@ -9,10 +9,10 @@
   public static final class PerfettoHandshake.EnableTracingResponse {
     method public int getExitCode();
     method public String? getMessage();
-    method public String getRequiredVersion();
+    method public String? getRequiredVersion();
     property public final int exitCode;
     property public final String? message;
-    property public final String requiredVersion;
+    property public final String? requiredVersion;
   }
 
   public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
   public static final class PerfettoHandshake.ResponseExitCodes {
     field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
     field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+    field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
     field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
     field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
     field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
+++ b/tracing/tracing-perfetto-common/api/public_plus_experimental_current.txt
@@ -9,10 +9,10 @@
   public static final class PerfettoHandshake.EnableTracingResponse {
     method public int getExitCode();
     method public String? getMessage();
-    method public String getRequiredVersion();
+    method public String? getRequiredVersion();
     property public final int exitCode;
     property public final String? message;
-    property public final String requiredVersion;
+    property public final String? requiredVersion;
   }
 
   public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
   public static final class PerfettoHandshake.ResponseExitCodes {
     field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
     field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+    field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
     field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
     field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
     field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/api/restricted_current.txt b/tracing/tracing-perfetto-common/api/restricted_current.txt
index d830d16..6615b79 100644
--- a/tracing/tracing-perfetto-common/api/restricted_current.txt
+++ b/tracing/tracing-perfetto-common/api/restricted_current.txt
@@ -9,10 +9,10 @@
   public static final class PerfettoHandshake.EnableTracingResponse {
     method public int getExitCode();
     method public String? getMessage();
-    method public String getRequiredVersion();
+    method public String? getRequiredVersion();
     property public final int exitCode;
     property public final String? message;
-    property public final String requiredVersion;
+    property public final String? requiredVersion;
   }
 
   public static final class PerfettoHandshake.ExternalLibraryProvider {
@@ -22,6 +22,7 @@
   public static final class PerfettoHandshake.ResponseExitCodes {
     field public static final androidx.tracing.perfetto.PerfettoHandshake.ResponseExitCodes INSTANCE;
     field public static final int RESULT_CODE_ALREADY_ENABLED = 2; // 0x2
+    field public static final int RESULT_CODE_CANCELLED = 0; // 0x0
     field public static final int RESULT_CODE_ERROR_BINARY_MISSING = 11; // 0xb
     field public static final int RESULT_CODE_ERROR_BINARY_VERIFICATION_ERROR = 13; // 0xd
     field public static final int RESULT_CODE_ERROR_BINARY_VERSION_MISMATCH = 12; // 0xc
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
index 78696b3..d9fc71c 100644
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
@@ -62,7 +62,14 @@
             " $pathExtra " +
             "$targetPackage/$RECEIVER_CLASS_NAME"
         val rawResponse = executeShellCommand(command)
-        return parseResponse(rawResponse)
+
+        return try {
+            parseResponse(rawResponse)
+        } catch (e: IllegalArgumentException) {
+            val message = "Exception occurred while trying to parse a response." +
+                " Error: ${e.message}. Raw response: $rawResponse."
+            EnableTracingResponse(ResponseExitCodes.RESULT_CODE_ERROR_OTHER, null, message)
+        }
     }
 
     private fun parseResponse(rawResponse: String): EnableTracingResponse {
@@ -71,6 +78,10 @@
             .firstOrNull { it.contains("Broadcast completed: result=") }
             ?: throw IllegalArgumentException("Cannot parse: $rawResponse")
 
+        if (line == "Broadcast completed: result=0") return EnableTracingResponse(
+            ResponseExitCodes.RESULT_CODE_CANCELLED, null, null
+        )
+
         val matchResult =
             Regex("Broadcast completed: (result=.*?)(, data=\".*?\")?(, extras: .*)?")
                 .matchEntire(line)
@@ -203,6 +214,15 @@
     }
 
     public object ResponseExitCodes {
+        /**
+         * Indicates that the broadcast resulted in `result=0`, which is an equivalent
+         * of [android.app.Activity.RESULT_CANCELED].
+         *
+         * This most likely means that the app does not expose a [PerfettoHandshake] compatible
+         * receiver.
+         */
+        public const val RESULT_CODE_CANCELLED: Int = 0
+
         public const val RESULT_CODE_SUCCESS: Int = 1
         public const val RESULT_CODE_ALREADY_ENABLED: Int = 2
 
@@ -228,6 +248,7 @@
 
     @Retention(AnnotationRetention.SOURCE)
     @IntDef(
+        ResponseExitCodes.RESULT_CODE_CANCELLED,
         ResponseExitCodes.RESULT_CODE_SUCCESS,
         ResponseExitCodes.RESULT_CODE_ALREADY_ENABLED,
         ResponseExitCodes.RESULT_CODE_ERROR_BINARY_MISSING,
@@ -239,7 +260,15 @@
 
     public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
         @EnableTracingResultCode public val exitCode: Int,
-        public val requiredVersion: String,
+
+        /**
+         * This can be `null` iff we cannot communicate with the broadcast receiver of the target
+         * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
+         * from the receiver. In either case, tracing is unlikely to work under these circumstances,
+         * and more context on how to proceed can be found in [exitCode] or [message] properties.
+         */
+        public val requiredVersion: String?,
+
         public val message: String?
     )
 }
diff --git a/wear/compose/compose-foundation/build.gradle b/wear/compose/compose-foundation/build.gradle
index ef5258f..6c20d7c 100644
--- a/wear/compose/compose-foundation/build.gradle
+++ b/wear/compose/compose-foundation/build.gradle
@@ -35,7 +35,7 @@
 
         implementation(libs.kotlinStdlib)
         implementation(project(":compose:foundation:foundation-layout"))
-        implementation(project(":profileinstaller:profileinstaller"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         testImplementation(libs.testRules)
         testImplementation(libs.testRunner)
diff --git a/wear/compose/compose-material/build.gradle b/wear/compose/compose-material/build.gradle
index 76fdfda..e67002e 100644
--- a/wear/compose/compose-material/build.gradle
+++ b/wear/compose/compose-material/build.gradle
@@ -40,7 +40,7 @@
         implementation(project(":compose:material:material-ripple"))
         implementation(project(":compose:ui:ui-util"))
         implementation(project(":wear:compose:compose-foundation"))
-        implementation(project(":profileinstaller:profileinstaller"))
+        implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
         androidTestImplementation(project(":compose:ui:ui-test"))
         androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/wear/compose/compose-navigation/build.gradle b/wear/compose/compose-navigation/build.gradle
index 1581111..bc7a4e8 100644
--- a/wear/compose/compose-navigation/build.gradle
+++ b/wear/compose/compose-navigation/build.gradle
@@ -32,7 +32,7 @@
 
     implementation(libs.kotlinStdlib)
     implementation("androidx.navigation:navigation-compose:2.4.0")
-    implementation(project(":profileinstaller:profileinstaller"))
+    implementation("androidx.profileinstaller:profileinstaller:1.2.0")
 
     androidTestImplementation(project(":compose:test-utils"))
     androidTestImplementation(project(":compose:ui:ui-test-junit4"))
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
index 4d95812..a940dba 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
@@ -28,6 +28,7 @@
 import androidx.test.filters.MediumTest;
 import androidx.test.filters.SdkSuppress;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -61,6 +62,7 @@
      * Test the algorithmic darkening on web content that doesn't support dark style.
      */
     @Test
+    @Ignore("b/235864049")  // Find a way to run with targetSdk T
     public void testSimplifiedDarkMode_rendersDark() throws Throwable {
         WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
         WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 6808524..d67a87b 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -40,7 +40,7 @@
     public WebSettingsCompatLightThemeTest() {
         // targetSdkVersion to T, it is min version the algorithmic darkening works.
         // TODO(http://b/214741472): Use VERSION_CODES.TIRAMISU once available.
-        super(WebViewLightThemeTestActivity.class, VERSION_CODES.CUR_DEVELOPMENT);
+        super(WebViewLightThemeTestActivity.class, 33);
     }
 
     /**
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 1b5b5ac..a917e8e 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -25,6 +25,7 @@
 import static org.hamcrest.Matchers.greaterThan;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -37,6 +38,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteException;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -199,6 +201,30 @@
     }
 
     @Test
+    public void test_InitializationExceptionHandler_migrationFailures() {
+        mContext = mock(Context.class);
+        when(mContext.getApplicationContext()).thenReturn(mContext);
+        mWorkDatabase = WorkDatabase.create(mContext, mConfiguration.getTaskExecutor(), true);
+        when(mWorkManager.getWorkDatabase()).thenReturn(mWorkDatabase);
+        mRunnable = new ForceStopRunnable(mContext, mWorkManager);
+
+        InitializationExceptionHandler handler = mock(InitializationExceptionHandler.class);
+        Configuration configuration = new Configuration.Builder(mConfiguration)
+                .setInitializationExceptionHandler(handler)
+                .build();
+
+        when(mWorkManager.getConfiguration()).thenReturn(configuration);
+        // This is what WorkDatabasePathHelper uses under the hood to migrate the database.
+        when(mContext.getDatabasePath(anyString())).thenThrow(
+                new SQLiteException("Unable to migrate database"));
+
+        ForceStopRunnable runnable = spy(mRunnable);
+        doNothing().when(runnable).sleep(anyLong());
+        runnable.run();
+        verify(handler, times(1)).handleException(any(Throwable.class));
+    }
+
+    @Test
     public void test_completeOnMultiProcessChecks() {
         ForceStopRunnable runnable = spy(mRunnable);
         doReturn(false).when(runnable).multiProcessChecks();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 805b8ee..cf9679b 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -38,6 +38,7 @@
 import android.database.sqlite.SQLiteConstraintException;
 import android.database.sqlite.SQLiteDatabaseCorruptException;
 import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteException;
 import android.database.sqlite.SQLiteTableLockedException;
 import android.os.Build;
 import android.text.TextUtils;
@@ -102,8 +103,29 @@
                 return;
             }
             while (true) {
-                // Migrate the database to the no-backup directory if necessary.
-                WorkDatabasePathHelper.migrateDatabase(mContext);
+
+                try {
+                    // Migrate the database to the no-backup directory if necessary.
+                    // Migrations are not retry-able. So if something unexpected were to happen
+                    // here, the best we can do is to hand things off to the
+                    // InitializationExceptionHandler.
+                    WorkDatabasePathHelper.migrateDatabase(mContext);
+                } catch (SQLiteException sqLiteException) {
+                    // This should typically never happen.
+                    String message = "Unexpected SQLite exception during migrations";
+                    Logger.get().error(TAG, message);
+                    IllegalStateException exception =
+                            new IllegalStateException(message, sqLiteException);
+                    InitializationExceptionHandler exceptionHandler =
+                            mWorkManager.getConfiguration().getInitializationExceptionHandler();
+                    if (exceptionHandler != null) {
+                        exceptionHandler.handleException(exception);
+                        break;
+                    } else {
+                        throw exception;
+                    }
+                }
+
                 // Clean invalid jobs attributed to WorkManager, and Workers that might have been
                 // interrupted because the application crashed (RUNNING state).
                 Logger.get().debug(TAG, "Performing cleanup operations.");
@@ -111,11 +133,11 @@
                     forceStopRunnable();
                     break;
                 } catch (SQLiteCantOpenDatabaseException
-                        | SQLiteDatabaseCorruptException
-                        | SQLiteDatabaseLockedException
-                        | SQLiteTableLockedException
-                        | SQLiteConstraintException
-                        | SQLiteAccessPermException exception) {
+                         | SQLiteDatabaseCorruptException
+                         | SQLiteDatabaseLockedException
+                         | SQLiteTableLockedException
+                         | SQLiteConstraintException
+                         | SQLiteAccessPermException exception) {
                     mRetryCount++;
                     if (mRetryCount >= MAX_ATTEMPTS) {
                         // ForceStopRunnable is usually the first thing that accesses a database