Merge "Version bump to alpha04" into androidx-main
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/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/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/gallery/GalleryScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
deleted file mode 100644
index fdc9e2d..0000000
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ /dev/null
@@ -1,25 +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.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
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/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/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/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index cdecf1d..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
@@ -74,15 +74,11 @@
     LazyLayoutItemProvider by LazyLayoutItemProvider(
         intervals = intervals,
         nearestItemsRange = nearestItemsRange,
-        itemContent = itemContentProvider(itemScope)
+        itemContent = { interval: LazyListIntervalContent, index: Int ->
+            interval.item.invoke(itemScope, index)
+        }
     )
 
-// Workaround for compiler crash
-private fun itemContentProvider(itemScope: LazyItemScope) =
-    @Composable { 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.
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/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 b127774a..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/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 7996078..6d14189 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1906,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();
@@ -1917,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;
   }
@@ -2770,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 707c956..32bc836 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2052,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();
@@ -2063,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;
   }
@@ -2983,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 928392d..2b300fe 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1906,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();
@@ -1917,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;
   }
@@ -2806,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/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/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/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/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/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/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index a40a3f6..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
@@ -1335,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 =
@@ -1358,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()) }
@@ -1377,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))
@@ -1439,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))
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/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 97c3e80..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") {
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index c9e0c84..02e8a8a 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,7 +29,7 @@
     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.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")
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-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 3a2b25f..3d7416c 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,7 +28,7 @@
 
     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")
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 70c2046..94c45c4 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -25,7 +25,7 @@
 
 dependencies {
     api(project(":navigation:navigation-common"))
-    api("androidx.activity:activity-ktx:1.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")
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/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 720e795..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
@@ -260,7 +260,15 @@
 
     public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
         @EnableTracingResultCode public val exitCode: Int,
+
+        /**
+         * 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/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/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -2,6 +2,7 @@
 package androidx.window.extensions {
 
   public interface WindowExtensions {
+    method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
@@ -12,6 +13,90 @@
 
 }
 
+package androidx.window.extensions.embedding {
+
+  public interface ActivityEmbeddingComponent {
+    method public boolean isActivityEmbedded(android.app.Activity);
+    method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+    method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+  }
+
+  public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+    method public boolean shouldAlwaysExpand();
+  }
+
+  public static final class ActivityRule.Builder {
+    ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+    method public androidx.window.extensions.embedding.ActivityRule build();
+    method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+  }
+
+  public class ActivityStack {
+    ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+    method public java.util.List<android.app.Activity!> getActivities();
+    method public boolean isEmpty();
+  }
+
+  public abstract class EmbeddingRule {
+  }
+
+  public class SplitInfo {
+    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+    method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+    method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+    method public float getSplitRatio();
+  }
+
+  public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+    method public int getFinishPrimaryWithSecondary();
+    method public int getFinishSecondaryWithPrimary();
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+    method public boolean shouldClearTop();
+  }
+
+  public static final class SplitPairRule.Builder {
+    ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+    method public androidx.window.extensions.embedding.SplitPairRule build();
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+  }
+
+  public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+    method public int getFinishPrimaryWithSecondary();
+    method public android.content.Intent getPlaceholderIntent();
+    method public boolean isSticky();
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+  }
+
+  public static final class SplitPlaceholderRule.Builder {
+    ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+  }
+
+  public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+    method public int getLayoutDirection();
+    method public float getSplitRatio();
+    field public static final int FINISH_ADJACENT = 2; // 0x2
+    field public static final int FINISH_ALWAYS = 1; // 0x1
+    field public static final int FINISH_NEVER = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.layout {
 
   public interface DisplayFeature {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6a28e00..c067593 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -1,11 +1,8 @@
 // Signature format: 4.0
 package androidx.window.extensions {
 
-  @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalWindowExtensionsApi {
-  }
-
   public interface WindowExtensions {
-    method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
+    method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
@@ -18,13 +15,13 @@
 
 package androidx.window.extensions.embedding {
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface ActivityEmbeddingComponent {
+  public interface ActivityEmbeddingComponent {
     method public boolean isActivityEmbedded(android.app.Activity);
     method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
     method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+  public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
     method public boolean shouldAlwaysExpand();
@@ -36,23 +33,23 @@
     method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityStack {
+  public class ActivityStack {
     ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
     method public java.util.List<android.app.Activity!> getActivities();
     method public boolean isEmpty();
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class EmbeddingRule {
+  public abstract class EmbeddingRule {
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitInfo {
+  public class SplitInfo {
     ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
     method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
     method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
     method public float getSplitRatio();
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+  public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
     method public int getFinishPrimaryWithSecondary();
     method public int getFinishSecondaryWithPrimary();
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
@@ -72,7 +69,7 @@
     method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+  public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
     method public int getFinishPrimaryWithSecondary();
     method public android.content.Intent getPlaceholderIntent();
     method public boolean isSticky();
@@ -89,7 +86,7 @@
     method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
   }
 
-  @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+  public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
     method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
     method public int getLayoutDirection();
     method public float getSplitRatio();
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -2,6 +2,7 @@
 package androidx.window.extensions {
 
   public interface WindowExtensions {
+    method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
     method public default int getVendorApiLevel();
     method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
   }
@@ -12,6 +13,90 @@
 
 }
 
+package androidx.window.extensions.embedding {
+
+  public interface ActivityEmbeddingComponent {
+    method public boolean isActivityEmbedded(android.app.Activity);
+    method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+    method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+  }
+
+  public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+    method public boolean shouldAlwaysExpand();
+  }
+
+  public static final class ActivityRule.Builder {
+    ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+    method public androidx.window.extensions.embedding.ActivityRule build();
+    method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+  }
+
+  public class ActivityStack {
+    ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+    method public java.util.List<android.app.Activity!> getActivities();
+    method public boolean isEmpty();
+  }
+
+  public abstract class EmbeddingRule {
+  }
+
+  public class SplitInfo {
+    ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+    method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+    method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+    method public float getSplitRatio();
+  }
+
+  public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+    method public int getFinishPrimaryWithSecondary();
+    method public int getFinishSecondaryWithPrimary();
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+    method public boolean shouldClearTop();
+  }
+
+  public static final class SplitPairRule.Builder {
+    ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+    method public androidx.window.extensions.embedding.SplitPairRule build();
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+    method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+    method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+  }
+
+  public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+    method public int getFinishPrimaryWithSecondary();
+    method public android.content.Intent getPlaceholderIntent();
+    method public boolean isSticky();
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+  }
+
+  public static final class SplitPlaceholderRule.Builder {
+    ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+    method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+  }
+
+  public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+    method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+    method public int getLayoutDirection();
+    method public float getSplitRatio();
+    field public static final int FINISH_ADJACENT = 2; // 0x2
+    field public static final int FINISH_ALWAYS = 1; // 0x1
+    field public static final int FINISH_NEVER = 0; // 0x0
+  }
+
+}
+
 package androidx.window.extensions.layout {
 
   public interface DisplayFeature {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
deleted file mode 100644
index 0da12db..0000000
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
+++ /dev/null
@@ -1,26 +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.window.extensions;
-
-import androidx.annotation.RequiresOptIn;
-
-/**
- * Denotes that the API uses experimental WindowManager extension APIs.
- */
-@RequiresOptIn
-public @interface ExperimentalWindowExtensionsApi {
-}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 6ac25bd..c1a9bd8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -58,6 +58,7 @@
      * @return the OEM implementation of {@link ActivityEmbeddingComponent}
      */
     @Nullable
-    @ExperimentalWindowExtensionsApi
-    ActivityEmbeddingComponent getActivityEmbeddingComponent();
+    default ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+        return null;
+    }
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index f4dd8a4..e87736c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -19,7 +19,6 @@
 import android.app.Activity;
 
 import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.util.List;
 import java.util.Set;
@@ -32,7 +31,6 @@
  * <p>This interface should be implemented by OEM and deployed to the target devices.
  * @see androidx.window.extensions.WindowExtensions
  */
-@ExperimentalWindowExtensionsApi
 public interface ActivityEmbeddingComponent {
 
     /**
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
index dbf8233..2d1fc33 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
@@ -23,14 +23,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.util.function.Predicate;
 
 /**
  * Split configuration rule for individual activities.
  */
-@ExperimentalWindowExtensionsApi
 public class ActivityRule extends EmbeddingRule {
     @NonNull
     private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 928a7b9..db275bb 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
@@ -19,7 +19,6 @@
 import android.app.Activity;
 
 import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -28,7 +27,6 @@
  * Description of a group of activities stacked on top of each other and shown as a single
  * container, all within the same task.
  */
-@ExperimentalWindowExtensionsApi
 public class ActivityStack {
     @NonNull
     private final List<Activity> mActivities;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index beac689..571eda0 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -16,13 +16,10 @@
 
 package androidx.window.extensions.embedding;
 
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
-
 /**
  * Base interface for activity embedding rules. Used to group different types of rules together when
  * updating from the core library.
  */
-@ExperimentalWindowExtensionsApi
 public abstract class EmbeddingRule {
     EmbeddingRule() {}
 }
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
index 351fd1b..cfd8b1a 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
@@ -17,10 +17,8 @@
 package androidx.window.extensions.embedding;
 
 import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 /** Describes a split of two containers with activities. */
-@ExperimentalWindowExtensionsApi
 public class SplitInfo {
     @NonNull
     private final ActivityStack mPrimaryActivityStack;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index 2cbb747..db9f65c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -25,14 +25,12 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.util.function.Predicate;
 
 /**
  * Split configuration rules for activity pairs.
  */
-@ExperimentalWindowExtensionsApi
 public class SplitPairRule extends SplitRule {
     @NonNull
     private final Predicate<Pair<Activity, Activity>> mActivityPairPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
index 83bfc32..1f8a8fe 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
@@ -24,7 +24,6 @@
 
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.util.function.Predicate;
 
@@ -32,7 +31,6 @@
  * Split configuration rules for split placeholders - activities used to occupy additional
  * available space on the side before the user selects content to show.
  */
-@ExperimentalWindowExtensionsApi
 public class SplitPlaceholderRule extends SplitRule {
     @NonNull
     private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 4b1d4e5..39b7df8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -24,7 +24,6 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.NonNull;
 import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -37,7 +36,6 @@
  * new activities started from the same process automatically by the embedding implementation on
  * the device.
  */
-@ExperimentalWindowExtensionsApi
 public abstract class SplitRule extends EmbeddingRule {
     @NonNull
     private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
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